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: 122/283
Findings: 3
Award: $22.56
π Selected for report: 0
π Solo Findings: 0
π 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.1173 USDC - $0.12
The MegingPool::claimRewards
function loops through all the rounds. Then loop through (in a nested loop) the current round's winners and check if the msg.sender is in the winnerAddresses
mapping. If he is a winner, he is minted a Figher NFT. The function continues until he claims all his won fighters. The issue is that if the msg.sender has a lot of unclaimed Fighters this function could be very costly or even introduce a denial of service (DoS).
The gas cost for the winner that has a lot of unclaimed fighers can be unreasonably high, potentially preventing him from claiming his rewards.
Place the following test into MergingPool.t.sol
// Testing claimRewards when the _ownerAddress has 6 rounds won and has not claimed his Fighter NFTs. function testPickWinnerAudit() public { _mintFromMergingPool(_ownerAddress); _mintFromMergingPool(_DELEGATED_ADDRESS); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); assertEq(_fighterFarmContract.ownerOf(1), _DELEGATED_ADDRESS); uint256[] memory _winners = new uint256[](2); _winners[0] = 0; _winners[1] = 1; // Filling the arrays to claim the NFTs string[] memory _modelURIs = new string[](8); _modelURIs[0] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[1] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[2] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[3] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[4] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[5] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[6] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelURIs[7] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; string[] memory _modelTypes = new string[](8); _modelTypes[0] = "original"; _modelTypes[1] = "original"; _modelTypes[2] = "original"; _modelTypes[3] = "original"; _modelTypes[4] = "original"; _modelTypes[5] = "original"; _modelTypes[6] = "original"; _modelTypes[7] = "original"; uint256[2][] memory _customAttributes = new uint256[2][](14); _customAttributes[0][0] = uint256(1); _customAttributes[0][1] = uint256(80); _customAttributes[1][0] = uint256(1); _customAttributes[1][1] = uint256(80); _customAttributes[1][1] = uint256(1); _customAttributes[1][1] = uint256(80); _customAttributes[2][1] = uint256(1); _customAttributes[2][1] = uint256(80); _customAttributes[1][1] = uint256(1); _customAttributes[1][1] = uint256(80); _customAttributes[2][1] = uint256(1); _customAttributes[2][1] = uint256(80); _customAttributes[1][1] = uint256(1); _customAttributes[1][1] = uint256(80); _customAttributes[2][1] = uint256(1); _customAttributes[2][1] = uint256(80); _customAttributes[1][1] = uint256(1); _customAttributes[1][1] = uint256(80); _customAttributes[2][1] = uint256(1); _customAttributes[2][1] = uint256(80); _customAttributes[1][1] = uint256(1); _customAttributes[1][1] = uint256(80); _customAttributes[2][1] = uint256(1); _customAttributes[2][1] = uint256(80); _customAttributes[1][1] = uint256(1); _customAttributes[1][1] = uint256(80); _customAttributes[2][1] = uint256(1); _customAttributes[2][1] = uint256(80); // Picking the winners for 6 rounds. _ownerAddress has won all 6 // winners of roundId 0 are picked _mergingPoolContract.pickWinner(_winners); // winners of roundId 1 are picked _mergingPoolContract.pickWinner(_winners); // winners of roundId 2 are picked _mergingPoolContract.pickWinner(_winners); // winners of roundId 3 are picked _mergingPoolContract.pickWinner(_winners); // winners of roundId 4 are picked _mergingPoolContract.pickWinner(_winners); // winners of roundId 5 are picked _mergingPoolContract.pickWinner(_winners); // _ownerAddress decides to claim his rewards after the 6th round uint256 gasStart = gasleft(); vm.prank(_ownerAddress); _mergingPoolContract.claimRewards(_modelURIs, _modelTypes, _customAttributes); uint256 gasEnd = gasleft(); // The gas that the _ownerAddress has to pay to call .claimRewards uint256 gasUsed = (gasStart - gasEnd); console.log("Gas used: ", gasUsed); }
Foundry Test
Consider making the function such that a user can only claim one NFT for a specified roundId. The function will cost significantly less gas and a user can choose for which round he wants to claim his Fighter. This method will remove the need for a nested loop.
DoS
#0 - c4-pre-sort
2024-02-22T09:22:30Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T09:22:46Z
raymondfam marked the issue as duplicate of #1541
#2 - c4-pre-sort
2024-02-23T23:37:33Z
raymondfam marked the issue as sufficient quality report
#3 - c4-judge
2024-03-11T13:01:38Z
HickupHH3 marked the issue as duplicate of #216
#4 - c4-judge
2024-03-19T04:12:58Z
HickupHH3 marked the issue as partial-50
π Selected for report: givn
Also found by: 0x11singh99, 0xAkira, 0xBinChook, 0xDetermination, 0xMosh, 0xStriker, 0xmystery, 14si2o_Flint, 7ashraf, Aamir, AlexCzm, BARW, Bauchibred, BenasVol, BigVeezus, Blank_Space, Bube, DarkTower, DeFiHackLabs, EagleSecurity, KmanOfficial, Krace, McToady, MrPotatoMagic, PetarTolev, Rolezn, SHA_256, SpicyMeatball, Tekken, Timenov, ZanyBonzy, agadzhalov, alexzoid, boredpukar, btk, cartlex_, dimulski, forkforkdog, haxatron, immeas, jesjupyter, juancito, kartik_giri_47538, klau5, lsaudit, merlinboii, nuthan2x, offside0011, oualidpro, peter, radev_sw, rekxor, rspadi, shaflow2, shaka, swizz, vnavascues, yotov721, yovchev_yoan
8.8123 USDC - $8.81
GameItems
the instantiateNeuronContract
function could lead to revert if not initializedDescription: In GameItems
the instantiateNeuronContract
function instantiates the NRN contract address. If someone calls mint
before instantiation, it will revert.
Impact: The function mint
could revert if nrnAddress is not instantiated.
Recommended Mitigation: Recommended mitigations are:
instantiateNeuronContract
function is called with the right address._neuronInstance
in the constructor and add a changeNeuronInstance
function in order to change it.GameItems
the createGameItem
function only emits an event, only if the item is not transferable and not when it is transferableDescription: In GameItems
the createGameItem
function is responsible for creating a game item, but an event is emitted only when the item is not transferable. In the contract, there is an event called Unlocked
, responsible for emitting an event for transferable items. Emit the Unlocked
event if transferable
param is equal to true.
Impact: Not communicating information on the blockchain if the item created is transferable.
Recommended Mitigation: Emit the Unlocked
event if transferable
param is equal to true
if (!transferable) { emit Locked(_itemCount); + } else { + emit Unlocked(_itemCount); + }
GameItems
the _replenishDailyAllowance
function casts uint32 to uint256, which can lead to integer overflowDescription: In GameItems
the _replenishDailyAllowance
function updates the allowances. In the update of dailyAllowanceReplenishTime
there is an unnecessary cast from uint256 to uint32. Parsing from a higher to a lower integer number is not a recommended practice.
Impact: The unsafe cast from uint32 to uint256, can potentially lead to integer overflow
Recommended Mitigation: Remove the casting
- dailyAllowanceReplenishTime[msg.sender][tokenId] = uint32(block.timestamp + 1 days); + dailyAllowanceReplenishTime[msg.sender][tokenId] = block.timestamp + 1 days;
Neuron
the functions transferOwnership
and adjustAdminAccess
do not emit an event after changing storageDescription: In Neuron
the functions transferOwnership
and adjustAdminAccess
change the state of the blockchain, therefore should emit an event.
Impact: Not communicating information on the blockchain if the ownership is transfered, or an address has been granted the role Admin
.
Recommended Mitigation: Add these events and emit them after the function is executed:
+ event TransferOwnership(address newOwnerAddress); function transferOwnership(address newOwnerAddress) external { require(msg.sender == _ownerAddress); _ownerAddress = newOwnerAddress; + emit TransferOwnership(newOwnerAddress); } + event AdminAccessAdjusted(address adminAddress, bool access); function adjustAdminAccess(address adminAddress, bool access) external { require(msg.sender == _ownerAddress); isAdmin[adminAddress] = access; + emit AdminAccessAdjusted(adminAddress, access) }
MergingPool
the functions updateWinnersPerPeriod
, transferOwnership
, and adjustAdminAccess
do not emit an event after changing storageDescription: In MergingPool
the functions updateWinnersPerPeriod
, transferOwnership
, and adjustAdminAccess
change the state of the blockchain, therefore should emit an event.
Impact: Not communicating information on the blockchain if the ownership is transfered, updateWinnersPerPeriod is updated, or an address has been granted the role Admin
.
Recommended Mitigation: Add these events and emit them after the function is executed:
+ event TransferOwnership(address newOwnerAddress); function transferOwnership(address newOwnerAddress) external { require(msg.sender == _ownerAddress); _ownerAddress = newOwnerAddress; + emit TransferOwnership(newOwnerAddress); } + event AdminAccessAdjusted(address adminAddress, bool access); function adjustAdminAccess(address adminAddress, bool access) external { require(msg.sender == _ownerAddress); isAdmin[adminAddress] = access; + emit AdminAccessAdjusted(adminAddress, access) } + event WinnerPerPeriodUpdate(uint256 newWinnersPerPeriod); function updateWinnersPerPeriod(uint256 newWinnersPerPeriodAmount) external { require(isAdmin[msg.sender]); winnersPerPeriod = newWinnersPerPeriodAmount; + emit WinnerPerPeriodUpdate(newWinnersPerPeriodAmount); }
FighterFarm
the functions instantiateAIArenaHelperContract
, instantiateMintpassContract
, instantiateNeuronContract
, and setMergingPoolAddress
could lead to revert if not instantiatedDescription: In FighterFarm
the functions instantiateAIArenaHelperContract
, instantiateMintpassContract
, instantiateNeuronContract
, and setMergingPoolAddress
function instantiate important addresses in the contract. If not called right after the contract deployment, some functions could be unusable.
Impact: Not instantiating these addresses could lead to some functions being unusable.
Recommended Mitigation: Recommended mitigations are:
GameItems
the function setTokenURI
can set the tokenURI to a nonexistent game itemDescription: In GameItems
the function setTokenURI
is responsible for setting the tokenURI to a specific tokenID. However, the function is public and onlyAdmin, meaning that an admin could without intention set a tokenURI to a non-existent tokenId.
Recommended Mitigation: Check if the tokenId
is bigger than the _itemCount
.
function setTokenURI(uint256 tokenId, string memory _tokenURI) public { + if (tokenId > _itemCount) revert CustomError(); require(isAdmin[msg.sender]); _tokenURIs[tokenId] = _tokenURI; }
GameItems::createGameItem
no check for params itemPrice
and dailyAllowance
to be greater than 0Description: In the GameItems
contract, the function createGameItem
has no check for important parameters - itemPrice
and dailyAllowance
.
Recommended Mitigation: Add the following check
+ if (itemPrice <= 0 && dailyAllowance <= 0) revert CustomError();
FighterOps::viewFighterInfo
returns the address that is passed at owner
.Description: In contract FighterOps
the function viewFighterInfo
returns the address that is passed to it in the parameter address owner
. The address is not used for any operations and is just returned.
Neuro::contributorAddress
initialized but never usedDescription: In contract Neuron
the contributorAddress
is supposed to distribute NRNs to early contributors. But the address is only initialized and never used.
VoltageManager::_ownerAddress
use the private keyword for better readabilityDescription: In contract VoltageManager
the variable _ownerAddress
has not been specified visibility. For better readability use the private keyword.
Recommended Mitigation: Add the private
keyword
/// The address that has owner privileges (initially the contract deployer). - address _ownerAddress; + address private _ownerAddress;
#0 - raymondfam
2024-02-26T04:43:06Z
Adequate amount of L and NC entailed.
#1 - c4-pre-sort
2024-02-26T04:43:09Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2024-03-18T04:51:08Z
HickupHH3 marked the issue as grade-b
π 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
GameItems
the variables name
and symbol
should be set to constants in order to save gasDescription: In the GameItems
contract the name
and symbol
variables are initialized at the start and never changed. Make them constant in order to save gas.
Recommended Mitigation:
/// @notice The name of this smart contract. - string public name = "AI Arena Game Items"; /// @notice The symbol for this smart contract. - string public symbol = "AGI"; /// @notice The name of this smart contract. + string public constant name = "AI Arena Game Items"; /// @notice The symbol for this smart contract. + string public constant symbol = "AGI";
GameItems
save the string return in contractURI
to a constant variableDescription: In the GameItems
contract the function contractURI
returns a string. The more gas-efficient way would be to store that string as a constant variable and then return it.
Proof of Concept: Paste this code in Remix.
// SPDX-License-Identifier: MIT pragma solidity 0.8.20; contract TestConstantGas { string private constant URI = "ipfs://bafybeih3witscmml3padf4qxbea5jh4rl2xp67aydqvqsxmyuzipwtpnii"; function getConstantPubilc() public pure returns(string memory) { return URI; } function returnString() public pure returns(string memory) { return "ipfs://bafybeih3witscmml3padf4qxbea5jh4rl2xp67aydqvqsxmyuzipwtpnii"; } }
Recommended Mitigation: Make a constant storage variable to store the string
+ string public constant URI = "ipfs://bafybeih3witscmml3padf4qxbea5jh4rl2xp67aydqvqsxmyuzipwtpnii" function contractURI() public pure returns (string memory) { - return "ipfs://bafybeih3witscmml3padf4qxbea5jh4rl2xp67aydqvqsxmyuzipwtpnii"; + return URI; }
GameItems
the transferOwnership
, adjustAdminAccess
, adjustTransferability
and instantiateNeuronContract
should use onlyOwner modifier to save gasDescription: In GameItems
the transferOwnership
, adjustAdminAccess
, adjustTransferability
and instantiateNeuronContract
functions all share a common require statement. Make a modifier with the require in order to save gas.
Recommended Mitigation: Add an onlyOwner modifier to all the functions mentioned above and remove the require statement in the function.
+ modifier onlyOwner() { + require(msg.sender == _ownerAddress); + _; + } + function transferOwnership(address newOwnerAddress) external onlyOwner { - require(msg.sender == _ownerAddress); + function adjustAdminAccess(address adminAddress, bool access) external onlyOwner { - require(msg.sender == _ownerAddress); + function adjustTransferability(uint256 tokenId, bool transferable) external onlyOwner { - require(msg.sender == _ownerAddress); + function instantiateNeuronContract(address nrnAddress) external onlyOwner { - require(msg.sender == _ownerAddress);
GameItems
the treasuryAddress
variable should be immutable to save gasDescription: In GameItems
the treasuryAddress
is initialized in the constructor and never changed. Make it immutable in order to save gas.
Recommended Mitigation:
/// @notice The address that recieves funds of purchased game items. - address public treasuryAddress; + address public immutable treasuryAddress;
Neuron
the transferOwnership
, addMinter
, addStaker
, addSpender
and adjustAdminAccess
should use onlyOwner modifier to save gasDescription: In Neuron
the transferOwnership
, addMinter
, addStaker
, addSpender
and adjustAdminAccess
functions all share a common require statement. Make a modifier with the require in order to save gas.
Recommended Mitigation: Add an onlyOwner modifier to all the functions mentioned above and remove the require statement in the function.
+ modifier onlyOwner() { + require(msg.sender == _ownerAddress); + _; + } + function transferOwnership(address newOwnerAddress) external onlyOwner { - require(msg.sender == _ownerAddress); + function addMinter(address newMinterAddress) external onlyOwner { - require(msg.sender == _ownerAddress); + function addStaker(address newStakerAddress) external onlyOwner { - require(msg.sender == _ownerAddress); + function addSpender(address newSpenderAddress) external onlyOwner { - require(msg.sender == _ownerAddress); + function adjustAdminAccess(address adminAddress, bool access) external onlyOwner { - require(msg.sender == _ownerAddress);
Neuron
the setupAirdrop
use uint256 instead of uint32 in the for loop to save gasDescription: In Neuron
the setupAirdrop
uses a uint32 for the for loop. It would be safer and more gas efficient to use uint256 instead.
Proof of Concept: When tested uint256 uses significantly less gas than uint32.
Paste this in Remix
// SPDX-License-Identifier: MIT pragma solidity 0.8.20; contract TestUintForLoop { uint256[] addresses; function uint32Test() external { for (uint32 i = 0; i < 1000; i++) { addresses.push(i); } } function uint256Test() external { for (uint256 i = 0; i < 1000; i++) { addresses.push(i); } } }
Recommended Mitigation: Replace uint32 with uint256
- for (uint32 i = 0; i < recipientsLength; i++) { + for (uint256 i = 0; i < recipientsLength; i++) {
VoltageManager
the transferOwnership
and adjustAdminAccess
should use onlyOwner modifier to save gasDescription: In VoltageManager
the transferOwnership
and adjustAdminAccess
functions share a common require statement. Make a modifier with the require in order to save gas.
Recommended Mitigation: Add an onlyOwner modifier to the functions mentioned above and remove the require statement in the function.
+ modifier onlyOwner() { + require(msg.sender == _ownerAddress); + _; + } + function transferOwnership(address newOwnerAddress) external onlyOwner { - require(msg.sender == _ownerAddress); + function adjustAdminAccess(address adminAddress, bool access) external onlyOwner { - require(msg.sender == _ownerAddress);
#0 - raymondfam
2024-02-25T21:44:30Z
7 generic G.
#1 - c4-pre-sort
2024-02-25T21:44:37Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2024-03-19T07:51:05Z
HickupHH3 marked the issue as grade-b