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: 44/103
Findings: 3
Award: $70.22
🌟 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
Detailed description of the impact of this finding.
Given baseTokenAmount
and fractionalTokenAmount
, the Add()
function calls the addQuote()
funtion to determine lpTokenAmount
. However, addQuote()
choses the minimum of baseTokenShare
and fractionalTokenShare
as the final value of lpTokenAmount
. A user will end up overpaying either the base tokens or the fractional tokens since Add()
will send the whole amount of baseTokenAmount
and fractionalTokenAmount
to the Pair contract.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L63
On one hand, the addQuote
function determines the lpTokenAmount
as
uint256 baseTokenShare = (baseTokenAmount * lpTokenSupply) / baseTokenReserves(); uint256 fractionalTokenShare = (fractionalTokenAmount * lpTokenSupply) / fractionalTokenReserves(); return Math.min(baseTokenShare, fractionalTokenShare);
On the other hand, the Add()
function takes the whole amount baseTokenAmount
and fractionalTokenAmount
, so one of them would be overpaid.
Remix
We need to revise the addQuote()
function to return not only lpTokenAmount
, but also actual actualBaseTokenAmount
and actutalFractionalTokenAmount
.
function addQuote(uint256 baseTokenAmount, uint256 fractionalTokenAmount) public view returns (uint256 ltTokenAmount, uint256 actualBaseTokenAmount, uint256 actualFractionalTokenAmount) 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(); if(baseTokenShare < fractionTokenShare) { // @audit ltTokenAmout = baseTokenShare; actualBaseTokenAmount = baseTokenAmount; actualFractionalTokenAmount = baseTokenShare*frationalTokenReserves()/lpTokenSupply; } else{ ltTokenAmout = fractionalTokenShare; actualBaseTokenAmount = fractionalTokenShare*baseTokenReserves()/lpTokenSupply; actualFractionalTokenAmount = fractionalTokenAmount; } } else { // if there is no liquidity then init ltTokenAmount = Math.sqrt(baseTokenAmount * fractionalTokenAmount); actualBaseTokenAmount = baseTokenAmount; actualFractionalTokenAmount = fractionalTokenAmount; } }
Then, the add()
function to use actualBaseTokenAmount
and actualFractionalTokenAmount
to transfer the right amount of tokens and if necessary, we need to refund the base token if it is ETH.
function add(uint256 baseTokenAmount, uint256 fractionalTokenAmount, uint256 minLpTokenAmount) public payable returns (uint256 lpTokenAmount) { // *** Checks *** // // check the token amount inputs are not zero require(baseTokenAmount > 0 && fractionalTokenAmount > 0, "Input token amount is zero"); // check that correct eth input was sent - if the baseToken equals address(0) then native ETH is used require(baseToken == address(0) ? msg.value == baseTokenAmount : msg.value == 0, "Invalid ether input"); uint256 actualBaseTokenAmount; // @audit uint256 actualFrationalTokenAmount; // @audit , // calculate the lp token shares to mint (lpTokenAmount, actualBaseTokenAmount, actualFractionalTokenAmount) = addQuote(baseTokenAmount, fractionalTokenAmount); // @audit, need to calculate the actual amount for each token // check that the amount of lp tokens outputted is greater than the min amount require(lpTokenAmount >= minLpTokenAmount, "Slippage: lp token amount out"); // *** Effects *** // // transfer fractional tokens in _transferFrom(msg.sender, address(this), actualFractionalTokenAmount); // @audit: use the actualFractionalTokenAmount // *** Interactions *** // // mint lp tokens to sender lpToken.mint(msg.sender, lpTokenAmount); // transfer base tokens in if the base token is not ETH if (baseToken != address(0)) { // transfer base tokens in ERC20(baseToken).safeTransferFrom(msg.sender, address(this), actualBaseTokenAmount); // @audit: use actualBaseTokenAmount instead } else{ uint256 refundAmount = baseTokenAmount - actualBaseTokenAmount; // @audit: need to refund if(refundAmount > 0) msg.sender.safeTransferETH(refundAmount); // @audit: need to refund } emit Add(actualBaseTokenAmount, actualFractionalTokenAmount, lpTokenAmount); }
#0 - Shungy
2022-12-21T20:04:40Z
Dup #90
#1 - c4-judge
2022-12-28T11:04:30Z
berndartmueller marked the issue as duplicate of #376
#2 - c4-judge
2023-01-10T09:01:34Z
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
Detailed description of the impact of this finding. An attack can front run the first Add() transaction and gain free baseTokens using a similar idea of sandwich attack.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L63
User Bob attemps to run the first Add
Transaction with bastTokenAmount = 10e18 and
fractionalTokenAmount = 1e18.
The attacker Alice front run this transaction with baseTokenAmount = 1e18 and fractionTokenAmount = 1e18, and set the price as 1 baseToken/fractionToken first, Alice will get back 1e18 lpTokens.
Bob continues to run the Add
transaction from step 1, but now he is the second to run Add
,
and has to add the liquidity with the price set by Alice, and will return
around 1e18 lptokens instead with his 1e18 fractionalTokens and 10e18 base tokens.
After Bob's transaction, the reserves of baseToken and fractional tokens have changed: there are 11e18 baseTokens and 2e18 fractionalTokens, so the price is around 5.5 baseToken/fractionalToken.
Attacker Alice removes all her liquidity from the pair, she can get 1e18 fractionalTokens, and around 5.5e18 base tokens, she gains almost 4.5e18 base tokens for free!
Remix
The addQuote() fuction needs a better design to prevent such attack.
#0 - c4-judge
2022-12-20T14:35:10Z
berndartmueller marked the issue as duplicate of #442
#1 - c4-judge
2023-01-10T09:14:28Z
berndartmueller marked the issue as satisfactory
🌟 Selected for report: Zarf
Also found by: 0xDave, Apocalypto, CRYP70, Franfran, Jeiwan, UNCHAIN, adriro, bytehat, chaduke, hansfriese, hihen, kiki_dev, koxuan, minhtrng, rajatbeladiya, unforgiven, wait, yixxas
22.9693 USDC - $22.97
Detailed description of the impact of this finding. The calculation of buyQuote seems to not quite right, we need to add 1 add the end to avoid edge case such as zero. See the standard implementation at Uniswap V2: https://github.com/Uniswap/v2-periphery/blob/0335e8f7e1bd1e8d8329fd300aea2ef2f36dd19f/contracts/libraries/UniswapV2Library.sol#L58
function buyQuote(uint256 outputAmount) public view returns (uint256) { return (outputAmount * 1000 * baseTokenReserves()) / ((fractionalTokenReserves() - outputAmount) * 997) + 1; }
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept. https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L398-L400
Remix
#0 - Shungy
2022-12-21T20:20:25Z
Dup #436
#1 - c4-judge
2022-12-23T13:51:19Z
berndartmueller marked the issue as duplicate of #243
#2 - berndartmueller
2022-12-23T13:52:32Z
Due to the low quality of the submission (impact not clearly stated, missing recommended mitigation step, poor PoC), I'm applying a partial credit of 50%.
#3 - c4-judge
2022-12-23T13:52:37Z
berndartmueller marked the issue as partial-50
#4 - c4-judge
2023-01-10T09:44:20Z
berndartmueller changed the severity to 2 (Med Risk)