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: 25/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
In the Market currently using the LinearBondingCurve, the price increases each time a token is bought. There's also a dynamic fee for each token purchase. However, this fee is usually too small, which can allow attackers to execute a sandwich attack and profit within a single block.
Here's a summary of what could potentially happen (example in PoC section):
Consider the following scenario where LINEAR_INCREASE = 1e18 / 1000 = 1e16
and the current tokenCount = 15
.
price = 16e16
fee = 16e16 * (1e17 / 4) / 1e18 = 4e15
=> total paid price + fee = 164e15
.price = 17e16
fee = 17e17 * (1e17 / 4) / 1e18 = 4.25e15
=> total received price - fee = 165.75e15
.Profit = 165.75e15 - 164e15 = 1.75e15
.Manual review
Consider modifying the fee formula to prevent attackers from profiting after deducting the fee.
Math
#0 - c4-pre-sort
2023-11-18T09:43:09Z
minhquanym marked the issue as duplicate of #12
#1 - c4-judge
2023-11-28T23:16:53Z
MarioPoneder marked the issue as satisfactory
🌟 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
In the Market, each token's price varies (increasing or decreasing) according to the price curve. This means that users or the UI cannot accurately predict or calculate the payment amount when initiating an off-chain transaction. Consequently, a user might end up paying significantly more than expected if many other users are also buying at the same time.
Consider the following scenario:
Manual review
This is a common issue. If we examine any Decentralized Exchange (DEX) like Uniswap, for example, they all have a user protection parameter like minAmountOut
.
Consider adding a maxAmountSpent
parameter to allow users to limit the amount they are willing to spend on a buy/sell.
MEV
#0 - c4-pre-sort
2023-11-18T10:32:31Z
minhquanym marked the issue as duplicate of #12
#1 - c4-judge
2023-11-28T23:34:18Z
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
During the `buy(...) function, the buyer must pay a fee in addition to the price. This fee is distributed among all the circulating tokens.
A comment explicitly states that the caller should not claim fees from this buy. Although it is true that the caller cannot claim fees because the rewardsLastClaimedValue
is calculated using the old rewards value, the fee distribution is accounted for all tokens in circulation. As a result, fees are distributed, but the caller cannot claim their portion, causing it to be permanently locked in the contract.
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 // @audit Fees are splitted to tokensInCirculation but sender cannot claim _splitFees(_id, fee, shareData[_id].tokensInCirculation); rewardsLastClaimedValue[_id][msg.sender] = shareData[_id].shareHolderRewardsPerTokenScaled; ... }
Consider the following scenario:
tokensInCirculation = 10
. Assume shareHolderRewardsPerTokenScaled = 1e18
and rewardsLastClaimedValue[Alice] = 1e18
(Alice is the last user to interact with the market).1e18
) is used for calculation.tokensInCirculation = 10
(which includes Alice's 3 tokens). Assume it will make shareHolderRewardsPerTokenScaled = 2e18
then it will also update rewardsLastClaimedValue[Alice] = 2e18
.rewardsLastClaimedValue[Alice]
is already updated, causing them to be locked in the contract forever.Manual review
If the intention is to not distribute the fee to the buyer, only call:
_splitFees(_id, fee, shareData[_id].tokensInCirculation - tokensByAddress[_id][msg.sender]);
Math
#0 - c4-pre-sort
2023-11-18T04:09:20Z
minhquanym marked the issue as duplicate of #302
#1 - c4-judge
2023-11-28T22:39:45Z
MarioPoneder changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-11-28T22:39:51Z
MarioPoneder marked the issue as satisfactory
#3 - c4-judge
2023-11-28T23:54:06Z
MarioPoneder marked the issue as duplicate of #9