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: 26/120
Findings: 2
Award: $208.48
🌟 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/main/1155tech-contracts/src/Market.sol#L150 https://github.com/code-423n4/2023-11-canto/blob/main/1155tech-contracts/src/Market.sol#L153
If another user frontruns a buy or sell transaction, the order may be executed at an unintended price.
function buy(uint256 _id, uint256 _amount) external { require(shareData[_id].creator != msg.sender, "Creator cannot buy"); (uint256 price, uint256 fee) = getBuyPrice(_id, _amount); // Reverts for non-existing ID 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 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); }
The buy function receives tokens from the user through transferFrom. Therefore, users are likely to max approve their tokens to the market for use.
If a malicious user frontruns the buy, raising the price, the user ends up buying at a higher price than intended.
Afterwards, the malicious user can sell at a higher price than they bought for.
POC:
function testFrontrun() public { testBuy(); vm.prank(vic); token.approve(address(market), type(uint256).max); vm.prank(bad); token.approve(address(market), type(uint256).max); uint256 before = token.balanceOf(bad); vm.prank(bad); market.buy(1, 1); // <- frontrun vm.prank(vic); market.buy(1, 1); vm.prank(bad); market.sell(1, 1); // <- and sell uint256 after = token.balanceOf(bad); console.log(after - before); } // console : 582500000000000
VS Code
The buy and sell functions should take a minimum price value as an argument for a slippage check.
Other
#0 - c4-pre-sort
2023-11-18T09:56:29Z
minhquanym marked the issue as duplicate of #12
#1 - c4-judge
2023-11-28T23:27:24Z
MarioPoneder marked the issue as satisfactory
🌟 Selected for report: Krace
Also found by: 0xAadi, 0xpiken, AS, D1r3Wolf, PENGUN, SpicyMeatball, Yanchuan, bin2chen, d3e4, ether_sky, glcanvas, immeas, lanrebayode77, leegh, mojito_auditor, rvierdiiev, tnquanghuy0512
207.1122 USDC - $207.11
A portion of the holder share from the amount purchased through 'buy' is locked in the contract.
/// @notice Buy amount of tokens for a given share ID /// @param _id ID of the share /// @param _amount Amount of shares to buy function buy(uint256 _id, uint256 _amount) external { require(shareData[_id].creator != msg.sender, "Creator cannot buy"); (uint256 price, uint256 fee) = getBuyPrice(_id, _amount); // Reverts for non-existing ID 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 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); }
Through the Market.buy function, one can purchase shares created by a specific creator.
The user attempting to purchase must pay the token price + fee, and this fee is divided among the holder, creator, and platform.
The buyer cannot distribute the currently purchased share.
However, this amount is not distributed to the creator, creator, or platform, but rather is locked in the contract.
The following scenario occurs:
POC
function testclaimPlatform() public { testBuy(); market.claimPlatformFee(); for(uint i =0;i<1000;i++) market.buy(1,1); market.claimPlatformFee(); market.sell(1, 1001); market.claimPlatformFee(); vm.prank(bob); market.claimCreatorFee(1); console.log(token.balanceOf(address(market))); } // console: 1928000904761904273 (1.928e18)
VS Code
function buy(uint256 _id, uint256 _amount) external { require(shareData[_id].creator != msg.sender, "Creator cannot buy"); (uint256 price, uint256 fee) = getBuyPrice(_id, _amount); // Reverts for non-existing ID 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 _splitFees(_id, fee, shareData[_id].tokensInCirculation); uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); // 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) { SafeERC20.safeTransfer(token, msg.sender, rewardsSinceLastClaim); } emit SharesBought(_id, msg.sender, _amount, price, fee); }
Do _splitFees first. Since the tokenCount hasn't increased, the inability to split fees for ongoing buys remains.
Other
#0 - c4-pre-sort
2023-11-18T04:10:17Z
minhquanym marked the issue as duplicate of #9
#1 - c4-judge
2023-11-28T23:42:10Z
MarioPoneder changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-11-28T23:44:32Z
MarioPoneder marked the issue as satisfactory