The remove_liquidity() function fails to properly handle precision loss during integer division when calculating the amounts of tokens to return to the liquidity provider. This can result in a disproportionate amount of token_a and token_b returned, leading to an imbalance in the pool's reserves. This imbalance is not checked in swap calculations which can allow an attacker to drain disproportionate amounts of tokens.
When a liquidity provider redeems LP tokens, the amounts of token_a and token_b to be returned are calculated using the formulas (lpt_to_redeem * reserve_a) / total_lp_supply and (lpt_to_redeem * reserve_b) / total_lp_supply. If lpt_to_redeem * reserve_X is less than total_lp_supply, then amount_X_to_return will be 0 due to integer truncation. Due to the ratio maintained of tokens in the liquidity pool, this scenario can occur for one token while the other returns a non-zero amount, skewing the pool's ratio without a corresponding burn of LP tokens for the "lost" token.
This creates a "stale" or "inflated" reserve for the token that was not withdrawn, which the swap_exact_out() function then incorrectly uses for its calculations.
Likelihood: High
If reserve_a is much smaller than reserve_b, then even a moderately sized lpt_to_redeem can result in the smaller reserve's calculated return amount truncating to 0, while the larger reserve's amount remains positive.
Impact: High
The attacker can identify that one token is artificially cheaper within the pool than its true market value due to the inflated reserve. They can then execute swap_exact_out transactions to drain the correctly priced token at a favorable rate such that liquidity providers suffer significant loss and can have their funds drained.
Assume an AMM pool state where reserve_a = 100, reserve_b = 1000, and total_lp_supply = 1500.
remove_liquidityIf an attacker wishes to remove_liquidity by burning lpt_to_redeem = 2, then the following happens:
The check if amount_a_to_return == 0 && amount_b_to_return == 0 && lpt_to_redeem > 0 passes since amount_b_to_return is 1, which results in an updated state where reserve_a = 100, reserve_b = 998, and total_lp_supply = 1498. The implied value for reserve_a should actually be 99.86... rather than 100, meaning reserve_a is now inflated relative to the burned LP tokens. This step can be repeated up until the result of (lpt_to_redeem * reserve_b) / total_lp_supply is exactly 1. In the example state, if this step were repeated, one could redeem enough from reserve_b to cause a state where reserve_a = 100, reserve_b = 500, and total_lp_supply = 1000.
swap_exact_outNow with the pool state of reserve_a = 100 and reserve_b = 500, a call can be made to swap_exact_out with amount_out = 10 and zero_for_one = true. The following is the arithmetic that results from the current state:
Here, the malicious actor indicated they wanted to buy 10 of token_b. The amount calculated from the current reserves indicates the malicious actor will have to supply 2 token_a to get 10 token_b. Because of this, amount_in_final = 2 and the new pool state is reserve_a = 100 + 2 = 102 and reserve_b = 500 - 10 = 490. The attacker has now obtained 10 token_b for the price of 2 token_a, even though the pool started at a 1:10 ratio and should have intended to maintain something close to that.
The primary mitigation is to prevent remove_liquidity from executing if it results in a non-proportional return. This ensures that the pool's state remains consistent with the LP tokens burned.
Diff
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.