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
Rank: 119/283
Findings: 5
Award: $24.20
π Selected for report: 0
π Solo Findings: 0
π Selected for report: CodeWasp
Also found by: 0x13, 0xAlix2, 0xAsen, 0xCiphky, 0xE1, 0xLogos, 0xaghas, 0xlemon, 0xlyov, 0xvj, ADM, Aamir, BARW, Bauchibred, DMoore, DanielArmstrong, Draiakoo, Fulum, GhK3Ndf, Josh4324, Kalogerone, KmanOfficial, Krace, KupiaSec, Limbooo, MidgarAudits, MrPotatoMagic, PedroZurdo, Pelz, Tendency, _eperezok, adam-idarrha, al88nsk, alexxander, alexzoid, aslanbek, blutorque, btk, c0pp3rscr3w3r, cartlex_, cats, d3e4, deadrxsezzz, denzi_, devblixt, dimulski, erosjohn, evmboi32, fnanni, grearlake, hulkvision, immeas, israeladelaja, jaydhales, jesjupyter, jnforja, juancito, klau5, korok, ktg, ladboy233, matejdb, merlinboii, novamanbg, nuthan2x, oualidpro, peanuts, petro_1912, pkqs90, pynschon, radev_sw, rouhsamad, sashik_eth, shaka, sobieski, soliditywala, stackachu, tallo, thank_you, ubl4nk, vnavascues, web3pwn, xchen1130, zhaojohnson
0.1044 USDC - $0.10
FighterFarm
tokens must not be transferred, while staked. To enforce this the protocol has implemented checks on FighterFarm::safeTransferFrom
before allowing transfer.
function safeTransferFrom( address from, address to, uint256 tokenId ) public override(ERC721, IERC721) { require(_ableToTransfer(tokenId, to)); _safeTransfer(from, to, tokenId, ""); }
function _ableToTransfer(uint256 tokenId, address to) private view returns(bool) { return ( _isApprovedOrOwner(msg.sender, tokenId) && balanceOf(to) < MAX_FIGHTERS_ALLOWED && !fighterStaked[tokenId] ); }
However since FighterFarm
is an ERC721 contract witch also allows you transfer tokens using 721::safeTransferFrom
with the data argument which isn't overridden by FighterFarm
and isn't protected by any checks.
function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory data ) public virtual override { require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved"); _safeTransfer(from, to, tokenId, data); }
function TransferringFighterWhileStaked() public { _mintFromMergingPool(_ownerAddress); _fighterFarmContract.addStaker(_ownerAddress); _fighterFarmContract.updateFighterStaking(0, true); // Token is now staked assertEq(_fighterFarmContract.fighterStaked(0), true); _fighterFarmContract.safeTransferFrom( _ownerAddress, _DELEGATED_ADDRESS, 0, "" // data param ); // Token has been transfer to new owner assertEq(_fighterFarmContract.ownerOf(0) == _DELEGATED_ADDRESS, true); }
Foundry
721::safeTransferFrom
with the data argument and implement checks to verify if the NFT isn't staked, then
allow transfer.ERC721
#0 - c4-pre-sort
2024-02-23T04:40:17Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-23T04:40:25Z
raymondfam marked the issue as duplicate of #54
#2 - c4-pre-sort
2024-02-23T04:47:23Z
raymondfam marked the issue as duplicate of #739
#3 - c4-pre-sort
2024-02-23T04:54:13Z
raymondfam marked the issue as sufficient quality report
#4 - c4-judge
2024-03-11T02:09:26Z
HickupHH3 changed the severity to 3 (High Risk)
#5 - c4-judge
2024-03-11T02:41:07Z
HickupHH3 marked the issue as satisfactory
π Selected for report: CodeWasp
Also found by: 0x13, 0xAlix2, 0xAsen, 0xCiphky, 0xE1, 0xLogos, 0xaghas, 0xlemon, 0xlyov, 0xvj, ADM, Aamir, BARW, Bauchibred, DMoore, DanielArmstrong, Draiakoo, Fulum, GhK3Ndf, Josh4324, Kalogerone, KmanOfficial, Krace, KupiaSec, Limbooo, MidgarAudits, MrPotatoMagic, PedroZurdo, Pelz, Tendency, _eperezok, adam-idarrha, al88nsk, alexxander, alexzoid, aslanbek, blutorque, btk, c0pp3rscr3w3r, cartlex_, cats, d3e4, deadrxsezzz, denzi_, devblixt, dimulski, erosjohn, evmboi32, fnanni, grearlake, hulkvision, immeas, israeladelaja, jaydhales, jesjupyter, jnforja, juancito, klau5, korok, ktg, ladboy233, matejdb, merlinboii, novamanbg, nuthan2x, oualidpro, peanuts, petro_1912, pkqs90, pynschon, radev_sw, rouhsamad, sashik_eth, shaka, sobieski, soliditywala, stackachu, tallo, thank_you, ubl4nk, vnavascues, web3pwn, xchen1130, zhaojohnson
0.1044 USDC - $0.10
FighterFarm
states that a user can only own 10 NTF fighters MAX_FIGHTERS_ALLOWED = 10
. To enforce this the protocol has implemented checks on FighterFarm::safeTransferFrom
before allowing transfer, and when minting as well, so users cannot mint more than the max allowed.
function safeTransferFrom( address from, address to, uint256 tokenId ) public override(ERC721, IERC721) { require(_ableToTransfer(tokenId, to)); _safeTransfer(from, to, tokenId, ""); }
function _ableToTransfer(uint256 tokenId, address to) private view returns(bool) { return ( _isApprovedOrOwner(msg.sender, tokenId) && balanceOf(to) < MAX_FIGHTERS_ALLOWED && !fighterStaked[tokenId] ); }
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);
However since FighterFarm
is an ERC721 contract witch also allows you transfer tokens using ERC721::safeTransferFrom
with the data argument which isn't overridden by FighterFarm
and isn't protected by any checks.
function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory data ) public virtual override { require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved"); _safeTransfer(from, to, tokenId, data); }
function safeTransferFrom(address from,address to,uint256 tokenId, bytes memory data)
since there are no checks.The NFT balance of the user will be more than MAX_FIGHTERS_ALLOWED
function UserCanOwnMoreThanMaxAllowed() public { uint8 amount = 10; uint256 maxFightersAllowed = 10; mintAmountFighterTokens(address(this), amount); assertEq(_fighterFarmContract.balanceOf(address(this)), amount); vm.prank(_other); mintAmountFighterTokens(_other, amount); assertEq(_fighterFarmContract.balanceOf(_other), amount); for (uint256 i = 10; i < 20; i++) { vm.prank(_other); _fighterFarmContract.safeTransferFrom( _other, address(this), i, "" // data param ); } assertEq(_fighterFarmContract.balanceOf(address(this)), 20); assertTrue( _fighterFarmContract.balanceOf(address(this)) > maxFightersAllowed ); }
Foundry
ERC721::safeTransferFrom
with the data argument and implement checks to verify if the NFT isn't staked, then allow transfer.Other
#0 - c4-pre-sort
2024-02-23T17:47:28Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-23T17:47:45Z
raymondfam marked the issue as sufficient quality report
#2 - c4-pre-sort
2024-02-23T17:47:53Z
raymondfam marked the issue as duplicate of #739
#3 - c4-judge
2024-03-11T02:09:27Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-11T02:58:36Z
HickupHH3 marked the issue as satisfactory
π Selected for report: Aamir
Also found by: 0rpse, 0x11singh99, 0x13, 0xAlix2, 0xAsen, 0xBinChook, 0xCiphky, 0xE1, 0xKowalski, 0xLogos, 0xWallSecurity, 0xaghas, 0xbranded, 0xlemon, 0xlyov, 0xpoor4ever, 0xprinc, 0xvj, ADM, Aymen0909, BARW, Bauchibred, Breeje, CodeWasp, DMoore, DeFiHackLabs, Draiakoo, Fulum, GhK3Ndf, Greed, Jorgect, Josh4324, Kalogerone, KmanOfficial, Krace, Limbooo, McToady, MidgarAudits, MrPotatoMagic, PedroZurdo, Pelz, Ryonen, SovaSlava, SpicyMeatball, Tendency, Timenov, ZanyBonzy, _eperezok, al88nsk, alexxander, alexzoid, aslanbek, blutorque, btk, cartlex_, cats, csanuragjain, deadrxsezzz, denzi_, devblixt, dimulski, djxploit, erosjohn, evmboi32, fnanni, grearlake, haxatron, hulkvision, immeas, israeladelaja, jaydhales, jesjupyter, jnforja, josephdara, juancito, kiqo, klau5, korok, krikolkk, ktg, kutugu, ladboy233, lil_eth, m4ttm, matejdb, merlinboii, n0kto, ni8mare, novamanbg, nuthan2x, oualidpro, pa6kuda, peter, petro_1912, pkqs90, pynschon, sandy, sashik_eth, shaflow2, shaka, sobieski, soliditywala, solmaxis69, stackachu, tallo, thank_you, tpiliposian, ubl4nk, visualbits, vnavascues, web3pwn, xchen1130, zhaojohnson
0.0037 USDC - $0.00
Game items have of the attribute of transferability, if it's set to true,
users can transfer them otherwise they cannot. To enforce this the protocol has
implemented checks on safeTransferFrom
before allowing transfer.
File: src/GameItems.sol function safeTransferFrom( address from, address to, uint256 tokenId, uint256 amount, bytes memory data ) public override(ERC1155) { require(allGameItemAttributes[tokenId].transferable); super.safeTransferFrom(from, to, tokenId, amount, data); }
However GameItems
is an ERC1155 contract witch also allows you transfer tokens using ERC1155::safeBatchTransferFrom
which isn't overridden by GameItems
and isn't protected by any checks.
function safeBatchTransferFrom( address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data ) public virtual override { require( from == _msgSender() || isApprovedForAll(from, _msgSender()), "ERC1155: caller is not token owner nor approved" ); _safeBatchTransferFrom(from, to, ids, amounts, data); }
function TransferNonTransferableUsingBatchTransfer() public { _fundUserWith4kNeuronByTreasury(_ownerAddress); _gameItemsContract.createGameItem( "Battery", "https://ipfs.io/ipfs/", true, false, // transferable 10_000, 1 * 10 ** 18, 10 ); _gameItemsContract.mint(0, 1); uint256[f] memory ids = new uint256[](1); uint256[] memory amounts = new uint256[](1); amounts[0] = 1; _gameItemsContract.safeBatchTransferFrom( _ownerAddress, address(_receiver), ids, amounts, "" ); assertEq(_gameItemsContract.balanceOf(address(_receiver), 0), 1); assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 0); }
Foundry
ERC1155::safeBatchTransferFrom
and implement checks to verify if the game item is transferable, then
allow transfer.Other
#0 - c4-pre-sort
2024-02-22T03:53:31Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T03:53:39Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:27:59Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:47:39Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-05T04:53:22Z
HickupHH3 marked the issue as satisfactory
π Selected for report: klau5
Also found by: 0xAleko, 0xAlix2, 0xAsen, 0xCiphky, 0xKowalski, 0xlemon, 0xvj, 14si2o_Flint, Aamir, AlexCzm, Aymen0909, BARW, Blank_Space, DanielArmstrong, Davide, Draiakoo, Giorgio, McToady, MrPotatoMagic, PoeAudits, Ryonen, Silvermist, SpicyMeatball, Tychai0s, VAD37, Varun_05, alexxander, alexzoid, aslanbek, blutorque, btk, cats, d3e4, denzi_, evmboi32, fnanni, givn, grearlake, haxatron, jesjupyter, juancito, ke1caM, ktg, lanrebayode77, linmiaomiao, matejdb, merlinboii, n0kto, novamanbg, nuthan2x, petro_1912, pynschon, radin100, sashik_eth, shaka, sl1, soliditywala, solmaxis69, t0x1c, ubl4nk, vnavascues, xchen1130, yotov721, zhaojohnson
1.1225 USDC - $1.12
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L509-L510 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L377-L390 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/AiArenaHelper.sol#L83-L95
FighterFarm::reRoll
allows users to generate new random traits for their fighter, but when rerolling the function uses the previous dendroidBool
which is derived from bool dendroidBool = fighterType == 1;
but this only set when the user first mints and not when rerolling. In the case of when a user first mints a Fighter NFT with (fighterType
1), then decide to reroll the fighter using (fighterType
0) the NFT will not get random traits.
bool dendroidBool = fighterType == 1;
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 // Previously stored dendroidBool. ); _tokenURIs[tokenId] = ""; }
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); }
fighterType
1) to (fighterType
0).function rerollDoesNotGenerateRandomAttributes() public { _fundUserWith4kNeuronByTreasury(address(this)); mintAmountFighterTypeOneTokens(address(this), 1); assertEq(_fighterFarmContract.balanceOf(address(this)), 1); (, uint256[6] memory attributes, , , , , ) = _fighterFarmContract .getAllFighterInfo( 0 // tokenId ); for (uint256 i; i < attributes.length; i++) { assertEq(99, attributes[i]); } _neuronContract.addSpender(address(_fighterFarmContract)); _fighterFarmContract.reRoll( 0, // tokenId 0 // fighterType ); for (uint256 i; i < attributes.length; i++) { assertEq(99, attributes[i]); // physicalAttributes did not change } }
Foundry
dendroidBool
when rerolling, so it would take into consideration the updated fighterType
Other
#0 - c4-pre-sort
2024-02-22T01:28:47Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T01:28:53Z
raymondfam marked the issue as duplicate of #306
#2 - c4-judge
2024-03-05T04:32:22Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-19T09:05:07Z
HickupHH3 changed the severity to 3 (High Risk)
π Selected for report: haxatron
Also found by: 0xAlix2, 0xCiphky, 0xStriker, 0xaghas, 0xbranded, 0xlamide, 0xmystery, 0xvj, 14si2o_Flint, Aamir, AgileJune, Aymen0909, DanielArmstrong, DarkTower, Draiakoo, EagleSecurity, Giorgio, Krace, KupiaSec, MidgarAudits, MrPotatoMagic, PoeAudits, Ryonen, SpicyMeatball, Topmark, Tychai0s, VAD37, Varun_05, VrONTg, WoolCentaur, _eperezok, alexxander, alexzoid, aslanbek, blutorque, btk, cartlex_, d3e4, devblixt, evmboi32, fnanni, immeas, jesjupyter, juancito, ke1caM, klau5, ktg, lil_eth, merlinboii, nuthan2x, peter, petro_1912, pkqs90, pynschon, radin100, sandy, sashik_eth, shaka, sl1, soliditywala, t0x1c, ubl4nk, visualbits, vnavascues
1.1225 USDC - $1.12
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L104-L111 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L499-L501 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L462-L474
To create the element for the fighter _createFighterBase
function uses dna % numElements[generation[fighterType]]
. The numElements
is only set in the constructor numElements[0] = 3;
and there is no setter function to modify it. When the fighterType
generation is incremented the numElements
for that generation will be 0, which leads to modulo by 0. Consequently, every time a user tries to mint a fighter NFT with (customAttributes[0] == 100)
the modulo by zero will cause the transaction to revert.
constructor(address ownerAddress, address delegatedAddress, address treasuryAddress_) ERC721("AI Arena Fighter", "FTR") { _ownerAddress = ownerAddress; _delegatedAddress = delegatedAddress; treasuryAddress = treasuryAddress_; numElements[0] = 3; }
if (customAttributes[0] == 100) { (element, weight, newDna) = _createFighterBase(dna, fighterType); }
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); }
FighterFarm
will be DoS'd and users cannot mint any fighter NFTs.function ClaimFightersFailModByZero() public { uint8[2] memory numToMint = [1, 0]; bytes memory claimSignature = abi.encodePacked( hex"407c44926b6805cf9755a88022102a9cb21cde80a210bc3ad1db2880f6ea16fa4e1363e7817d5d87e4e64ba29d59aedfb64524620e2180f41ff82ca9edf942d01c" ); string[] memory claimModelHashes = new string[](1); claimModelHashes[ 0 ] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; string[] memory claimModelTypes = new string[](1); claimModelTypes[0] = "original"; //Increment generation for fighter type 0 _fighterFarmContract.incrementGeneration(0); // Reverts because of modulo by 0 vm.expectRevert(); _fighterFarmContract.claimFighters( numToMint, claimSignature, claimModelHashes, claimModelTypes ); }
Foundry
numElements
to a non-zero value when incrementing fighter generation, to prevent modulo by 0.DoS
#0 - c4-pre-sort
2024-02-22T18:34:38Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T18:34:47Z
raymondfam marked the issue as duplicate of #45
#2 - c4-judge
2024-03-07T06:59:57Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-08T03:14:58Z
HickupHH3 removed the grade
#4 - c4-judge
2024-03-08T03:15:01Z
HickupHH3 marked the issue as satisfactory
π Selected for report: nuthan2x
Also found by: 0xE1, 0xblackskull, 0xgrbr, 0xvj, Greed, McToady, MidgarAudits, PetarTolev, Sabit, SovaSlava, SpicyMeatball, Timenov, Tychai0s, _eperezok, alexxander, btk, c0pp3rscr3w3r, favelanky, jesjupyter, josephdara, juancito, klau5, kutugu, lil_eth, merlinboii, pynschon, sandy, shaflow2, zaevlad
21.8606 USDC - $21.86
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/Neuron.sol#L93-L96 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/Neuron.sol#L101-L104 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/Neuron.sol#L109-L112
Neuron
improperly uses AccessControl::_setupRole
to grant roles, _setupRole
should only be called from the constructor when setting up the initial roles for the system. When a role is granted to an address it can either be revoke by an address that has the adminRole
for that role or if the user renounces the role, but since no address has been assigned the adminRole
for any of the roles (MINTER_ROLE
, SPENDER_ROLE
, STAKER_ROLE
), there is no way of revoking granted roles.
function addMinter(address newMinterAddress) external { require(msg.sender == _ownerAddress); _setupRole(MINTER_ROLE, newMinterAddress); }
function addStaker(address newStakerAddress) external { require(msg.sender == _ownerAddress); _setupRole(STAKER_ROLE, newStakerAddress); }
function addSpender(address newSpenderAddress) external { require(msg.sender == _ownerAddress); _setupRole(SPENDER_ROLE, newSpenderAddress); }
* [WARNING] * ==== * This function should only be called from the constructor when setting * up the initial roles for the system. * * Using this function in any other way is effectively circumventing the admin * system imposed by {AccessControl}. * ==== * * NOTE: This function is deprecated in favor of {_grantRole}. */ function _setupRole(bytes32 role, address account) internal virtual { _grantRole(role, account); }
function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { _revokeRole(role, account); }
MINTER_ROLE
, SPENDER_ROLE
,STAKER_ROLE
) turn out to be malicious or compromised, there's no way of revoking their access._setupRole
in the constructor to set up adminRoles
, then grant roles using grantRole
which performs the required checks.Access Control
#0 - c4-pre-sort
2024-02-22T05:05:28Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T05:05:37Z
raymondfam marked the issue as duplicate of #20
#2 - c4-judge
2024-03-05T07:31:06Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-05T07:31:11Z
HickupHH3 marked the issue as partial-75
#4 - c4-judge
2024-03-05T07:33:46Z
HickupHH3 marked the issue as full credit
#5 - c4-judge
2024-03-05T07:37:00Z
HickupHH3 marked the issue as partial-75
#6 - HickupHH3
2024-03-05T07:37:35Z
partial credit because it while there's some mention on no address having the adminRole
, it's not mentioned as a mitigation