SSSwap

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

Flawed Liquidity Calculation Using sqrt(A×B) Enables Economic Exploit

Root + Impact

https://github.com/CodeHawks-Contests/2025-05-ssswap/blob/27a2ef878023b0111ec4acc33503d99ae1ae36fa/programs/amm/src/instructions/liquidity_operations.rs#L269

Description

  • In a correctly implemented AMM (Automated Market Maker) like Uniswap V2, LP tokens are minted to liquidity providers in proportion to the liquidity they contribute relative to the existing pool. This ensures all LP tokens represent a consistent share of the underlying reserves.


  • In the affected contract, the LP token minting logic incorrectly uses sqrt(amount_token_a * amount_token_b) regardless of the pool's current state. This logic is only appropriate during the initial pool creation. Applying it afterward results in LP token misallocation, allowing users to receive more LP tokens than they are entitled to.

// Incorrect LP minting logic in `provide_liquidity()`
// This code is used even when the pool has existing reserves.
let lp_amount_to_mint_u128 = amount_a_u128
.checked_mul(amount_b_u128)
.ok_or(AmmError::Overflow)?
.sqrt(); @> Used outside of initialization context

Risk

Likelihood:

  • Any time a user adds liquidity to a non-empty pool, this flawed logic is triggered.

  • The function does not check whether this is an initial liquidity event, and applies the same square-root formula universally.

Impact:

  • Users can receive inflated LP token amounts, gaining a disproportionate share of the pool.

  • This leads to dilution of existing LP holders and breaks the invariant that each LP token represents a proportional share of the underlying assets.

Proof of Concept

// Assume existing reserves
reserve_a = 1000;
reserve_b = 1000;
total_lp_supply = 1000;
// User adds liquidity
amount_a = 1000;
amount_b = 10000;
// Incorrect calculation:
lp_minted = sqrt(1000 * 10000) = sqrt(10_000_000) = 3162
// Correct calculation:
lp_from_a = 1000 * 1000 / 1000 = 1000
lp_from_b = 10000 * 1000 / 1000 = 10000
lp_to_mint = min(1000, 10000) = 1000
// Over-mint: Attacker gets 3162 LP instead of 1000

Recommended Mitigation

Use the standard LP minting formula after the pool has been initialized:

let reserve_a = context.accounts.vault_a.amount;
let reserve_b = context.accounts.vault_b.amount;
let total_lp_supply = context.accounts.lp_mint.supply;
let lp_from_a = amount_a
.checked_mul(total_lp_supply)
.ok_or(AmmError::Overflow)?
.checked_div(reserve_a)
.ok_or(AmmError::DivisionByZero)?;
let lp_from_b = amount_b
.checked_mul(total_lp_supply)
.ok_or(AmmError::Overflow)?
.checked_div(reserve_b)
.ok_or(AmmError::DivisionByZero)?;
let lp_to_mint = lp_from_a.min(lp_from_b);
Updates

Lead Judging Commences

0xtimefliez Lead Judge 12 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.