SSSwap

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

Incorrect Liquidity‐Token Mint Calculation

Root + Impact

Description

  • In Uniswap V2, the first liquidity provider must receive sqrt(amountA×amountB)  −  MINIMUM_LIQUIDITY

    (and the contract permanently burns MINIMUM_LIQUIDITY to lock it).

  • Every subsequent provider must receive min⁡(amountA×totalSupply/reserveA , amountB×totalSupply/reserveB) .

// Root cause in the codebase with @> marks to highlight the relevant section
fn liquidity_calculation(amount_token_a: u64, amount_token_b: u64) -> Result<u64> {
let amount_a_u128 = amount_token_a as u128;
let amount_b_u128 = amount_token_b as u128;
// @> Incorrect: always sqrt(amountA * amountB)
// @> instead of:
// @> if first provider: sqrt(amountA * amountB) - MINIMUM_LIQUIDITY
// @> else: min(amountA * totalSupply / reserveA, amountB * totalSupply / reserveB)
let lp_amount_to_mint_u128 = amount_a_u128
.checked_mul(amount_b_u128)
.ok_or(AmmError::Overflow)?
.sqrt();
let lp_amount_to_mint = lp_amount_to_mint_u128 as u64;
if lp_amount_to_mint == 0 {
return err!(AmmError::LpAmountCalculation);
}
Ok(lp_amount_to_mint)
}

Risk

Likelihood:

  • When the pool is brand‐new (total LP supply == 0), the code mints √(A·B) instead of √(A·B) – MINIMUM_LIQUIDITY; there is no branch distinguishing “first provider” from “subsequent provider.”

  • Any time total_lp > 0 (pool already has liquidity), callers still get √(A·B), which is not proportional to existing reserves. This occurs on every non‐initial provide_liquidity call.

Impact:

  • Initial LP: The very first liquidity provider receives too many LP tokens (they should have seen MINIMUM_LIQUIDITY locked forever). That permanently breaks the invariants of total supply and can allow economic exploits later.

  • Subsequent LPs: Every subsequent provider receives an incorrect number of LP tokens—often far larger than their fair share—leading to severe dilution of existing LPs and potential loss of value.

Proof of Concept

Recommended Mitigation

+ let total_lp_supply = context.accounts.lp_mint.supply;
...
+ if total_lp_supply == 0 {
+ // First provider:
+ // sqrt(amt_a * amt_b) - MINIMUM_LIQUIDITY
+ let raw = amt_a.checked_mul(amt_b).ok_or(AmmError::Overflow)?;
+ let mut initial_liquidity = raw.sqrt();
+ initial_liquidity = initial_liquidity
+ .checked_sub(MINIMUM_LIQUIDITY)
+ .ok_or(AmmError::LpAmountCalculation)?;
+ if initial_liquidity == 0 {
+ return err!(AmmError::LpAmountCalculation);
+ }
+ let mintable = initial_liquidity
+ .try_into()
+ .map_err(|_| AmmError::Overflow)?;
+ return Ok(mintable);
+ } else {
+ // Subsequent provider:
+ let liq_from_a = amt_a
+ .checked_mul(total_lp)
+ .ok_or(AmmError::Overflow)?
+ .checked_div(res_a)
+ .ok_or(AmmError::DivisionByZero)?;
+ let liq_from_b = amt_b
+ .checked_mul(total_lp)
+ .ok_or(AmmError::Overflow)?
+ .checked_div(res_b)
+ .ok_or(AmmError::DivisionByZero)?;
+ let mintable_u128 = std::cmp::min(liq_from_a, liq_from_b);
+ if mintable_u128 == 0 {
+ return err!(AmmError::LpAmountCalculation);
+ }
+ let mintable = mintable_u128
+ .try_into()
+ .map_err(|_| AmmError::Overflow)?;
+ return Ok(mintable);
+ }
Updates

Lead Judging Commences

0xtimefliez Lead Judge 5 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Liquidity Provision is prone to JIT

Support

FAQs

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