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: 107/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-L169 https://github.com/code-423n4/2023-11-canto/blob/335930cd53cf9a137504a57f1215be52c6d67cb3/1155tech-contracts/src/Market.sol#L174-L198
The absence of slippage control in the Market::buy()
or Market::sell()
functions leaves users vulnerable to significant price fluctuations during periods characterized by high levels of market activity and potential sandwich attacks by MEV searchers. Possibly leading users into engaging in unfavourable buys/sells.
When examining both the buy()
and sell()
functions within the Market
contract, it becomes apparent that the prices and fees are dependent on the current number of outstanding tokens (tokenCount
). During periods of high market activity, the tokenCount
can exhibit considerable fluctuations, causing prices to vary significantly. The lack of slippage control for users, aside from managing their allowances to the Market
contract (which is inefficient due to the need for additional calls to set limits for each modification, adding indirections and more latency to users actions), exposes them to the risk of engaging in unfavorable buys/sells. This can also happen during calls to mintNFT
/burnNFT
however with diminished impact as for this cases it only affects the fees paid.
Furthermore, this vulnerability can be exploited by MEV searchers employing sandwich attacks. MEV operators can strategically sandwich the user order against their own buy()
/sell()
order. This manipulation can lead to potential profits for the MEV searchers and losses to users.
Consider the simplified example below for a market dealing with token X (small values will be used for ease of explanation and assume that fees are zero). A new share is created with a bounding curve with priceIncrease
set to 1. After some time, 99 shares are bought. Subsequently, Alice attempts to buy 10 shares at the expected price of 1045 (calculated as the sum of 100 + 101 + 102 + ... + 109).
Therefore Alice pays more than she expected, while Bob ends up with a profit.
Manual review
Consider adding an additional parameters maxPrice
and minPrice
as exemplified below.
diff --git a/Market.sol b/Market.mod.sol index 10bf50a..3874a18 100644 --- a/Market.sol +++ b/Market.mod.sol @@ -158,9 +158,12 @@ contract Market is ERC1155, Ownable2Step { /// @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 { + function buy(uint256 _id, uint256 _amount, uint256 maxPrice) external { require(shareData[_id].creator != msg.sender, "Creator cannot buy"); (uint256 price, uint256 fee) = getBuyPrice(_id, _amount); // Reverts for non-existing ID + if (maxPrice != 0) { + require( price <= maxPrice, "Price too high!"); + } 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 @@ -182,8 +185,11 @@ contract Market is ERC1155, Ownable2Step { /// @notice Sell amount of tokens for a given share ID /// @param _id ID of the share /// @param _amount Amount of shares to sell - function sell(uint256 _id, uint256 _amount) external { + function sell(uint256 _id, uint256 _amount, uint256 minPrice) external { (uint256 price, uint256 fee) = getSellPrice(_id, _amount); + if (minPrice != 0) { + require( price >= minPrice, "Price too low!"); + } // 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)
MEV
#0 - c4-pre-sort
2023-11-18T09:58:10Z
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:28:15Z
MarioPoneder marked the issue as satisfactory