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: 59/103
Findings: 2
Award: $47.25
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Jeiwan
Also found by: 0xxm, 9svR6w, BAHOZ, Bobface, CRYP70, Chom, HE1M, Junnon, RaymondFam, UNCHAIN, __141345__, bytehat, carlitox477, caventa, cccz, chaduke, hansfriese, hihen, koxuan, mauricio1802, minhquanym, minhtrng, nicobevi, obront, shung, unforgiven, wait
40.2564 USDC - $40.26
https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L63-L99 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L417-L428
When users provide LP, the exact amounts required for baseToken
and fractionalToken
could vary depends on many factors, such as former txn swap in the same block. It is quite possible that the provided baseTokenAmount
and fractionalTokenAmount
are not perfectly proportional to the reserves. But the add()
function just transfer whatever as input, the extra token will not be returned.
As a result:
The add()
function will transfer whatever amount provided, even if the amount are not proportional to the reserves.
File: src/Pair.sol 63: function add(uint256 baseTokenAmount, uint256 fractionalTokenAmount, uint256 minLpTokenAmount) 64: public 65: payable 66: returns (uint256 lpTokenAmount) 67: { 76: // calculate the lp token shares to mint 77: lpTokenAmount = addQuote(baseTokenAmount, fractionalTokenAmount); 78: 79: // check that the amount of lp tokens outputted is greater than the min amount 80: require(lpTokenAmount >= minLpTokenAmount, "Slippage: lp token amount out"); 81: 82: // *** Effects *** // 83: 84: // transfer fractional tokens in 85: _transferFrom(msg.sender, address(this), fractionalTokenAmount); 86: 87: // *** Interactions *** // 88: 89: // mint lp tokens to sender 90: lpToken.mint(msg.sender, lpTokenAmount); 91: 92: // transfer base tokens in if the base token is not ETH 93: if (baseToken != address(0)) { 94: // transfer base tokens in 95: ERC20(baseToken).safeTransferFrom(msg.sender, address(this), baseTokenAmount); 96: } 97: 98: emit Add(baseTokenAmount, fractionalTokenAmount, lpTokenAmount); 99: } 417: function addQuote(uint256 baseTokenAmount, uint256 fractionalTokenAmount) public view returns (uint256) { 418: uint256 lpTokenSupply = lpToken.totalSupply(); 419: if (lpTokenSupply > 0) { 420: // calculate amount of lp tokens as a fraction of existing reserves 421: uint256 baseTokenShare = (baseTokenAmount * lpTokenSupply) / baseTokenReserves(); 422: uint256 fractionalTokenShare = (fractionalTokenAmount * lpTokenSupply) / fractionalTokenReserves(); 423: return Math.min(baseTokenShare, fractionalTokenShare); 424: } else { 425: // if there is no liquidity then init 426: return Math.sqrt(baseTokenAmount * fractionalTokenAmount); 427: } 428: }
Imagine the following: the pool begins with DAI 1,000, NFTToken 1,000, lpToken totalSupply 1,000, price is 1.0000.
If recalculate by the actual reserves, the amount should be 100 DAI and 63 NFTToken. The difference 37 NFTToken is lost to the pair.
Also, the AMM price of the 2 token should not vary due to newly added liquidity.
The numbers used above is exaggerate for slippage, but the idea persists, that in-proportional amounts could result in user loss.
Manual analysis.
baseTokenReserves()/fractionalTokenReserves()
, if the ratio of the amount used (baseTokenAmount/fractionalTokenAmount
) differ too much from the spot price, just revert the transaction. Similar idea to the slippage control.#0 - c4-judge
2022-12-28T12:48:09Z
berndartmueller marked the issue as duplicate of #376
#1 - c4-judge
2023-01-10T09:02:04Z
berndartmueller changed the severity to 3 (High Risk)
#2 - c4-judge
2023-01-10T09:02:09Z
berndartmueller marked the issue as satisfactory
🌟 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/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L417-L428 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L63-L99
When a new pair is created, a malicious user can front run mint the first LP, and the price can be arbitrarily low or high, regardless of the class being floor or rare.
Later LP providers could mistakenly provide liquidity at the price set by the malicious actor, allowing the malicious user to swap tokens at the incorrect price and extract profit.
When the lpTokenSupply
is 0, the price can be set to arbitrary value. Later LP providers can only follow the price.
File: src/Pair.sol 417: function addQuote(uint256 baseTokenAmount, uint256 fractionalTokenAmount) public view returns (uint256) { 418: uint256 lpTokenSupply = lpToken.totalSupply(); 419: if (lpTokenSupply > 0) { 420: // calculate amount of lp tokens as a fraction of existing reserves 421: uint256 baseTokenShare = (baseTokenAmount * lpTokenSupply) / baseTokenReserves(); 422: uint256 fractionalTokenShare = (fractionalTokenAmount * lpTokenSupply) / fractionalTokenReserves(); 423: return Math.min(baseTokenShare, fractionalTokenShare); 424: } else { 425: // if there is no liquidity then init 426: return Math.sqrt(baseTokenAmount * fractionalTokenAmount); 427: } 428: }
But the transferred amount of baseToken
and fractionalToken
remain the same, which are not sensitive to the ratio of underlying reserves.
063: function add(uint256 baseTokenAmount, uint256 fractionalTokenAmount, uint256 minLpTokenAmount) 064: public 065: payable 066: returns (uint256 lpTokenAmount) 067: { 068: // *** Checks *** // 076: // calculate the lp token shares to mint 077: lpTokenAmount = addQuote(baseTokenAmount, fractionalTokenAmount); 078: 079: // check that the amount of lp tokens outputted is greater than the min amount 080: require(lpTokenAmount >= minLpTokenAmount, "Slippage: lp token amount out"); 081: 082: // *** Effects *** // 083: 084: // transfer fractional tokens in 085: _transferFrom(msg.sender, address(this), fractionalTokenAmount); 086: 087: // *** Interactions *** // 088: 089: // mint lp tokens to sender 090: lpToken.mint(msg.sender, lpTokenAmount); 091: 092: // transfer base tokens in if the base token is not ETH 093: if (baseToken != address(0)) { 094: // transfer base tokens in 095: ERC20(baseToken).safeTransferFrom(msg.sender, address(this), baseTokenAmount); 096: } 097: 098: emit Add(baseTokenAmount, fractionalTokenAmount, lpTokenAmount); 099: } 100:
Imagine the following:
add(10,00*1e18, 10*1e18, 100*e18)
add(10,000, 1, 100)
, setting a price such that 1 NFT = 10,000 DAI.The above can also go in the other direction, to set the price extremely high, the idea is analogous.
Manual analysis.
Enforced the pair creator to make the first deposit with proper initial price.
#0 - c4-judge
2022-12-20T14:34:38Z
berndartmueller marked the issue as duplicate of #442
#1 - c4-judge
2023-01-10T09:12:42Z
berndartmueller marked the issue as satisfactory