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: 190/283
Findings: 7
Award: $3.88
π 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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L346 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L363 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L539
Following function ensures that a user can not transfer the staked fighter.
function _ableToTransfer(uint256 tokenId, address to) private view returns(bool) { return ( _isApprovedOrOwner(msg.sender, tokenId) && balanceOf(to) < MAX_FIGHTERS_ALLOWED && !fighterStaked[tokenId] ); }
The above function is used in FighterFarm.transferFrom() and FighterFarm.safeTransferFrom(address from,address to, uint256 tokenId) to restrict the transferability of a staked fighter.
Since FighterFarm is ERC721, so it contains a function
safeTransferFrom(address from,address to,uint256 tokenId,bytes memory data)
with an extra param of data
.
The transferability check is not enforced for this function hence a user can use this function in order to transfer the staked Fighter.
Add this as first test in FighterFarm.t.sol;
function test_stakedFighterTransferability() 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"; // Right sender of signature should be able to claim fighter _fighterFarmContract.claimFighters( numToMint, claimSignature, claimModelHashes, claimModelTypes ); assertEq(_fighterFarmContract.balanceOf(_ownerAddress), 1); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); assertEq(_fighterFarmContract.totalSupply(), 1); _fighterFarmContract.addStaker(_ownerAddress); _fighterFarmContract.updateFighterStaking(0, true); assertEq(_fighterFarmContract.fighterStaked(0), true); vm.prank(_ownerAddress); vm.expectRevert(); // reverting with simple transfer _fighterFarmContract.transferFrom(address(this), address(1), 0); vm.prank(_ownerAddress); // allows transfer with this version of safeTransferFrom _fighterFarmContract.safeTransferFrom(address(this), address(1), 0,"0x00"); assertEq(_fighterFarmContract.ownerOf(0),address(1)); }
Manual review.
Also override this version of safeTranferFrom like below.
function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory data ) public override(ERC721, IERC721) { require(_ableToTransfer(tokenId, to)); _safeTransfer(from, to, tokenId, data); }
Token-Transfer
#0 - c4-pre-sort
2024-02-23T05:36:31Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T05:36:38Z
raymondfam marked the issue as duplicate of #739
#2 - c4-judge
2024-03-11T02:49:47Z
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/main/src/GameItems.sol#L301 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L291
There are two types of GameItems. Transferable and Non-transferable (also, admin can adjust transferability of any GameItem). To ensure that an item is transferable or not, following check is enforced in GameItems.safeTransferFrom()
require(allGameItemAttributes[tokenId].transferable);
Since GameItems.sol is ERC1155, a standard ERC1155 contains a function safeBatchTransferFrom() that can be used by the user to transfer the non-transferable token since the above check is not enforced in safeBatchTransferFrom()
Add this as first test in GameItems.t.sol.
function test_buypassTransferability() public { _fundUserWith4kNeuronByTreasury(address(this)); _gameItemsContract.mint(0, 10); // making the item non-transferable _gameItemsContract.adjustTransferability(0, false); uint[] memory ids = new uint[](1); uint[] memory amount = new uint[](1); ids[0] = 0; amount[0] = 10; vm.expectRevert(); _gameItemsContract.safeTransferFrom( address(this), address(this), 0, 10, "0x00" ); // bypassing non-transferability _gameItemsContract.safeBatchTransferFrom( address(this), address(1), ids, amount, "0x00" ); assertEq(_gameItemsContract.balanceOf(address(1), 0), 10); }
Manual analysis.
Also override the safeBatchTransferFrom() like below.
function safeBatchTransferFrom( address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data ) public override { for (uint i; i < ids.length; i++) { require(allGameItemAttributes[ids[i]].transferable); } super.safeBatchTransferFrom(from, to, ids, amounts, data); }
Token-Transfer
#0 - c4-pre-sort
2024-02-22T04:17:42Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T04:17:50Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:29:07Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:56:37Z
HickupHH3 marked the issue as satisfactory
π Selected for report: Abdessamed
Also found by: 0rpse, 0xAlix2, 0xAsen, 0xCiphky, 0xlemon, 0xmystery, 0xvj, ADM, Aamir, Archime, BARW, DarkTower, Draiakoo, FloatingPragma, JCN, McToady, MrPotatoMagic, OMEN, PetarTolev, Ryonen, SpicyMeatball, Tendency, VAD37, Velislav4o, VrONTg, Zac, adamn000, ahmedaghadi, alexxander, alexzoid, bhilare_, btk, cats, d3e4, denzi_, devblixt, dimulski, evmboi32, fnanni, givn, haxatron, immeas, jesjupyter, juancito, ke1caM, klau5, korok, krikolkk, matejdb, n0kto, niser93, peter, pkqs90, radin100, shaka, sl1, soliditywala, stackachu, stakog, t0x1c, vnavascues, yotov721, zhaojohnson
1.2667 USDC - $1.27
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L233
FighterFarm.redeemMintPass() takes input from user to redeem the mint pass and mint the fighter against each mint pass. However, user can mint fighters with the attributes they want due to the control of input params. That should not be the case as all users will try to mint fighters with best attribues/qualities and that will basically to some extent cancels out the rarity of fighters as fighter will be a super hero.
For example user can input such DNA that earn him a fighter with best weight
and element
.
Also user can control the fighter type he wants to have.
Manual Review
Maybe input data can be generated signed from server that will ensure fair input params and then verified on-chain like we did here in FighterFarm.claimFighters()
Other
#0 - c4-pre-sort
2024-02-22T08:16:02Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T08:16:08Z
raymondfam marked the issue as duplicate of #33
#2 - c4-pre-sort
2024-02-26T00:54:05Z
raymondfam marked the issue as duplicate of #1626
#3 - c4-judge
2024-03-06T03:36:16Z
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/main/src/FighterFarm.sol#L370
FighterFarm.reRoll() expects the user to correctly input fighter type of a Fighter NFT.
However, this can be miss-used by the malicious user to change the fighter type of the Fighter that should not be possible (sponsor also confirmed that change of fighter type is not allowed by design).
This will allow users to make their average NFT worth more by changing their NFT's fighter type.
Add this as first test in FighterFarm.t.sol that shows user can change the fighter type of the fighter NFT.
function testReroll() public { _mintFromMergingPool(_ownerAddress); // get 4k neuron from treasury _fundUserWith4kNeuronByTreasury(_ownerAddress); // after successfully minting a fighter, update the model if (_fighterFarmContract.ownerOf(0) == _ownerAddress) { uint256 neuronBalanceBeforeReRoll = _neuronContract.balanceOf( _ownerAddress ); uint8 tokenId = 0; uint8 fighterType = 0; _neuronContract.addSpender(address(_fighterFarmContract)); // first reRoll with fighter type 0 _fighterFarmContract.reRoll(tokenId, fighterType); // then reRoll with fighter type 1 _fighterFarmContract.reRoll(tokenId, uint8(1)); } }
Manual Review
1 - Create a global mapping that keeps track of tokenId -> fighterType upon minting of fighters. 2 - Fetch fighterType of tokenId from that global mapping rather than taking input from user.
Other
#0 - c4-pre-sort
2024-02-22T02:13:12Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T02:13:20Z
raymondfam marked the issue as duplicate of #306
#2 - c4-judge
2024-03-05T04:30:23Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-05T04:35:00Z
HickupHH3 marked the issue as satisfactory
#4 - 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/main/src/FighterFarm.sol#L470 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L110
Generation of a fighertype can be increased by FighterFarm.incrementGeneration() whose logic is below:
function incrementGeneration(uint8 fighterType) external returns (uint8) { require(msg.sender == _ownerAddress); generation[fighterType] += 1; maxRerollsAllowed[fighterType] += 1; return generation[fighterType]; }
However, when a fighter's generation is incremented, its numElements
are not being set.
So consider the following scenario:
Generation of fightertype CHAMPION has been incremented using incrementGeneration(0).
Now the generation of CHAMPION fightertype becomes from 0 to 1.
But numElement[1] = 0
because it is not set when incrementing the generation.
Now the following line inside FigherFarm._createFighterBase() will cause revert due to % with 0 because generation[CHAMPION] = 1
and a numElements[1] = 0
.
uint256 element = dna % numElements[generation[fighterType]];
_createFighterBase()
is used by _createNewFighter()
that is used to create new fighter and reRoll()
. So minting of new fighter and reRoll of existing fighter is not possible and MergingPool.claimRewards() won't work as well blocking the claim of rewards.
Make this first test in FigherFarm.t.sol;
function test_incrementGenertionBug() public { // normal mint working fine vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool( address(this), "_neuralNetHash0", "original0", [uint256(100), uint256(100)] ); // incrementing generation _fighterFarmContract.incrementGeneration(0); vm.prank(address(_mergingPoolContract)); vm.expectRevert(); _fighterFarmContract.mintFromMergingPool( address(this), "_neuralNetHash", "original", [uint256(100), uint256(100)] ); }
Manual Review
Also set numElements
when incrementing the generation of a fightertype like below.
function incrementGeneration(uint8 fighterType, uint8 _numElements) external returns (uint8) { require(msg.sender == _ownerAddress); generation[fighterType] += 1; maxRerollsAllowed[fighterType] += 1; numElements[fighterType] = __numElements; return generation[fighterType]; }
DoS
#0 - c4-pre-sort
2024-02-22T18:51:09Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T18:51:19Z
raymondfam marked the issue as duplicate of #45
#2 - c4-judge
2024-03-07T07:04:47Z
HickupHH3 marked the issue as satisfactory
π Selected for report: ahmedaghadi
Also found by: 0x13, 0xAleko, 0xDetermination, 0xKowalski, 0xPluto, 0xRiO, 0xvj, AlexCzm, Avci, BARW, BigVeezus, Cryptor, DeFiHackLabs, Draiakoo, Fitro, Giorgio, GoSlang, Greed, Honour, Kalogerone, KmanOfficial, Krace, McToady, MidgarAudits, MrPotatoMagic, Nyxaris, ReadyPlayer2, Ryonen, SovaSlava, SpicyMeatball, VAD37, _eperezok, alexzoid, almurhasan, btk, cu5t0mpeo, deadrxsezzz, djxploit, dvrkzy, emrekocak, erosjohn, evmboi32, fnanni, grearlake, inzinko, jesjupyter, jesusrod15, josephdara, ke1caM, klau5, ktg, ladboy233, merlinboii, nuthan2x, peanuts, pipidu83, pontifex, radev_sw, sl1, sobieski, soliditywala, t0x1c, taner2344, vnavascues, y4y, yovchev_yoan, zaevlad
0.2347 USDC - $0.23
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L294
There is only way to claim NRN that is through claimNRN().
If a user have not claimNRN for a long while, he may be unable to claim because claimNRN() will calculate NRN from the last round he claimed to the current-1 round, because if the difference between numRoundsClaimed[user] and current-1 round is too large, then the transaction may reach the block gas limit and user won't be able to claimNRN().
Manual review
Add another function that allows user to claim for any previous round he wishes and keep track in a mapping that user has claimed for which round. That way even if he claimNRN after a long while, he may be able to claim individually if faces max block gas limit issue.
DoS
#0 - c4-pre-sort
2024-02-25T02:28:15Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-25T02:28:35Z
raymondfam marked the issue as duplicate of #1541
#2 - c4-judge
2024-03-11T13:01:20Z
HickupHH3 marked the issue as duplicate of #216
#3 - c4-judge
2024-03-11T13:08:05Z
HickupHH3 changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-03-12T02:45:08Z
HickupHH3 marked the issue as partial-50
#5 - c4-judge
2024-03-21T02:57:45Z
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/main/src/FighterFarm.sol#L214
DNA plays an important part in determining that what element
and weight
to give to a Fighter.
However, inside FighterFarm.claimFighters(), DNA is calculated as below
uint256(keccak256(abi.encode(msg.sender, fighters.length))),
Since it takes msg.sender
as well as fighters.length
into account, a claimer can wait enough time so that fighters.length
comes to a value that would yield user such a DNA whose element
and weight
is more useful for the fighter.
Consider the following example:
1 - address(123) is allowed to claim a fighter.
2 - Suppose fighters.length = 10
, so the owner of address(123) can simulate the transaction to find what DNA would uint256(keccak256(abi.encode(address(123), fighters.length)))
will yield and what will be the element
and weight
against that DNA for the fighter.
3 - He can continue simulate after each time fighters.length
changes to mint a fighter with more useful weight
and element
.
Below is the coded POC which shows how fighters.length
influence weight
and element
element = 2 and weight = 93 will be assigned to the fighter when fighter.length = 0
. Below POC verifies that.
function test_whenLengthIsZero() 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"; // @audit minting fighter when fighter.lenght = 0 _fighterFarmContract.claimFighters( numToMint, claimSignature, claimModelHashes, claimModelTypes ); }
element = 0 and weight = 67 will be assigned to the fighter when length will be 1. Below POC verifies that.
function test_whenLengthIsOne() public { vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool( _ownerAddress, "_neuralNetHash", "original", [uint256(100), uint256(100)] ); 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"; // @audit minting fighter when fighter.lenght = 1 _fighterFarmContract.claimFighters( numToMint, claimSignature, claimModelHashes, claimModelTypes ); }
Manual Review
Use Chainlink VRF for the source of DNA randomness rather then depending of user controlled params.
Other
#0 - c4-pre-sort
2024-02-24T01:59:28Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T02:00:11Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:53:16Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-15T02:10:54Z
HickupHH3 changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-03-22T04:21:57Z
HickupHH3 marked the issue as duplicate of #376