AI Arena - Varun_05'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: 213/283

Findings: 2

Award: $1.96

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Lines of code

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

Vulnerability details

Impact

There is a limit which is maxRerollsAllowed for a particular fighter type but reRoll function there is no check for identifying which fighterType does a fighter belongs to which can cause numRerolls to exceed maxRerollsAllowed for a particular fighter.

Proof of Concept

Following is the reRoll function

function reRoll(uint8 tokenId, uint8 fighterType) public { require(msg.sender == ownerOf(tokenId)); require(numRerolls[tokenId] < maxRerollsAllowed[fighterType]); require(_neuronInstance.balanceOf(msg.sender) >= rerollCost, "Not enough NRN for reroll"); _neuronInstance.approveSpender(msg.sender, rerollCost); bool success = _neuronInstance.transferFrom(msg.sender, treasuryAddress, rerollCost); if (success) { numRerolls[tokenId] += 1; uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId]))); (uint256 element, uint256 weight, uint256 newDna) = _createFighterBase(dna, fighterType); fighters[tokenId].element = element; fighters[tokenId].weight = weight; fighters[tokenId].physicalAttributes = _aiArenaHelperInstance.createPhysicalAttributes( newDna, generation[fighterType], fighters[tokenId].iconsType, fighters[tokenId].dendroidBool ); _tokenURIs[tokenId] = ""; } }

From above it is clear that fighterType is an input which is set but the user so he can specify any fighterType even the wrong fighterType for that particular tokenId.So if there is different value of maxRerollsAllowed[fighterType] for different fighter types then a user can specify fighterType which allows more rerolls which can lead to exceeding the limit of maxRerolls of fightertype which the tokenId originally belongs to.

Tools Used

Manual Review

add the following check

function reRoll(uint8 tokenId, uint8 fighterType) public { require(msg.sender == ownerOf(tokenId)); require(numRerolls[tokenId] < maxRerollsAllowed[fighterType]); require(_neuronInstance.balanceOf(msg.sender) >= rerollCost, "Not enough NRN for reroll"); @==> require(fighterType == fighters[tokenId].dendroidBool?1:0,"Wrong fighterType); _neuronInstance.approveSpender(msg.sender, rerollCost); bool success = _neuronInstance.transferFrom(msg.sender, treasuryAddress, rerollCost); if (success) { numRerolls[tokenId] += 1; uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId]))); (uint256 element, uint256 weight, uint256 newDna) = _createFighterBase(dna, fighterType); fighters[tokenId].element = element; fighters[tokenId].weight = weight; fighters[tokenId].physicalAttributes = _aiArenaHelperInstance.createPhysicalAttributes( newDna, generation[fighterType], fighters[tokenId].iconsType, fighters[tokenId].dendroidBool ); _tokenURIs[tokenId] = ""; } }

Assessed type

Error

#0 - c4-pre-sort

2024-02-22T02:31:16Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-22T02:31:22Z

raymondfam marked the issue as duplicate of #306

#2 - c4-judge

2024-03-05T04:36:24Z

HickupHH3 marked the issue as satisfactory

#3 - 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/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L110 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L470

Vulnerability details

Impact

Number of elements is not set for fighters of generation greater than zero which leads to failure in creating new fighters.

Proof of Concept

Consider the following flow 1.See claimFighters function which is as below

function claimFighters( uint8[2] calldata numToMint, bytes calldata signature, string[] calldata modelHashes, string[] calldata modelTypes ) external { bytes32 msgHash = bytes32(keccak256(abi.encode( msg.sender, numToMint[0], numToMint[1], nftsClaimed[msg.sender][0], nftsClaimed[msg.sender][1] ))); require(Verification.verify(msgHash, signature, _delegatedAddress)); uint16 totalToMint = uint16(numToMint[0] + numToMint[1]); require(modelHashes.length == totalToMint && modelTypes.length == totalToMint); nftsClaimed[msg.sender][0] += numToMint[0]; nftsClaimed[msg.sender][1] += numToMint[1]; for (uint16 i = 0; i < totalToMint; i++) { _createNewFighter( msg.sender, uint256(keccak256(abi.encode(msg.sender, fighters.length))), modelHashes[i], modelTypes[i], i < numToMint[0] ? 0 : 1, 0, [uint256(100), uint256(100)] ); } }

From above we can clearly see that it makes call to the _createNewFighter which takes [uint256(100), uint256(100)] as the last input value Now _createNewFighter function is as follows

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

Now the following condition is fulfilled

if (customAttributes[0] == 100) { (element, weight, newDna) = _createFighterBase(dna, fighterType); }

So _createFighterBase function is called which is as follows

function _createFighterBase( uint256 dna, uint8 fighterType ) private view returns (uint256, uint256, uint256) { uint256 element = dna % numElements[generation[fighterType]]; uint256 weight = dna % 31 + 65; uint256 newDna = fighterType == 0 ? dna : uint256(fighterType); return (element, weight, newDna); }

Now main issue arises in the following line

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

In the whole codebase numElements is set only for the value zero i.e in the constructor

constructor(address ownerAddress, address delegatedAddress, address treasuryAddress_) ERC721("AI Arena Fighter", "FTR") { _ownerAddress = ownerAddress; _delegatedAddress = delegatedAddress; treasuryAddress = treasuryAddress_; @==> numElements[0] = 3; }

Now in the following line uint256 element = dna % numElements[generation[fighterType]] , generation[fighterType] is initially set as 0 for both types of fighter but as can be seen in the following line generation of a fighter type can be increased

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

So now if gernation[fighterType] = 1 then in following line will change to uint256 element = dna % numElements[generation[fighterType]] uint256 element = dna % numElements[1] and as numElements is set only for 0 generation then dna % numElements[1] wojuld revert as numElements[1] = 0 Hence leading to failure in creating new fighters.

Tools Used

Manual review

Add the following in the below function

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

Assessed type

Error

#0 - c4-pre-sort

2024-02-22T19:04:05Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-22T19:04:18Z

raymondfam marked the issue as duplicate of #45

#2 - c4-judge

2024-03-08T03:18:28Z

HickupHH3 marked the issue as partial-75

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