Platform: Code4rena
Start Date: 28/09/2023
Pot Size: $36,500 USDC
Total HM: 5
Participants: 115
Period: 6 days
Judge: 0xDjango
Total Solo HM: 1
Id: 290
League: ETH
Rank: 25/115
Findings: 2
Award: $202.85
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xDetermination
Also found by: DeFiHackLabs, Norah, Pessimistic, PwnStars, SpicyMeatball, Testerbot, ThreeSigma, bin2chen, blutorque, deadrxsezzz, dirk_y, ether_sky, hals, neumo, rokinot, said, seerether, turvy_fuzz
198.4834 USDC - $198.48
Prime.pendingScoreUpdates
is used to record the number of users whose score
needs to be recalculated when addMarket()
, updateAlpha()
, updateMultipliers()
occurs.
Record pendingScoreUpdates=totalIrrevocable + totalRevocable
when the above methods occur
If there is a partial user update via updateScores()
, the corresponding number is reduced, pendingScoreUpdates --
and records whether the current round of users has been modified : isScoreUpdated[nextScoreUpdateRoundId][user] = true
function updateScores(address[] memory users) external { if (pendingScoreUpdates == 0) revert NoScoreUpdatesRequired(); if (nextScoreUpdateRoundId == 0) revert NoScoreUpdatesRequired(); for (uint256 i = 0; i < users.length; ) { address user = users[i]; if (!tokens[user].exists) revert UserHasNoPrimeToken(); if (isScoreUpdated[nextScoreUpdateRoundId][user]) continue; address[] storage _allMarkets = allMarkets; for (uint256 j = 0; j < _allMarkets.length; ) { address market = _allMarkets[j]; _executeBoost(user, market); _updateScore(user, market); unchecked { j++; } } pendingScoreUpdates--; @> isScoreUpdated[nextScoreUpdateRoundId][user] = true; unchecked { i++; } emit UserScoreUpdated(user); } }
When the user burns it also determines if isScoreUpdated[nextScoreUpdateRoundId][user] = false
, and performs pendingScoreUpdates--
;
function _burn(address user) internal { ... tokens[user].exists = false; tokens[user].isIrrevocable = false; @> _updateRoundAfterTokenBurned(user); emit Burn(user); } function _updateRoundAfterTokenBurned(address user) internal { if (totalScoreUpdatesRequired > 0) totalScoreUpdatesRequired--; @> if (pendingScoreUpdates > 0 && !isScoreUpdated[nextScoreUpdateRoundId][user]) { @> pendingScoreUpdates--; } }
But the new mint()
user does not change isScoreUpdated[nextScoreUpdateRoundId][user]
to true
.
So that in the current round, the new mint
user immediately burn
, or someone malicious can execute updateScores(new user)
which will take up pendingScoreUpdates
, causing pendingScoreUpdates
to be miscalculated.
burn()
immediately after mint()
for the current round will result in inaccurate counting of pendingScoreUpdates
.
cause updateScores()
to judge pendingScoreUpdates==0
revert, can't be executed
Given: totalIrrevocable = 5 totalRevocable = 5
When:
addMarket()
: pendingScoreUpdates = 5 + 5 =10claim()
to mint new Token , isScoreUpdated[1][alice] = falseupdateScores(alice]
: pendingScoreUpdates = 10 - 1 = 9pendingScoreUpdates
correct should be 10
when mint set isScoreUpdated[nextScoreUpdateRoundId][user] = true
function _mint(bool isIrrevocable, address user) internal { if (tokens[user].exists) revert IneligibleToClaim(); tokens[user].exists = true; tokens[user].isIrrevocable = isIrrevocable; if (isIrrevocable) { totalIrrevocable++; } else { totalRevocable++; } if (totalIrrevocable > irrevocableLimit || totalRevocable > revocableLimit) revert InvalidLimit(); + isScoreUpdated[nextScoreUpdateRoundId][user] = true emit Mint(user, isIrrevocable); }
Context
#0 - c4-pre-sort
2023-10-04T22:26:22Z
0xRobocop marked the issue as duplicate of #555
#1 - c4-judge
2023-11-01T20:29:41Z
fatherGoose1 marked the issue as satisfactory
#2 - c4-judge
2023-11-05T00:52:22Z
fatherGoose1 changed the severity to 3 (High Risk)
🌟 Selected for report: Bauchibred
Also found by: 0x3b, 0xDetermination, 0xMosh, 0xScourgedev, 0xTheC0der, 0xTiwa, 0xWaitress, 0xdice91, 0xfusion, 0xpiken, 0xprinc, 0xweb3boy, ArmedGoose, Aymen0909, Breeje, Brenzee, Daniel526, DavidGiladi, DeFiHackLabs, Flora, Fulum, HChang26, Hama, IceBear, J4X, Krace, KrisApostolov, Maroutis, Mirror, MohammedRizwan, Norah, PwnStars, SPYBOY, TangYuanShen, Testerbot, ThreeSigma, Tricko, al88nsk, alexweb3, ast3ros, berlin-101, bin2chen, blutorque, btk, d3e4, deth, e0d1n, ether_sky, ge6a, gkrastenov, glcanvas, hals, imare, inzinko, jkoppel, jnforja, joaovwfreire, josephdara, kutugu, lotux, lsaudit, mahdirostami, merlin, n1punp, nadin, neumo, nisedo, nobody2018, oakcobalt, orion, peanuts, pep7siup, pina, ptsanev, rokinot, rvierdiiev, said, santipu_, sashik_eth, seerether, squeaky_cactus, terrancrypt, tonisives, twicek, vagrant, xAriextz, y4y
4.3669 USDC - $4.37
Label | Description |
---|---|
L-01 | addMarket() needs to be forced not to have the same underlying |
L-02 | BLOCKS_PER_YEAR The block out time in L2 may be modified |
L-03 | sweepToken() Need to limit remaining tokens to no less than tokenAmountAccrued[token_] |
addMarket()
needs to be forced not to have the same underlyingIf different vToken
s have the same underlying
, there are multiple problems
underlying
, and different vTokens will compete with each other for rewardsIt is recommended to revert if there is already an underlying
.
function addMarket(address vToken, uint256 supplyMultiplier, uint256 borrowMultiplier) external { _checkAccessAllowed("addMarket(address,uint256,uint256)"); if (markets[vToken].exists) revert MarketAlreadyExists(); + if (vTokenForAsset[_getUnderlying(vToken)] !=address(0) revert InvalidVToken(); bool isMarketExist = InterfaceComptroller(comptroller).markets(vToken); if (!isMarketExist) revert InvalidVToken(); ... }
BLOCK_TIME actually changes every now and then, especially in L2s.For example, the recent Bedrock upgrade in Optimism completely changed the block time generation.
Since BLOCKS_PER_YEAR is currently immutable
, if BLOCK_TIME
is modified, it will not be able to be adjusted, affecting the calculation of APR.
Suggest to change it to be changeable
tokenAmountAccrued[token_]
sweepToken()
can be used to move tokens
But there is no restriction if the token moved is a rewarded token
If the remaining token after removal is less than tokenAmountAccrued[token_]
, it will cause the following 2 methods to underflow
accrueTokens()
getEffectiveDistributionSpeed()
suggestion:
function sweepToken(IERC20Upgradeable token_, address to_, uint256 amount_) external onlyOwner { uint256 balance = token_.balanceOf(address(this)); - if (amount_ > balance) { + if (balance - amount < tokenAmountAccrued[token_]) { revert InsufficientBalance(amount_, balance); } emit SweepToken(address(token_), to_, amount_); token_.safeTransfer(to_, amount_); }
#0 - 0xRobocop
2023-10-07T02:00:56Z
L-02 Dup of #39 L-03 Dup of #42
#1 - c4-pre-sort
2023-10-07T02:01:09Z
0xRobocop marked the issue as sufficient quality report