AI Arena - merlin'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: 239/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/RankedBattle.sol#L342 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L476-L478 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L491-L498

Vulnerability details

Bug Description

In the RankedBattle smart contract, there is an updateBattleRecord function designed to update the battle record for a fighter. If a fighter does not have any points for this round, NRNs are at risk of being lost. After a user loses all staked NRN tokens and they all transfer to the StakeAtRisk smart contract, they can still participate in battles without risking the loss of points or NRN tokens when they lose.

Proof-of-Concept

When the updateBattleRecord function is called, the internal function _addResultPoints is invoked if the condition amountStaked[tokenId] + stakeAtRisk > 0 is met: RankedBattle.sol#L341-L344

uint256 stakeAtRisk = _stakeAtRiskInstance.getStakeAtRisk(tokenId);
        if (amountStaked[tokenId] + stakeAtRisk > 0) {
            _addResultPoints(
                battleResult,
                tokenId,
                eloFactor,
                mergingPortion,
                fighterOwner
            );
        }

The smart contract ensures that users cannot lose more NRNs than they have staked in their staking pool: RankedBattle.sol#L472-L478

} else if (battleResult == 2) {
            /// If the user lost the match

            /// Do not allow users to lose more NRNs than they have in their staking pool
            if (curStakeAtRisk > amountStaked[tokenId]) {
                curStakeAtRisk = amountStaked[tokenId];
            }

So when the user has amountStaked = 0, stakeAtRisk > 0, then the current curStakeAtRisk = 0, which means the user will not lose any NRN tokens: RankedBattle.sol#L491-L498

 } else {
                /// If the fighter does not have any points for this round, NRNs become at risk of being lost
                bool success = _neuronInstance.transfer(_stakeAtRiskAddress, curStakeAtRisk);
                if (success) {
                    _stakeAtRiskInstance.updateAtRiskRecords(curStakeAtRisk, tokenId, fighterOwner);
                    amountStaked[tokenId] -= curStakeAtRisk;
                }
            }

The following test case illustrates the scenario:

function testPlayMatchWithZeroAmountStake() public {
        address player = vm.addr(3);
        _mintFromMergingPool(player);
        _fundUserWith4kNeuronByTreasury(player);
        vm.prank(player);
        // stakes 1_000 NRN token
        _rankedBattleContract.stakeNRN(1_000 * 10 ** 18, 0);

        // loses battle and 1 NRN token
        vm.prank(address(_GAME_SERVER_ADDRESS));
        _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true);
        assertEq(_stakeAtRiskContract.getStakeAtRisk(0), 1 * 10 ** 18);

        // unstakes 999 NRN tokens
        vm.prank(player);
        _rankedBattleContract.unstakeNRN(999 * 10 ** 18, 0);

        // The user participates in battles while having amountStaked - 0,
        // meaning they are not risking anything
        vm.prank(address(_GAME_SERVER_ADDRESS));
        _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true);
        assertEq(_stakeAtRiskContract.getStakeAtRisk(0), 1 * 10 ** 18);
    }

Impact

The user will not lose points or NRN tokens when all their tokens are in the StakeAtRisk smart contract.

Tools Used

Manual

Consider not allowing the user to participate in a battle when amountStaked = 0:

function updateBattleRecord(
        uint256 tokenId,
        uint256 mergingPortion,
        uint8 battleResult,
        uint256 eloFactor,
        bool initiatorBool
    ) external {
        // code
        uint256 stakeAtRisk = _stakeAtRiskInstance.getStakeAtRisk(tokenId);
-        if (amountStaked[tokenId] + stakeAtRisk > 0) {
+        if (amountStaked[tokenId] != 0 && amountStaked[tokenId] + stakeAtRisk > 0) {
            _addResultPoints(
                battleResult,
                tokenId,
                eloFactor,
                mergingPortion,
                fighterOwner
            );
        }
        // code
    }

Assessed type

Invalid Validation

#0 - c4-pre-sort

2024-02-22T16:50:32Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2024-02-22T16:50:40Z

raymondfam marked the issue as duplicate of #833

#2 - c4-pre-sort

2024-02-24T05:28:18Z

raymondfam marked the issue as sufficient quality report

#3 - c4-judge

2024-03-13T11:32:38Z

HickupHH3 marked the issue as duplicate of #1641

#4 - c4-judge

2024-03-13T14:17:55Z

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