AI Arena - favelanky'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: 108/283

Findings: 3

Award: $42.82

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Awards

29.1474 USDC - $29.15

Labels

bug
2 (Med Risk)
insufficient quality report
satisfactory
:robot:_48_group
duplicate-1507

External Links

Lines of code

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

Vulnerability details

Impact

The contracts of the Ai Arena ecosystem are designed with a way to change addresses. In case of some emergency, it should be possible to revoke given roles.

Proof of Concept

Neuron.sol contract is a token contract which can give different privileges like MINTER, SPENDER, STAKER roles to other contracts. The AccessControl contract is used to implement role mechanics. However, it requires to set an DEFAULT_ADMIN_ROLE for admin. The contract is not doing this, what leads to impossibility to revoke given roles.

POC (add it to Neuron.t.sol contract):

    function testRevokeRole() public {
        address minter = vm.addr(3);
        _neuronContract.addMinter(minter);
        // this will revert
        _neuronContract.revokeRole(_neuronContract.MINTER_ROLE(), minter);
    }
Running 1 test for test/Neuron.t.sol:NeuronTest
[FAIL. Reason: revert: AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000] testRevokeRole() 

Tools Used

Manual Analysis

Consider adding DEFAULT_ADMIN_ROLE to the owner of the contract.

    constructor(address ownerAddress, address treasuryAddress_, address contributorAddress) ERC20("Neuron", "NRN") {
        _ownerAddress = ownerAddress;
        treasuryAddress = treasuryAddress_;
        isAdmin[_ownerAddress] = true;
        _mint(treasuryAddress, INITIAL_TREASURY_MINT);
        _mint(contributorAddress, INITIAL_CONTRIBUTOR_MINT);
+       _grantRole(DEFAULT_ADMIN_ROLE, _ownerAddress);
    }
	function transferOwnership(address newOwnerAddress) external {
		require(msg.sender == _ownerAddress);
		_ownerAddress = newOwnerAddress;
		_grantRole(DEFAULT_ADMIN_ROLE, newOwnerAddress);
		revokeRole(DEFAULT_ADMIN_ROLE, _ownerAddress);
	}

Assessed type

Access Control

#0 - c4-pre-sort

2024-02-22T05:14:42Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2024-02-22T05:14:49Z

raymondfam marked the issue as duplicate of #20

#2 - c4-judge

2024-03-05T10:01:27Z

HickupHH3 marked the issue as satisfactory

Lines of code

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

Vulnerability details

Impact

The user will know the attributes of the NFT before minting and can exploit it to get the wanted ones. He can simply create infinite number of EOAs to bruteforce attributes locally.

Proof of Concept

The _createFighterBase is used to determine some parameters of the NFT. Parameters are determined using pseudo-randomness which allows to any user to know what they will mint. The dna parameter is equal to to uint256(keccak256(abi.encode(msg.sender, fighters.length))) for creating and uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId]))) for rerolling NFT.

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

Tools Used

Manual review

Do not rely on pseudo-randomness to determine NFT attributes. Consider using a decentralized oracle for the generation of random numbers, such asΒ Chainlinks VRF.

Assessed type

Other

#0 - c4-pre-sort

2024-02-24T02:01:38Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-24T02:01:47Z

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

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

HickupHH3 marked the issue as duplicate of #376

Awards

13.6293 USDC - $13.63

Labels

bug
G (Gas Optimization)
grade-b
sufficient quality report
G-09

External Links

[G-1] Use Merkle Tree

Use Merkle Tree for airdropping tokens. This will save a decent amount of gas and will require only storing the root inside the contract.

    function setupAirdrop(address[] calldata recipients, uint256[] calldata amounts) external {
        require(isAdmin[msg.sender]);
        require(recipients.length == amounts.length);
        uint256 recipientsLength = recipients.length;
        for (uint32 i = 0; i < recipientsLength; i++) {
            _approve(treasuryAddress, recipients[i], amounts[i]);
        }
    }

    /// @notice Claims the specified amount of tokens from the treasury address to the caller's address.
    /// @param amount The amount to be claimed
    function claim(uint256 amount) external {
        require(allowance(treasuryAddress, msg.sender) >= amount, "ERC20: claim amount exceeds allowance");
        transferFrom(treasuryAddress, msg.sender, amount);
        emit TokensClaimed(msg.sender, amount);
    }

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L127-L145

[G-2] Sum at the end

Updating global variables inside a loop is quite expensive. Here it is possible to add the value once.

    function claimNRN() external {
        require(numRoundsClaimed[msg.sender] < roundId, "Already claimed NRNs for this period");
        uint256 claimableNRN = 0;
        uint256 nrnDistribution;
        uint32 lowerBound = numRoundsClaimed[msg.sender];
        for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) {
            nrnDistribution = getNrnDistribution(currentRound);
            claimableNRN += (accumulatedPointsPerAddress[msg.sender][currentRound] * nrnDistribution)
                / totalAccumulatedPoints[currentRound];
            // @gas we can sum up at the end
-           numRoundsClaimed[msg.sender] += 1;
        }
+       numRoundsClaimed[msg.sender] += roundId - lowerBound;
        if (claimableNRN > 0) {
            amountClaimed[msg.sender] += claimableNRN;
            // @high time bomb probably, we should transfer from the treasury
            _neuronInstance.mint(msg.sender, claimableNRN);
            emit Claimed(msg.sender, claimableNRN);
        }
    }

https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L294-L311

[G-3] Address saved twice

The contract stores the address and instance, which is redundant. Simply use StakeAtRisk(_stakeAtRiskAddress) where needed. The _stakeAtRiskInstance can be removed.

contract RankedBattle {
	...
	address _stakeAtRiskAddress;
	...
	StakeAtRisk _stakeAtRiskInstance;
}

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

#0 - raymondfam

2024-02-25T21:08:14Z

3G

#1 - c4-pre-sort

2024-02-25T21:08:18Z

raymondfam marked the issue as sufficient quality report

#2 - c4-judge

2024-03-19T07:54:38Z

HickupHH3 marked the issue as grade-b

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