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: 24/120
Findings: 2
Award: $208.90
🌟 Selected for report: 1
🚀 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.7866 USDC - $1.79
https://github.com/code-423n4/2023-11-canto/blob/main/1155tech-contracts/src/Market.sol#L152
Price for shares inside Market
is calculated using bonding curve. Currently LinearBondingCurve
is supported. This bonging curve increases each next shares with fixed amount and also uses 10% / log2(shareIndex)
to calculate fee for the share.
In order to calculate price to buy
shares getBuyPrice
is used and to calculate price to sell
shares getSellPrice
is used.
https://github.com/code-423n4/2023-11-canto/blob/main/1155tech-contracts/src/Market.sol#L132-L145
function getBuyPrice(uint256 _id, uint256 _amount) public view returns (uint256 price, uint256 fee) { // If id does not exist, this will return address(0), causing a revert in the next line address bondingCurve = shareData[_id].bondingCurve; (price, fee) = IBondingCurve(bondingCurve).getPriceAndFee(shareData[_id].tokenCount + 1, _amount); } function getSellPrice(uint256 _id, uint256 _amount) public view returns (uint256 price, uint256 fee) { // If id does not exist, this will return address(0), causing a revert in the next line address bondingCurve = shareData[_id].bondingCurve; (price, fee) = IBondingCurve(bondingCurve).getPriceAndFee(shareData[_id].tokenCount - _amount + 1, _amount); }
So both functions use IBondingCurve(bondingCurve).getPriceAndFee
, but getBuyPrice
provides current token index
as start index for curve, while getSellPrice
provides current token index - amount to sell
as start index for curve.
So in case when someone wants to buy shares, then price depends on current circulation supply. In case if this supply is increased right after user's buy
tx, then he will pay more for the shares. And in case if someone sells shares right before buy
tx, then user will pay less amount(which is good of course).
The same we can say about sell
function. In case if someone sells shares right before user's sell
execution, then user receives smaller amount and in case if someone buys shares, then user gets bigger amount.
As the price at the moment when user initiates buy
or sell
function can be changed before execution(with frontrunning or just innocent), that means that slippage protection should be introduced, so user can be sure that he will not pay more than expected or receive less than expected.
When user expect to buy
share for 5 usd, then attacker can sandwhich user's tx shares in order to make profit.
buy
shares that cost 5 usdbuy
tx to be executed so user buys shares more expensive(10 usd)sell
shares that cost now 10 usd and earn on this(5 usd).Same thing can be done with sell
sandwhiching. When user expects to sell
his share for 20 usd
sell
own share(and get 20 usd)sell
his share cheaper(and get 15 usd)While buy
sandwhiching needs user to give full approval to Market
as this price is then sent from user, sell
sandwhiching doesn't need that, because it only sends tokens to the victim.
User can lose funds.
VsCode
Make user provide slippage to buy
, sell
, burnNFT
and mintNFT
functions.
Error
#0 - c4-pre-sort
2023-11-18T09:42:10Z
minhquanym marked the issue as primary issue
#1 - c4-pre-sort
2023-11-18T10:50:20Z
minhquanym marked the issue as sufficient quality report
#2 - c4-sponsor
2023-11-27T09:31:33Z
OpenCoreCH (sponsor) confirmed
#3 - MarioPoneder
2023-11-28T23:13:51Z
One the one hand, according to our guidelines:
Unless there is something uniquely novel created by combining vectors, most submissions regarding vulnerabilities that are inherent to a particular system or the Ethereum network as a whole should be considered QA. Examples of such vulnerabilities include front running, sandwich attacks, and MEV.
But on the other hand, a slippage percentage parameter or expected amount has become state-of-the-art which also makes this a shortcoming of the protocol.
Consequently, it seems appropriate to move forward with Medium
severity.
#4 - c4-judge
2023-11-28T23:15:30Z
MarioPoneder marked the issue as satisfactory
#5 - c4-judge
2023-11-28T23:15:53Z
MarioPoneder changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-11-28T23:40:09Z
MarioPoneder marked the issue as selected for report
#7 - OpenCoreCH
2023-11-30T12:47:20Z
Introduced params in https://github.com/mkt-market/1555tech-contracts/pull/1
#8 - Aamirusmani1552
2023-11-30T18:40:05Z
This issue was confirmed by the sponsor that it is not relevant when I asked. They totally refused that and said it will not be an issue as they will have to buy a lot of shares and it will incur more price as it is linearly increasing. So I didn't submit it. But the same issue is marked selected here. Lesson learned.
🌟 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
https://github.com/code-423n4/2023-11-canto/blob/main/1155tech-contracts/src/Market.sol#L156-L159
When user buys shares, then their's price and fee is calculated. They are paid by user. And fees are split among protocol creator, share creator and shares holders.
Fee split is done using _splitFees
. This function increases shareData[_id].shareHolderRewardsPerTokenScaled
, as fee is distributed among shares. The function has _tokenCount
param, which in our case is passed as all tokens in circulation.
Because _splitFees
changes shareData[_id].shareHolderRewardsPerTokenScaled
user's accumulated reward is calculated before _splitFees
is called. After fee split, user's rewardsLastClaimedValue[_id][msg.sender]
variable is updated to the current shareData[_id].shareHolderRewardsPerTokenScaled
, which means that user is fully settled with rewards at the moment.
But this can be not like that. In case if user had shares before buy
call, then it means that amount of shares that he already has are included into shareData[_id].tokensInCirculation
, which means that fee that he provided in current buy
call is also distributed to his old shares. But because rewardsLastClaimedValue[_id][msg.sender]
is reset for user to the new value, that means that those rewards will not be claimable by him and they will stay in the contract.
User losses part ofrewards
VsCode
Call uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id)
after split is done, so new reward rate is used for old shares.
- uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); // Split the fee among holder, creator and platform _splitFees(_id, fee, shareData[_id].tokensInCirculation); + uint256 rewardsSinceLastClaim = _getRewardsSinceLastClaim(_id); rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled;
Error
#0 - c4-pre-sort
2023-11-18T04:00:05Z
minhquanym marked the issue as duplicate of #9
#1 - c4-judge
2023-11-28T23:44:13Z
MarioPoneder marked the issue as satisfactory