SSSwap

First Flight #41
Beginner FriendlyRust
100 EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Rounding Errors in Swap Output Lead to User Fund Loss

Summary

The swap_exact_in and swap_exact_out functions in swap_operations.rs perform token swaps using integer division without additional validation for rounding losses. As a result, when the input amount is too small relative to the reserves, the computed output can round down to zero (amount_out = 0), leading to direct loss of user funds. Even for moderate swaps, the use of floor division causes truncation of fractional values, leading to unaccounted slippage and potential exploit scenarios.

Vulnerability Details

In both swap_exact_in and swap_exact_out, the output amount is calculated using the constant product formula:

let numerator: u128 = (reserve_out as u128).checked_mul(amount_in as u128)?;
let denominator: u128 = (reserve_in as u128).checked_add(amount_in as u128)?;
let amount_out_u128: u128 = numerator.checked_div(denominator)?;
let amount_out: u64 = amount_out_u128.try_into()?;


This uses integer division (checked_div), effectively applying a floor operation that discards any fractional part of the division result. The contract does not check whether amount_out == 0 nor does it require a minimum input to ensure meaningful output.

When amount_in is small compared to reserve_in, this calculation can result in amount_out == 0, causing the user to send tokens and receive nothing. Even when amount_out > 0, the truncation introduces unaccounted loss.

Impact

  • Loss of funds for small swaps: Users performing small swaps may receive zero tokens in return.

  • Rounding loss for moderate swaps: Even medium-sized swaps incur additional hidden slippage due to truncation.

  • Exploitability: An attacker can repeatedly submit micro-swaps (e.g., amount_in = 1), accumulating tokens in one reserve without giving any in return, skewing the pool.

  • Denial of Service: Such manipulations can destabilize the pool, leading to extreme slippage and discouraging further use.

Example 1: Zero output

With reserve_A = 500, reserve_B = 500, and amount_in = 1:

amount_out = floor(500 × 1 / (500 + 1)) = floor(500 / 501) = 0;

User sends 1 token and receives nothing.

Example 2: Rounding loss

With reserve_A = 1,000, reserve_B = 1,000, and amount_in = 50:

amount_out = floor(1000 × 50 / (1000 + 50)) = floor(50000 / 1050) = 47

User should receive 47.619 tokens, but gets only 47 due to truncation.


Proof of Concept

  1. Deploy the contract without rounding protections.

  2. Initialize pool with reserve_A = 500, reserve_B = 500.

  3. Execute swap_exact_in(amount_in = 1).

  4. Observe that amount_out = 0. User receives nothing and pool becomes 501 A / 500 B.

  5. Execute swap_exact_in(amount_in = 50) on the same pool.

  6. Observe amount_out = 47, while a full-precision calculation would yield ≈ 47.619.

Tools Used

Manual review.

Recommendations

  • Reject swaps with zero output:

if amount_out == 0 {
return Err(AmmError::InsufficientOutputAmount.into());
}

Add InsufficientOutputAmount to AmmError.


  • Require minimum amount_in relative to reserve:

let min_swap: u64 = reserve_in.checked_div(1000).unwrap_or(1);
require!(
amount_in >= min_swap,
AmmError::AmountBelowMinimum
);

Define AmountBelowMinimum in AmmError.


  • User interface warning: Inform users that small swaps may result in zero output due to rounding.

Updates

Lead Judging Commences

0xtimefliez Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.