AI Arena - Matue'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: 216/283

Findings: 2

Award: $1.92

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

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/main/src/FighterFarm.sol#L462-L474

Vulnerability details

Impact

In MergingPool, we have the claimRewards function used to claim a fighter, in which we arbitrate the fighter's parameters, as follows:

MergingPool.sol#L139-L167

function claimRewards( string[] calldata modelURIs, string[] calldata modelTypes, uint256[2][] calldata customAttributes ) external { uint256 winnersLength; uint32 claimIndex = 0; uint32 lowerBound = numRoundsClaimed[msg.sender]; for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) { numRoundsClaimed[msg.sender] += 1; winnersLength = winnerAddresses[currentRound].length; for (uint32 j = 0; j < winnersLength; j++) { if (msg.sender == winnerAddresses[currentRound][j]) { _fighterFarmInstance.mintFromMergingPool( msg.sender, modelURIs[claimIndex], modelTypes[claimIndex], customAttributes[claimIndex] ); claimIndex += 1; } } } if (claimIndex > 0) { emit Claimed(msg.sender, claimIndex); } }

FighterFarm.sol#L313-L331

function mintFromMergingPool( address to, string calldata modelHash, string calldata modelType, uint256[2] calldata customAttributes ) public { require(msg.sender == _mergingPoolAddress); _createNewFighter( to, uint256(keccak256(abi.encode(msg.sender, fighters.length))), modelHash, modelType, 0, 0, customAttributes ); }

Function _createNewFighter mentioned before:

FighterFarm.sol#L484-L531

    function _createNewFighter(
        address to, 
        uint256 dna, 
        string memory modelHash,
        string memory modelType, 
        uint8 fighterType,
        uint8 iconsType,
        uint256[2] memory customAttributes
    ) 
        private 
    {  
        require(balanceOf(to) < MAX_FIGHTERS_ALLOWED);
        uint256 element; 
        uint256 weight;
        uint256 newDna;
        if (customAttributes[0] == 100) {
            (element, weight, newDna) = _createFighterBase(dna, fighterType);
        }
        else {
            element = customAttributes[0];
            weight = customAttributes[1];
            newDna = dna;
        }
        uint256 newId = fighters.length;

        bool dendroidBool = fighterType == 1;
        FighterOps.FighterPhysicalAttributes memory attrs = _aiArenaHelperInstance.createPhysicalAttributes(
            newDna,
            generation[fighterType],
            iconsType,
            dendroidBool
        );
        fighters.push(
            FighterOps.Fighter(
                weight,
                element,
                attrs,
                newId,
                modelHash,
                modelType,
                generation[fighterType],
                iconsType,
                dendroidBool
            )
        );
        _safeMint(to, newId);
        FighterOps.fighterCreatedEmitter(newId, weight, element, generation[fighterType]);
    }

As we can see, the weight and element are completely arbitrated by the users, and can be very low or very high, breaking the logic of the game.

As the documentation mentions here, we have a small number of elements, and each element corresponds to an integer value, however, we can assign a value that is not linked to any element, thus being able to have benefits over other players.

As for weight, we can assign a value of zero, or even much greater than 100 (which is the limit).

Proof of Concept

You can use the following test case in the MerginPool.t.sol file:

function testParametersThreshold() public {
        _mintFromMergingPool(_ownerAddress);
        _mintFromMergingPool(_DELEGATED_ADDRESS);
        assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress);
        assertEq(_fighterFarmContract.ownerOf(1), _DELEGATED_ADDRESS);
        uint256[] memory _winners = new uint256[](2);
        _winners[0] = 0;
        _winners[1] = 1;
        // winners of roundId 0 are picked
        _mergingPoolContract.pickWinner(_winners);
        assertEq(_mergingPoolContract.isSelectionComplete(0), true);
        assertEq(_mergingPoolContract.winnerAddresses(0, 0) == _ownerAddress, true);
        // winner matches ownerOf tokenId
        assertEq(_mergingPoolContract.winnerAddresses(0, 1) == _DELEGATED_ADDRESS, true);
        string[] memory _modelURIs = new string[](2);
        _modelURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m";
        _modelURIs[1] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m";
        string[] memory _modelTypes = new string[](2);
        _modelTypes[0] = "original";
        _modelTypes[1] = "original";
        uint256[2][] memory _customAttributes = new uint256[2][](2);
        _customAttributes[0][0] = uint256(1);
        _customAttributes[0][1] = uint256(0);
        _customAttributes[1][0] = uint256(100000000000);
        _customAttributes[1][1] = uint256(100000000000);
        // winners of roundId 1 are picked
        _mergingPoolContract.pickWinner(_winners);
        // winner claims rewards for previous roundIds

        _mergingPoolContract.claimRewards(_modelURIs, _modelTypes, _customAttributes);
    }

