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: 208/283
Findings: 1
Award: $2.06
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: t0x1c
Also found by: 0rpse, 0xAadi, 0xBinChook, 0xCiphky, 0xDetermination, 14si2o_Flint, AC000123, Aamir, Abdessamed, Blank_Space, CodeWasp, DanielArmstrong, DarkTower, Draiakoo, Honour, Kalogerone, Krace, McToady, Merulez99, MidgarAudits, MrPotatoMagic, PedroZurdo, Silvermist, Tychai0s, VAD37, Velislav4o, VrONTg, WoolCentaur, YouCrossTheLineAlfie, ZanyBonzy, alexxander, aslanbek, btk, csanuragjain, d3e4, dimulski, djxploit, erosjohn, evmboi32, fnanni, forgebyola, forkforkdog, handsomegiraffe, immeas, israeladelaja, juancito, ktg, n0kto, neocrao, ni8mare, okolicodes, peanuts, petro_1912, shaflow2, shaka, swizz, ubermensch, ubl4nk, yotov721
2.0593 USDC - $2.06
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L244 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L439
RankedBattle::stakeNRN
leads to a rounding error and exploit in the system.The RankedBattle::stakeNRN
function enables a player to stake 1 wei, doing so the player is eligible to battle for points.
Thus,in the curStakeAtRisk = (bpsLostPerLoss * (amountStaked[tokenId] + stakeAtRisk)) / 10 ** 4
calculation ,the value gets rounded down to 0,because the division results in a decimal number and it rounds down to the nearest number.
This error leads to risk-free guaranteed losses for the player , making it impossible for the
player to lose any tokens, granting him only a winnable outcome.
RankedBattle::stakeNRN
function,which is enabling him to battle for points.stakeAtRisk
tokens are 0. (i've lost 2 times in a row in the PoC for further proof).Place the following test into StakeAtRisk.t.sol
.
function testGetStakeAtRisk() public { address player = vm.addr(3); uint256 STAKE_AMOUNT = 0.000000000000000001 * 10 ** 18; // 1 wei Neuron uint256 tokenId = 0; // player gets a fighter _mintFromMergingPool(player); // tokenId - 0 // player gets NRN _fundUserWith4kNeuronByTreasury(player); vm.prank(player); // player stakes NRN _rankedBattleContract.stakeNRN(STAKE_AMOUNT, 0); assertEq(_rankedBattleContract.amountStaked(0), STAKE_AMOUNT); // true console.log("AmountStaked:", _rankedBattleContract.amountStaked(tokenId)); // player battles vm.prank(address(_GAME_SERVER_ADDRESS)); // loses battle _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); uint256 curStakeAtRiskAfter1stLostBattle = ( 10 * (_rankedBattleContract.amountStaked(tokenId) + _stakeAtRiskContract.getStakeAtRisk(tokenId)) ) / 10 ** 4; uint256 amountStakedAfter1stLostBattle = _rankedBattleContract.amountStaked(tokenId); uint256 stakeAtRiskAfter1stLostBattle = _stakeAtRiskContract.getStakeAtRisk(tokenId); assertEq(stakeAtRiskAfter1stLostBattle, 0); // 0 risk for the player console.log("StakeAtRisk_After_1st_LostBattle:", _stakeAtRiskContract.getStakeAtRisk(tokenId)); console.log("AmountStaked_After_1st_LostBattle:", _rankedBattleContract.amountStaked(tokenId)); console.log("curStakeAtRisk_After_1st_LostBattle:", curStakeAtRiskAfter1stLostBattle); // loses battle for 2nd time vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); uint256 curStakeAtRiskAfter2ndLostBattle = (10 * (amountStakedAfter1stLostBattle + stakeAtRiskAfter1stLostBattle)) / 10 ** 4; uint256 stakeAtRiskAfter2ndLostBattleShouldBe = stakeAtRiskAfter1stLostBattle + curStakeAtRiskAfter2ndLostBattle; uint256 amountStakedAfter2ndLostBattleShouldBe = amountStakedAfter1stLostBattle - curStakeAtRiskAfter2ndLostBattle; assertEq(_rankedBattleContract.amountStaked(tokenId), amountStakedAfter2ndLostBattleShouldBe); assertEq(_stakeAtRiskContract.getStakeAtRisk(tokenId), stakeAtRiskAfter2ndLostBattleShouldBe); console.log("curStakeAtRisk_After_2nd_LostBattle:", curStakeAtRiskAfter2ndLostBattle); console.log("AmountStaked_After_2nd_LostBattle::", _rankedBattleContract.amountStaked(tokenId)); console.log("stakeAtRisk_After_2nd_LostBattle:", _stakeAtRiskContract.getStakeAtRisk(tokenId)); console.log("AmountToBeLost:", _stakeAtRiskContract.amountLost(player)); // player wins vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); // The player has 750 points assertEq(_rankedBattleContract.accumulatedPointsPerFighter(tokenId, 0), 750); console.log("stakeAtRisk_After_2_LostBattles_And_1Win:", _stakeAtRiskContract.getStakeAtRisk(tokenId)); console.log( "AccumulatedPointsPerFigter_After_2_LostBattles_And_1Win:", _rankedBattleContract.accumulatedPointsPerFighter(tokenId, 0) );
I will consider the staking amount to be atleast 1e18, not allowing the users to stake a smaller unit than Ether, potentially rounding the amount to 0.
function stakeNRN(uint256 amount, uint256 tokenId) external { - require(amount > 0, "Amount cannot be 0"); + require(amount >= 1e18, "Amount must be atleast 1"); require(_fighterFarmInstance.ownerOf(tokenId) == msg.sender, "Caller does not own fighter"); require(_neuronInstance.balanceOf(msg.sender) >= amount, "Stake amount exceeds balance"); require(hasUnstaked[tokenId][roundId] == false, "Cannot add stake after unstaking this round"); _neuronInstance.approveStaker(msg.sender, address(this), amount); bool success = _neuronInstance.transferFrom(msg.sender, address(this), amount); if (success) { if (amountStaked[tokenId] == 0) { _fighterFarmInstance.updateFighterStaking(tokenId, true); } amountStaked[tokenId] += amount; globalStakedAmount += amount; stakingFactor[tokenId] = _getStakingFactor( // e math calculations tokenId, _stakeAtRiskInstance.getStakeAtRisk(tokenId)); _calculatedStakingFactor[tokenId][roundId] = true; emit Staked(msg.sender, amount); } }
Math
#0 - c4-pre-sort
2024-02-22T16:07:52Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T16:08:00Z
raymondfam marked the issue as duplicate of #38
#2 - c4-judge
2024-03-07T02:49:49Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-07T02:58:22Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-07T03:17:16Z
HickupHH3 marked the issue as satisfactory