AI Arena - dutra'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: 218/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
:robot:_30_group
duplicate-932

External Links

Lines of code

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/MergingPool.sol#L139-L167

Vulnerability details

Impact

An attacker can call the function claimRewards in the contract MergingPool with arbitrary parameters.

In the execution path: claimRewards -> _fighterFarmInstance.mintFromMergingPool -> __createNewFighter we can see that the attributes element and weight can be user controlled, enabling an attacker to send a very low or very high value, thus breaking the logic of the game.

As the documentation mentions, we have a small number of elements, and each element corresponds to an integer value. But an attacker can assign a value that is not linked to any element, benefiting themself.

In the weight parameter we can assign a value of zero or a value than 100, which should be the limit.

Proof of Concept

Add the following test case to MerginPool.t.sol file:

function testAttackFighterParams() 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);
    }

Run with the command: forge test --mt testAttackFighterParams -vvvv

Tools Used

vscode

Validate the attributes with lower and upper bound according to the project's documentation.

Assessed type

Invalid Validation

#0 - c4-pre-sort

2024-02-24T09:11:44Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2024-02-24T09:11:53Z

raymondfam marked the issue as duplicate of #226

#2 - c4-judge

2024-03-11T10:31:11Z

HickupHH3 marked the issue as satisfactory

Lines of code

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

Vulnerability details

Impact

The function AiArenaHelper::createPhysicalAttributes is responsible for calculating the rarity of the fighter's physical attributes.

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]
            );
        }
    }

In line 107, we can see that rarity is calculated according to dna, and dna is equal to the address of the fighter's owner.

With this design, users can write a script to generate random addresses until reaching an address that corresponds to a rarity well above the average of the users, gaining an advantage on selling this fighters.

Proof of Concept

Create new addresses 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 MerginPool::claimRewards or the FighterFarm::redeemMintPass functions.

Tools Used

vscode

Add more invariants to the rarity calculation, using factors that cannot be bruteforced by users.

Assessed type

Other

#0 - c4-pre-sort

2024-02-24T02:05:14Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-24T02:05:27Z

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:54:14Z

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

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