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 5 days 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.