AI Arena - 0xG0P1's results

In AI Arena you train an AI character to battle in a platform fighting game. Imagine a cross between Pokémon and Super Smash Bros, but the characters are AIs, and you can train them to learn almost any skill in preparation for battle.

General Information

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

AI Arena

Findings Distribution

Researcher Performance

Rank: 240/283

Findings: 1

Award: $1.01

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/StakeAtRisk.sol#L104

Vulnerability details

Impact

In this scenario, it's crucial to acknowledge that the user's rewards will be forfeited due to their inability to reclaim the stakeAtRisk NRN. This unfortunate outcome stems from the transaction reverting, rendering the reclaim process impossible. As a result, the user faces the loss of their earned rewards, adding to the significance of addressing the underlying issue to ensure a fair and functional system.

Proof of Concept

In the given scenario, Bob, as the owner of a fighter NFT with ID 0, initially stakes 3000 * 10 ** 18 NRN (Neuron tokens) and engages in a battle, unfortunately losing all his NRN in the first round. Subsequently, Bob decides to unstake by calling the Unstake function to transfer his fighter NFT. Upon transferring the NFT to Alice, the amountLost associated with Bob's address reflects a value of 3000 * 10 ** 18 NRN.

Now, Alice participates in a battle and emerges victorious. Despite Alice not staking any NRN herself, the stakeAtRisk for the token ID 0 remains at 3000 * 10 ** 18 NRN, allowing her to claim the NRN. However, when the game server executes the updateBattleRecord function, triggering the _addResultPoints function, which subsequently invokes reclaimNRN in StakeAtRisk.sol, a transaction revert occurs. This reversal transpires because the amountLost for Alice is decremented to 0, resulting in an arithmetic underflow.

Test :

    function testReclaim() public{
        address bob = address(4);
        address alice = address(5);
        uint256 stakeAmount = 3_000 * 10 ** 18;
        _fundUserWith4kNeuronByTreasury(bob);
        _fundUserWith4kNeuronByTreasury(alice);
        _mintFromMergingPool(bob);
        vm.startPrank(bob);
        _rankedBattleContract.stakeNRN(stakeAmount,0);
        vm.stopPrank();
        vm.startPrank(address(_GAME_SERVER_ADDRESS));
        _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true);
        vm.stopPrank();
        vm.startPrank(bob);
        _rankedBattleContract.unstakeNRN(5000 * 10 ** 18,0);
        _fighterFarmContract.safeTransferFrom(bob,alice,0);
        vm.stopPrank();

        address nn = _fighterFarmContract.ownerOf(0);
        console.log(alice,nn);

        uint stakeRisk = _stakeAtRiskContract.getStakeAtRisk(0);
        console.log(stakeRisk);
        uint amountL = _stakeAtRiskContract.amountLost(alice);
        console.log(amountL);

        vm.startPrank(address(_GAME_SERVER_ADDRESS));
        _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true);
        vm.stopPrank();

        uint stakeRisk1 = _stakeAtRiskContract.getStakeAtRisk(0);
        console.log(stakeRisk1);

    }

Results :

Running 7 tests for test/StakeAtRisk.t.sol:StakeAtRiskTest [PASS] testGetStakeAtRisk() (gas: 879916) [FAIL. Reason: panic: arithmetic underflow or overflow (0x11)] testReclaim() (gas: 1049485) [PASS] testReclaimNRN() (gas: 811810) [PASS] testSetNewRoundFromNonRankedBattle() (gas: 13479) [PASS] testSetNewRoundWithNoStakeAtRisk() (gas: 52248) [PASS] testSetNewRoundWithStakeAtRisk() (gas: 889159) [PASS] testUpdateAtRiskRecords() (gas: 885255) Test result: FAILED. 6 passed; 1 failed; 0 skipped; finished in 14.07ms

Tools Used

Manual Review

function unstakeNRN(uint256 amount, uint256 tokenId) external { require(_fighterFarmInstance.ownerOf(tokenId) == msg.sender, "Caller does not own fighter"); require(stakeAtRisk[roundId][tokenId] == 0, "Cant unstake") //Add the Following check if (amount > amountStaked[tokenId]) { amount = amountStaked[tokenId]; } amountStaked[tokenId] -= amount; globalStakedAmount -= amount; stakingFactor[tokenId] = _getStakingFactor( tokenId, _stakeAtRiskInstance.getStakeAtRisk(tokenId) ); _calculatedStakingFactor[tokenId][roundId] = true; hasUnstaked[tokenId][roundId] = true; bool success = _neuronInstance.transfer(msg.sender, amount); if (success) { if (amountStaked[tokenId] == 0) { _fighterFarmInstance.updateFighterStaking(tokenId, false); } emit Unstaked(msg.sender, amount); } }

Assessed type

DoS

#0 - c4-pre-sort

2024-02-24T04:52:26Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-24T04:52:34Z

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:30Z

HickupHH3 changed the severity to 2 (Med Risk)

#5 - c4-judge

2024-03-13T10:05:38Z

HickupHH3 marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter