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: 29/115
Findings: 1
Award: $198.48
🌟 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
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L827
User will break internal accounting and will lead to key function revert
Whenever, _startScoreUpdateRound
is invoked pendingScoreUpdates
is set to the current totalIrrevocable + totalRevocable
function _startScoreUpdateRound() internal { nextScoreUpdateRoundId++; totalScoreUpdatesRequired = totalIrrevocable + totalRevocable; pendingScoreUpdates = totalScoreUpdatesRequired; }
However, this accounting can easily be broken in the following scenario:
After _startScoreUpdateRound
is invoked, a user is minted their prime token
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(); emit Mint(user, isIrrevocable); }
Shortly after the user unstakes from the XVS vault and falls bellow the minimum stake threshold, therefore getting his Prime token burned. _burn
at the end calls _updateRoundAfterTokenBurned
function _updateRoundAfterTokenBurned(address user) internal { if (totalScoreUpdatesRequired > 0) totalScoreUpdatesRequired--; if (pendingScoreUpdates > 0 && !isScoreUpdated[nextScoreUpdateRoundId][user]) { pendingScoreUpdates--; } }
As it can be seen, it decreases pendingScoreUpdates
. This would break internal accounting. This is especially a problem as then when the function updateScores
is called with all users pending score updates, it will revert due to underflow:
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--; // @audit - will revert here due to underflow isScoreUpdated[nextScoreUpdateRoundId][user] = true; unchecked { i++; } emit UserScoreUpdated(user); } }
Since pendingScoreUpdates
initial value will be lower than the number of accounts updated, it will unexpectedly revert to underflow.
Note that this issue could both happen intentionally and unintentionally, hence the medium severity of it.
Manual review
Add the following change to _mint
:
function _mint(bool isIrrevocable, address user) internal { if (tokens[user].exists) revert IneligibleToClaim(); tokens[user].exists = true; tokens[user].isIrrevocable = isIrrevocable; + isScoreUpdated[nextScoreUpdateRoundId][user] = true; if (isIrrevocable) { totalIrrevocable++; } else { totalRevocable++; } if (totalIrrevocable > irrevocableLimit || totalRevocable > revocableLimit) revert InvalidLimit(); emit Mint(user, isIrrevocable); }
Context
#0 - c4-pre-sort
2023-10-04T22:06:57Z
0xRobocop marked the issue as duplicate of #555
#1 - c4-judge
2023-11-02T00:58:46Z
fatherGoose1 marked the issue as satisfactory
#2 - c4-judge
2023-11-05T00:52:22Z
fatherGoose1 changed the severity to 3 (High Risk)