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: 60/115
Findings: 1
Award: $32.27
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xDetermination
Also found by: 0xblackskull, 0xweb3boy, ADM, Breeje, Pessimistic, PwnStars, SBSecurity, Satyam_Sharma, ThreeSigma, al88nsk, blutorque, debo, dethera, maanas, neumo, oakcobalt, pina, said, sces60107, tapir, tsvetanovv, xAriextz
32.2731 USDC - $32.27
https://github.com/code-423n4/2023-09-venus/blob/b11d9ef9db8237678567e66759003138f2368d23/contracts/Tokens/Prime/Prime.sol#L196-L230 https://github.com/code-423n4/2023-09-venus/blob/b11d9ef9db8237678567e66759003138f2368d23/contracts/Tokens/Prime/Prime.sol#L204-L229 https://github.com/code-423n4/2023-09-venus/blob/b11d9ef9db8237678567e66759003138f2368d23/contracts/Tokens/Prime/Prime.sol#L208 https://github.com/code-423n4/2023-09-venus/blob/b11d9ef9db8237678567e66759003138f2368d23/contracts/Tokens/Prime/Prime.sol#L224-L226
If for some user
from the input array users
, the condition isScoreUpdated[nextScoreUpdateRoundId][user]
in:
if (isScoreUpdated[nextScoreUpdateRoundId][user]) continue;
is true
, continue
will be executed and all of the gas will be consumed by the infinite loop leading to an out of gas exception and the transaction will revert. This means the current implementation, vulnerable to an infinite loop, can cause a DoS for the whole updateScores
function and thus none of the users from the users
input array will have their scores updated, but all of the gas will be consumed.
In the file Prime.sol
there is a function called updateScores
. The following for
loop in updateScores
is problematic:
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); }
The issue in the outer for
loop is that the loop variable i
update:
unchecked { i++; }
comes after the line:
if (isScoreUpdated[nextScoreUpdateRoundId][user]) continue;
which means if isScoreUpdated[nextScoreUpdateRoundId][user]
ever becomes true
, an infinite loop happens (because the for
loop will be executed with the i
from the previous iteration again and again and again since unchecked { i++; }
is never executed because of the continue
statement).
Manual review.
There are many ways to fix this issue:
i
variable to its canonical place in the for
loop:function updateScores(address[] memory users) external { if (pendingScoreUpdates == 0) revert NoScoreUpdatesRequired(); if (nextScoreUpdateRoundId == 0) revert NoScoreUpdatesRequired(); for (uint256 i = 0; i < users.length; ++i) { 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; emit UserScoreUpdated(user); } }
This fix will make the code more readable and automatically fixes the issue.
N.B. As a general remark (when gas optimization is not of utmost importance), I would advise to always use the for
loop written like:
for (uint256 i = 0; i < users.length; ++i)
over the one with the trailing update (unchecked { i++; }
) that currently causes the infinite loop - it makes the code more understadable, easier to read and prevents issues like infinity loops.
i
variable (unchecked { i++; }
) right before continue
. Using this approach, the code would look like: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]) { unchecked { i++; } 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); } }
This implementation makes sure the update of the i
variable is performed before continue
is executed and the infinity loop is avoided.
Loop
#0 - c4-pre-sort
2023-10-04T22:44:35Z
0xRobocop marked the issue as duplicate of #556
#1 - c4-judge
2023-11-01T20:22:53Z
fatherGoose1 marked the issue as satisfactory