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 over 1 year 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.

Give us feedback!