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: 238/283
Findings: 1
Award: $1.01
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: klau5
Also found by: 0xAlix2, 0xCiphky, 0xDetermination, 0xG0P1, 0xMosh, 0xabhay, 14si2o_Flint, AlexCzm, Aymen0909, CodeWasp, DanielArmstrong, FloatingPragma, Giorgio, JCN, Jorgect, Kalogerone, KmanOfficial, Kow, KupiaSec, McToady, SpicyMeatball, VAD37, WoolCentaur, ZanyBonzy, alexxander, alexzoid, almurhasan, blutorque, csanuragjain, denzi_, dipp, djxploit, evmboi32, handsomegiraffe, haxatron, immeas, jesjupyter, ke1caM, klau5, lanrebayode77, lil_eth, merlin, merlinboii, nuthan2x, peanuts, shaflow2, shaka, sl1, solmaxis69, stakog, swizz, t0x1c, tallo, ubermensch, vnavascues, yotov721
1.0089 USDC - $1.01
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L454-L455 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L496 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L342-L343
The RankedBattle
smart contract poses a significant risk to the integrity of the staking mechanism. A user can exploit this vulnerability to avoid losing any more staked NRN tokens after a portion of their stake is at risk. By Unstaking all staked tokens except the at-risk portion, the user's amountStaked
becomes zero. Subsequently, if the user loses a battle, the contract attempts to transfer NRN tokens to the StakeAtRisk
address and then subtracts the curStakeAtRisk
from the user's amountStaked
. Since amountStaked is already zero, this subtraction would cause an underflow, allowing the user to avoid any further losses.
A user stakes a significant number of NRN tokens using the RankedBattle.sol::stakeNRN
function and participates in battles until a portion of their stake is at risk.
The user then calls RankedBattle.sol::unstakeNRN
to withdraw all staked tokens except those at risk, effectively setting their amountStaked
for the given tokenId
to zero.
When the user participates in subsequent battles, the updateBattleRecord
function is invoked, and the user passes the initial check because their amountStaked[tokenId]
is zero, but they have a non-zero stakeAtRisk.
if (amountStaked[tokenId] + stakeAtRisk > 0) { _addResultPoints(battleResult, tokenId, eloFactor, mergingPortion, fighterOwner); }
Within the updateBattleRecord
function, calls the _addResultPoints
:
Within the _addResultPoints
function, the curStakeAtRisk is calculated as follows:
curStakeAtRisk = (bpsLostPerLoss * (amountStaked[tokenId] + stakeAtRisk)) / 10**4; // Given that bpsLostPerLoss is 10 basis points, amountStaked[tokenId] is 0, and stakeAtRisk is 1000 * 1e18, the curStakeAtRisk calculation results in a value greater than the amountStaked[tokenId]. // try out in remix run the following code: // SPDX-License-Identifier: MIT pragma solidity 0.8.22; contract PreccisionLoss{ uint public curStakeAtRiskANS; uint public BPS = 10; uint public AmountStaked = 0; uint public StakeAtrisk = 1000 * 1e18; function curStakeAtRisk() public returns (uint) { curStakeAtRiskANS = (BPS * (AmountStaked + StakeAtrisk)) / 10**4; return curStakeAtRiskANS; } // output = 1000000000000000000 }
If the user loses the battle, the following conditional logic is executed:
// the user have curStakeAmount > amountStaked[tokenId]) if (curStakeAtRisk > amountStaked[tokenId]) { curStakeAtRisk = amountStaked[tokenId]; // curStakeAtRisk is now set to 0 because amountStaked[tokenId] is 0 } // As a result, curStakeAtRisk is incorrectly set to zero due to the user's amountStaked being zero. This leads to the following problematic behavior: bool success = _neuronInstance.transfer(_stakeAtRiskAddress, curStakeAtRisk); if (success) { _stakeAtRiskInstance.updateAtRiskRecords(curStakeAtRisk, tokenId, fighterOwner); // An underflow occurs here because curStakeAtRisk is non-zero (due to tokens still in stakeAtRisk), // but amountStaked[tokenId] is zero. This causes a revert due to underflow, preventing the user from // losing any NRN tokens even though they lost the battle. Additionally, the contract fails to update // the battle record for the tokenId because of this underflow revert. amountStaked[tokenId] -= curStakeAtRisk; }
The transaction reverts due to the underflow, and the user avoids losing any NRN tokens despite losing the battle. Furthermore, the contract is unable to update the battle record state changes for the tokenId.
The user can exploit this vulnerability by repeating the process: they can unstake any reclaimedTokens
, setting amountStaked
back to zero, and continue to participate in battles without any additional risk. This cycle can be repeated until the user has fully recovered the entire stakeAtRisk
amount, effectively nullifying the intended risk-reward dynamic of the staking system.
Manual Review
In UpdateBattleRecords
:
function updateBattleRecord( uint256 tokenId, uint256 mergingPortion, uint8 battleResult, uint256 eloFactor, bool initiatorBool ) external { require(msg.sender == _gameServerAddress); require(mergingPortion <= 100); address fighterOwner = _fighterFarmInstance.ownerOf(tokenId); require( !initiatorBool || _voltageManagerInstance.ownerVoltageReplenishTime(fighterOwner) <= block.timestamp || _voltageManagerInstance.ownerVoltage(fighterOwner) >= VOLTAGE_COST ); _updateRecord(tokenId, battleResult); uint256 stakeAtRisk = _stakeAtRiskInstance.getStakeAtRisk(tokenId); - if (amountStaked[tokenId] + stakeAtRisk > 0) + if (amountStaked[tokenId] + stakeAtRisk > 0 && amountStaked [tokenId > 0]){ _addResultPoints(battleResult, tokenId, eloFactor, mergingPortion, fighterOwner); } if (initiatorBool) { _voltageManagerInstance.spendVoltage(fighterOwner, VOLTAGE_COST); } totalBattles += 1; }
2.Introduce a minimum stake requirement for participating in battles to prevent users from gaming the system by staking zero tokens.
Under/Overflow
#0 - c4-pre-sort
2024-02-25T03:59:50Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-25T04:00:27Z
raymondfam marked the issue as duplicate of #1641
#2 - c4-judge
2024-03-12T03:34:04Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-12T04:01:25Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-12T04:03:31Z
HickupHH3 changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-03-13T10:45:06Z
HickupHH3 marked the issue as satisfactory