To execute, run this command: forge test --mt testParametersThreshold -vvvv

Tools Used

vscode, foundry

Implement validation on attributes entered by the user, to validate upper and lower limits.

Assessed type

Context

#0 - c4-pre-sort

2024-02-24T09:06:24Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2024-02-24T09:06:37Z

raymondfam marked the issue as duplicate of #226

#2 - c4-judge

2024-03-11T10:29:36Z

HickupHH3 marked the issue as satisfactory

Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/AiArenaHelper.sol#L83-L121

Vulnerability details

Impact

In AiArenaHelper we have the createPhysicalAttributes function, responsible for calculating the rarity of the fighter's physical attributes. As we can see in line 107, rarity is calculated according to dna, and dna is equal to the address of the fighter's owner.

AiArenaHelper.sol#L83-L121

    function createPhysicalAttributes(
        uint256 dna, 
        uint8 generation, 
        uint8 iconsType, 
        bool dendroidBool
    ) 
        external 
        view 
        returns (FighterOps.FighterPhysicalAttributes memory) 
    {
        if (dendroidBool) {
            return FighterOps.FighterPhysicalAttributes(99, 99, 99, 99, 99, 99);
        } else {
            uint256[] memory finalAttributeProbabilityIndexes = new uint[](attributes.length);

            uint256 attributesLength = attributes.length;
            for (uint8 i = 0; i < attributesLength; i++) {
                if (
                  i == 0 && iconsType == 2 || // Custom icons head (beta helmet)
                  i == 1 && iconsType > 0 || // Custom icons eyes (red diamond)
                  i == 4 && iconsType == 3 // Custom icons hands (bowling ball)
                ) {
                    finalAttributeProbabilityIndexes[i] = 50;
                } else {
                    uint256 rarityRank = (dna / attributeToDnaDivisor[attributes[i]]) % 100;
                    uint256 attributeIndex = dnaToIndex(generation, rarityRank, attributes[i]);
                    finalAttributeProbabilityIndexes[i] = attributeIndex;
                }
            }
            return FighterOps.FighterPhysicalAttributes(
                finalAttributeProbabilityIndexes[0],
                finalAttributeProbabilityIndexes[1],
                finalAttributeProbabilityIndexes[2],
                finalAttributeProbabilityIndexes[3],
                finalAttributeProbabilityIndexes[4],
                finalAttributeProbabilityIndexes[5]
            );
        }
    }

Therefore, users can automate a script to generate addresses and calculate the rarity of attributes, until reaching an address that corresponds to a rarity well above the average for legitimate users, thus gaining an advantage and selling these fighters.

Proof of Concept

Create new addresses on the blockchain and calculate the rarity using the createPhysicalAttributes function, repeat until you find a satisfactory rarity. When you find it, you can create the fighter using the claimRewards function of the MerginPool contract or the redeemMintPass function of FighterFarm, for example.

Tools Used

vscode

Try implementing a salt in the rarity calculation, add more variables to the calculation, taking the power of prediction out of the user's control.

Assessed type

Context

#0 - c4-pre-sort

2024-02-24T02:01:56Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-24T02:02:45Z

raymondfam marked the issue as duplicate of #53

#2 - c4-judge

2024-03-06T03:49:26Z

HickupHH3 changed the severity to 3 (High Risk)

#3 - c4-judge

2024-03-06T03:53:26Z

HickupHH3 marked the issue as satisfactory

#4 - c4-judge

2024-03-15T02:10:54Z

HickupHH3 changed the severity to 2 (Med Risk)

#5 - c4-judge

2024-03-22T04:23:05Z

HickupHH3 marked the issue as duplicate of #376

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