SSSwap

First Flight #41
Beginner FriendlyRust
100 EXP
View results
Submission Details
Severity: medium
Valid

Fee Calculation Precision Loss Leading to Zero Fees in Swap Operations

Root + Impact

Description

Normal Behavior: The swap functions are designed to charge a 0.3% LP fee (3/1000) on the output amount of each swap operation. This fee should be calculated as (amount_out * 3) / 1000, deducted from the user's output, and effectively retained in the pool to benefit liquidity providers.

Specific Issue: Due to integer division with floor behavior in the fee calculation (amount_out as u128 * 3).div_floor(&1000), when the output amount is small enough that amount_out * 3 < 1000 (i.e., amount_out < 333), the fee calculation results in zero. This allows attackers to perform completely fee-free swaps on small amounts, bypassing the intended fee mechanism and reducing LP revenue.

pub fn swap_exact_in(context: Context<SwapContext>, amount_in: u64, min_out: u64, zero_for_one: bool) -> Result<()> {
// ... reserve calculations and validation ...
if zero_for_one {
let numerator: u128 = (reserve_b as u128).checked_mul(amount_in as u128).ok_or(AmmError::Overflow)?;
let denominator: u128 = (reserve_a as u128).checked_add(amount_in as u128).ok_or(AmmError::Overflow)?;
if denominator == 0 {
return err!(AmmError::DivisionByZero);
}
​
let mut amount_out: u64 = numerator.div_floor(&denominator) as u64;
​
@> let lp_fees = (amount_out as u128 * 3).div_floor(&1000) as u64;
​
@> amount_out = amount_out - lp_fees;
​
if amount_out == 0 || amount_out < min_out {
return err!(AmmError::Slippage);
}
// ... transfer logic ...
} else {
// ... similar vulnerable pattern in the else branch ...
let mut amount_out: u64 = numerator.div_floor(&denominator) as u64;
​
@> let lp_fees = (amount_out as u128 * 3).div_floor(&1000) as u64;
​
@> amount_out = amount_out - lp_fees;
// ... rest of function ...
}
}

The issue occurs in both branches of the swap logic where div_floor on integer division causes lp_fees to be zero when amount_out * 3 < 1000, effectively making small swaps fee-free.


Risk

Likelihood:

Reason 1: This vulnerability occurs automatically on every swap where the output amount is less than 333 tokens (since 333 * 3 = 999 < 1000), which is common for small trades or low-value token swaps

Reason 2: Attackers can deliberately structure swap amounts to stay below the 333 token threshold, making exploitation predictable and repeatable without any special conditions

Impact:

Impact 1: Direct revenue loss to liquidity providers as fees that should be collected (0.3% of swap output) are completely bypassed, reducing the economic incentives for providing liquidity to the pool

Impact 2: Systematic exploitation through automated small swaps can accumulate significant fee losses over time, potentially making the pool economically unviable for legitimate liquidity providers

Proof of Concept

​

Recommended Mitigation

The first solution uses ceiling division to ensure fees are always rounded up, preventing zero fees. The second solution enforces a minimum fee of 1 token unit for any non-zero swap output.

- let lp_fees = (amount_out as u128 * 3).div_floor(&1000) as u64;
+ let lp_fees_u128 = (amount_out as u128 * 3 + 999) / 1000; // Ceiling division
+ let lp_fees = lp_fees_u128 as u64;
​
or
​
​
- let lp_fees = (amount_out as u128 * 3).div_floor(&1000) as u64;
+ let lp_fees = std::cmp::max(1, (amount_out as u128 * 3).div_floor(&1000) as u64);
Updates

Lead Judging Commences

0xtimefliez Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

div_floor instead div_ceil during lp fee calculations

Support

FAQs

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

Give us feedback!