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: 229/283
Findings: 4
Award: $1.26
π 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
The vulnerability allows users to circumvent the MAX_FIGHTERS_ALLOWED
check, enabling them to possess an unlimited number of fighters. This disrupts the balance in the game logic and violates the protocol's rules.
The _ableToTransfer
function, responsible for managing users token counts and verifying transfer approval, is absent in the safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
function. Consequently, when users perform token transfers using this function, these essential checks are omitted, permitting users to transfer any number of tokens without restriction.
function testPOC() public { uint8[2] memory numToMint = [9, 0]; bytes memory claimSignature = abi.encodePacked( hex"407c44926b6805cf9755a88022102a9cb21cde80a210bc3ad1db2880f6ea16fa4e1363e7817d5d87e4e64ba29d59aedfb64524620e2180f41ff82ca9edf942d01c" ); string[] memory claimModelHashes = new string[](9); for (uint16 i = 0; i < 9; i++) { claimModelHashes[i] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; } string[] memory claimModelTypes = new string[](9); for (uint16 i = 0; i < 9; i++) { claimModelTypes[i] = "original"; } address alice = makeAddr("alice"); address bob = makeAddr("bob"); vm.startPrank(alice); _fighterFarmContract.claimFighters(numToMint, claimSignature, claimModelHashes, claimModelTypes); assertEq(_fighterFarmContract.balanceOf(alice), 9); assertEq(_fighterFarmContract.totalSupply(), 9); vm.stopPrank(); vm.startPrank(bob); uint8[2] memory numToMint2 = [1, 0]; string[] memory claimModelHashes2 = new string[](1); claimModelHashes[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; string[] memory claimModelTypes2 = new string[](1); claimModelTypes[0] = "original"; _fighterFarmContract.claimFighters(numToMint2, claimSignature, claimModelHashes2, claimModelTypes2); assertEq(_fighterFarmContract.balanceOf(bob), 1); assertEq(_fighterFarmContract.totalSupply(), 10); bytes memory data = hex"00FF"; _fighterFarmContract.safeTransferFrom(bob, alice, 9, data); assertEq(_fighterFarmContract.balanceOf(alice), 10); assertEq(_fighterFarmContract.balanceOf(bob), 0); vm.stopPrank(); }
Important: For the sake of simplification in this proof of concept, the require(Verification.verify(msgHash, signature, _delegatedAddress));
line has been commented out to bypass signature verification and creation processes. However, this does not impact the demonstration of the vulnerability.
Forge
To address this issue, implement the _ableToTransfer
check within the safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
function.
Token-Transfer
#0 - c4-pre-sort
2024-02-23T05:41:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T05:41:36Z
raymondfam marked the issue as duplicate of #739
#2 - c4-judge
2024-03-11T02:50:24Z
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
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/GameItems.sol#L289-L303 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/141c947921cc5d23ee1d247c691a8b85cabbbd5d/contracts/token/ERC1155/ERC1155.sol#L236-L261
Users can transfer non-transferable tokens using the safeBatchTransferFrom
function, violating protocol logic and potentially disrupting game mechanics.
When a token is created with the transferability attribute set to false, it should not be transferable. However, the safeBatchTransferFrom
function does not include a check for transferability. Therefore, any non-transferable token can be transferred using this function, bypassing the intended restrictions.
function setUp() public { _ownerAddress = address(this); _treasuryAddress = vm.addr(1); _neuronContributorAddress = vm.addr(2); _gameItemsContract = new GameItems(_ownerAddress, _treasuryAddress); _neuronContract = new Neuron(_ownerAddress, _treasuryAddress, _neuronContributorAddress); _neuronContract.addSpender(address(_gameItemsContract)); _gameItemsContract.instantiateNeuronContract(address(_neuronContract)); _gameItemsContract.createGameItem("Battery", "https://ipfs.io/ipfs/", true, false, 10_000, 1 * 10 ** 18, 10); } function testPOC() public { // game item created as non transferable, but with safeBatchTransferFrom we can still transfer _fundUserWith4kNeuronByTreasury(_ownerAddress); _gameItemsContract.mint(0, 1); uint256[] memory ids = new uint256[](1); ids[0] = 0; uint256[] memory values = new uint256[](1); values[0] = 1; _gameItemsContract.safeBatchTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, ids, values, ""); assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, 0), 1); assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 0); }
Forge
To address this issue, override the safeBatchTransferFrom
function and incorporate a check for transferability. Ensure that the function verifies the transferability attribute of each token being transferred, preventing the transfer of non-transferable tokens.
Token-Transfer
#0 - c4-pre-sort
2024-02-22T04:24:11Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T04:24:17Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:29:24Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:56:53Z
HickupHH3 marked the issue as satisfactory
π 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#L110 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L458-L474
The issue could result in a critical vulnerability leading to the protocol becoming blocked. This occurs because the numElements mapping
, which assigns elements by generation, is only initialized for generation 0 in the constructor
. If the generation is increased beyond 0, numElements
remains uninitialized for the new generations. Consequently, when calculating the base attributes for a fighter, the expression numElements[generation[fighterType]]
will always yield zero for newer generations. This leads to a panic error due to attempting a modulo operation with zero, as stated in the Solidity documentation. As a result, contract execution halts, rendering the protocol non-functional.
The vulnerability can be demonstrated by incrementing the generation beyond 0 and attempting to mint new fighters. During the fighter creation process, the contract will attempt to access numElements[generation[fighterType]]
to determine base attributes. For new generations, where numElements
is uninitialized, this will always result in zero. Consequently, when the contract tries to perform a modulo operation with zero, a panic error occurs, causing contract execution to halt and effectively blocking the protocol.
function testPOC() public { _fighterFarmContract.incrementGeneration(1); assertEq(_fighterFarmContract.generation(1), 1); assertEq(_fighterFarmContract.generation(0), 0); assertEq(_fighterFarmContract.numElements(1), 0); //numElements is zero console.log(_fighterFarmContract.numElements(1)); ///console.log output is also zero }
Forge
To address this vulnerability and prevent protocol blockage, it is imperative to ensure that the numElements
mapping is properly initialized for all generations, not just generation 0. Recommended to create function for admin, to initialize it for each generation.
Math
#0 - c4-pre-sort
2024-02-22T18:47:09Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T18:47:17Z
raymondfam marked the issue as duplicate of #45
#2 - c4-judge
2024-03-07T07:04:29Z
HickupHH3 marked the issue as satisfactory
π Selected for report: klau5
Also found by: 0rpse, 0xBinChook, 0xDetermination, 0xGreyWolf, 0xLogos, 0xWallSecurity, 0xaghas, 0xgrbr, 0xkaju, 0xlyov, AlexCzm, BARW, Blank_Space, BoRonGod, Daniel526, DanielArmstrong, Draiakoo, FloatingPragma, Giorgio, Greed, Jorgect, Matue, McToady, MidgarAudits, Nyxaris, PUSH0, PedroZurdo, Pelz, PoeAudits, Silvermist, SpicyMeatball, Tekken, Tricko, Tumelo_Crypto, VAD37, WoolCentaur, Zac, alexzoid, andywer, aslanbek, bgsmallerbear, cats, d3e4, desaperh, dimulski, dutra, erosjohn, evmboi32, favelanky, fnanni, forkforkdog, gesha17, givn, grearlake, haxatron, honey-k12, iamandreiski, immeas, juancito, kaveyjoe, ke1caM, kiqo, klau5, korok, lil_eth, lsaudit, n0kto, ni8mare, niser93, pa6kuda, peanuts, peter, shaka, sl1, soliditywala, solmaxis69, t0x1c, tallo, thank_you, tpiliposian, visualbits, vnavascues, web3pwn, yotov721
0.0352 USDC - $0.04
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L185-L222 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L367-L391
The attacker can manipulate the generation process of fighters by controlling the input parameters msg.sender
and fighters.length
, potentially resulting in the creation of fighters with maximum attributes, undermining the randomness and fairness of the game.
The attacker monitors the length of the fighters array. When the array length is favorable (e.g., 33), the attacker triggers the claimFighters
function, providing attacker-controlled parameters. By manipulating msg.sender
and fighters.length
, the attacker ensures that the base attributes of the claimed fighters are maximized, bypassing any randomness. The same issue exists in reRoll
function.
This leads to an unfair advantage for the attacker, as they can consistently create fighters with optimal attributes.
function testPOC() public { address alice = makeAddr("alice"); uint256 weight; uint256 fightersLength; for(uint256 i =0; i< 1000; i++) { uint256 dna = uint256(keccak256(abi.encode(alice, i))); weight = dna % 31 + 65; if (weight == 95) { fightersLength = i; break; } } console.log(weight); // weight is 95 console.log(fightersLength); // length is 33 }
In this scenario, the attacker with the address "alice" monitors the length of the fighters array. When it reaches 33, the attacker claims a fighter, ensuring it obtains the maximum weight of 95.
Forge
To mitigate these issue, consider implementing off-chain trusted randomness sources like Chainlink VRF (Verifiable Random Function) to introduce genuine randomness into the generation of fighter attributes.
Other
#0 - c4-pre-sort
2024-02-24T01:51:58Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:52:29Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:52:13Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-15T02:10:55Z
HickupHH3 changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-03-22T04:21:41Z
HickupHH3 marked the issue as duplicate of #376