Platform: Code4rena
Start Date: 09/02/2024
Pot Size: $60,500 USDC
Total HM: 17
Participants: 283
Period: 12 days
Judge:
Id: 328
League: ETH
Rank: 252/283
Findings: 1
Award: $0.23
π Selected for report: 0
π Solo Findings: 0
π Selected for report: ahmedaghadi
Also found by: 0x13, 0xAleko, 0xDetermination, 0xKowalski, 0xPluto, 0xRiO, 0xvj, AlexCzm, Avci, BARW, BigVeezus, Cryptor, DeFiHackLabs, Draiakoo, Fitro, Giorgio, GoSlang, Greed, Honour, Kalogerone, KmanOfficial, Krace, McToady, MidgarAudits, MrPotatoMagic, Nyxaris, ReadyPlayer2, Ryonen, SovaSlava, SpicyMeatball, VAD37, _eperezok, alexzoid, almurhasan, btk, cu5t0mpeo, deadrxsezzz, djxploit, dvrkzy, emrekocak, erosjohn, evmboi32, fnanni, grearlake, inzinko, jesjupyter, jesusrod15, josephdara, ke1caM, klau5, ktg, ladboy233, merlinboii, nuthan2x, peanuts, pipidu83, pontifex, radev_sw, sl1, sobieski, soliditywala, t0x1c, taner2344, vnavascues, y4y, yovchev_yoan, zaevlad
0.2347 USDC - $0.23
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/MergingPool.sol#L139 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/MergingPool.sol#L118
In MergingPool.sol the function claimRewards is vulnerable to a dos attack. The main impact of this attack is that once the cost of calling the function exceeds the block gas limit the user can no longer claim their funds and they remain locked in the protocol permanently.
The error is quite evident in analyzing the claimRewards function as follows
function claimRewards( string[] calldata modelURIs, string[] calldata modelTypes, uint256[2][] calldata customAttributes ) external { uint256 winnersLength; uint32 claimIndex = 0; uint32 lowerBound = numRoundsClaimed[msg.sender]; //First loop for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) { numRoundsClaimed[msg.sender] += 1; winnersLength = winnerAddresses[currentRound].length; //Second loop for (uint32 j = 0; j < winnersLength; j++) { if (msg.sender == winnerAddresses[currentRound][j]) { _fighterFarmInstance.mintFromMergingPool( msg.sender, modelURIs[claimIndex], modelTypes[claimIndex], customAttributes[claimIndex] ); claimIndex += 1; } } } if (claimIndex > 0) { emit Claimed(msg.sender, claimIndex); } }
From above it is clear to see two unbounded loops namely the First and Second loop.
The First loop is unbounded because it is dependent on the number of rounds a user has not claimed rewards. For example consider a user that has never had a single victory in the course of a 1000 rounds and finally wins a reward. This means this loop will consume an enormous amount of gas. To further explain consider this excerpt from the code above:
uint32 lowerBound = numRoundsClaimed[msg.sender]; for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) {
It is clear to see that if a user has never claimed their lowerBound will be equal to zero and the roundId will be a larger value since the user has not claimed for multiple rounds.
However this is compounded by the unbounded loop 2, which iterates through the number of winners for each current roundId. Although the owner provides the winners array it is evident that even though the user has no winnings for that roundId, the function still iterates over the loop, causing a massive gas overhead. Consider:
winnersLength = winnerAddresses[currentRound].length; for (uint32 j = 0; j < winnersLength; j++) {
It is evident that this loop also causes a massive gas overhead. Once the call to claimRewards exceeds the maximum block limit a users rewards are locked permanently in the protocol.
Manual Review
I would recommend the function be overhauled to allow users to claim for only a specific round, or to allow users to claim for a specific range.
DoS
#0 - c4-pre-sort
2024-02-23T23:41:49Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T23:42:06Z
raymondfam marked the issue as duplicate of #1541
#2 - c4-judge
2024-03-11T13:00:24Z
HickupHH3 marked the issue as duplicate of #216
#3 - c4-judge
2024-03-11T13:08:07Z
HickupHH3 changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-03-11T13:08:23Z
HickupHH3 marked the issue as partial-50
#5 - c4-judge
2024-03-21T02:07:40Z
HickupHH3 marked the issue as satisfactory
π Selected for report: ahmedaghadi
Also found by: 0x13, 0xAleko, 0xDetermination, 0xKowalski, 0xPluto, 0xRiO, 0xvj, AlexCzm, Avci, BARW, BigVeezus, Cryptor, DeFiHackLabs, Draiakoo, Fitro, Giorgio, GoSlang, Greed, Honour, Kalogerone, KmanOfficial, Krace, McToady, MidgarAudits, MrPotatoMagic, Nyxaris, ReadyPlayer2, Ryonen, SovaSlava, SpicyMeatball, VAD37, _eperezok, alexzoid, almurhasan, btk, cu5t0mpeo, deadrxsezzz, djxploit, dvrkzy, emrekocak, erosjohn, evmboi32, fnanni, grearlake, inzinko, jesjupyter, jesusrod15, josephdara, ke1caM, klau5, ktg, ladboy233, merlinboii, nuthan2x, peanuts, pipidu83, pontifex, radev_sw, sl1, sobieski, soliditywala, t0x1c, taner2344, vnavascues, y4y, yovchev_yoan, zaevlad
0.2347 USDC - $0.23
The main impact is that user rewards can be locked in the protocol permanently as soon as the cost of calling the function exceeds the block gas limit.
Suppose a user named Alice is a user of the protocol.
Alice has been playing for a while and is accumulating rewards but has not claimed any yet, the current roundId in Ranked Battle is 1000. Which means there have been a 1000 total rounds since the beginning of the protocol. So Alice decides to claim by calling the function claimNRN which is as follows
function claimNRN() external { require(numRoundsClaimed[msg.sender] < roundId, "Already claimed NRNs for this period"); uint256 claimableNRN = 0; uint256 nrnDistribution; uint32 lowerBound = numRoundsClaimed[msg.sender]; for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) { nrnDistribution = getNrnDistribution(currentRound); claimableNRN += ( accumulatedPointsPerAddress[msg.sender][currentRound] * nrnDistribution ) / totalAccumulatedPoints[currentRound]; numRoundsClaimed[msg.sender] += 1; } if (claimableNRN > 0) { amountClaimed[msg.sender] += claimableNRN; _neuronInstance.mint(msg.sender, claimableNRN); emit Claimed(msg.sender, claimableNRN); } }
Taking a code block from above and annotating for the current situation:
//uint32 lowerBound = 0; //Since the user has not claimed before uint32 lowerBound = numRoundsClaimed[msg.sender]; //for (uint32 currentRound = 0; currentRound < 1000; currentRound++) {... //Substituting since a 1000 rounds have passed for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) {
It is evident that the longer a user takes to claim, the higher the chance of their call to claim exceeding the block gas limit.
Manual Review
I would recommend that the function be changed to allow user to claim for a specific roundId or a certain range.
DoS
#0 - c4-pre-sort
2024-02-25T02:18:57Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-25T02:19:24Z
raymondfam marked the issue as duplicate of #1541
#2 - c4-judge
2024-03-11T13:00:25Z
HickupHH3 marked the issue as duplicate of #216
#3 - c4-judge
2024-03-11T13:08:05Z
HickupHH3 changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-03-12T02:41:04Z
HickupHH3 marked the issue as partial-50
#5 - c4-judge
2024-03-21T02:07:55Z
HickupHH3 marked the issue as satisfactory