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: 71/283
Findings: 3
Award: $88.61
π Selected for report: 0
π Solo Findings: 0
π 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
29.1474 USDC - $29.15
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L68
Missing DEFAULT_ADMIN_ROLE in the Neuron contract will block admins to manage roles.
Neuron contract inherits from AccessControl contract to implement role access for some core functions. However a constructor is missing DEFAULT_ADMIN_ROLE setup for a contract owner and other roles.
DEFAULT_ADMIN_ROLE is used to manage other roles in the contract and revoke access from users in case of any problems.
Right now it creates two problems:
Wrong address can not be revoked from the contract as there is no admin for Staker, Minter and Spender roles;
If the admin will be changed the new admin will not be granted with an admin role for the contract;
So the Access control system for the Neuron contract is broken.
Manual review
Consider adding the next lines in the constructor:
constructor(address ownerAddress, address treasuryAddress_, address contributorAddress) ERC20("Neuron", "NRN") { _ownerAddress = ownerAddress; treasuryAddress = treasuryAddress_; isAdmin[_ownerAddress] = true; _mint(treasuryAddress, INITIAL_TREASURY_MINT); _mint(contributorAddress, INITIAL_CONTRIBUTOR_MINT); + _setupRole(DEFAULT_ADMIN_ROLE,ownerAddress); + _setRoleAdmin(MINTER_ROLE,DEFAULT_ADMIN_ROLE); + _setRoleAdmin(SPENDER_ROLE,DEFAULT_ADMIN_ROLE); + _setRoleAdmin(STAKER_ROLE,DEFAULT_ADMIN_ROLE); }
Governance
#0 - c4-pre-sort
2024-02-22T05:03:49Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T05:03:57Z
raymondfam marked the issue as duplicate of #20
#2 - c4-judge
2024-03-05T05:11:11Z
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/MergingPool.sol#L149 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L33
Users will not be able to cliam rewards from Merging Pool after some game rounds passed.
After some rounds of the game users are able to claim their rewards in Merging Pool contract by calling claimRewards
:
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]) { _fighterFarmInstance.mintFromMergingPool( msg.sender, modelURIs[claimIndex], modelTypes[claimIndex], customAttributes[claimIndex] ); claimIndex += 1; } } } if (claimIndex > 0) { emit Claimed(msg.sender, claimIndex); } }
The problem is it tryes to send rewards for multiple rounds at once.
Imagine the following situation.
To play a game any user must have at least one NFT Fighter. As we can see in Fighter Farm contract that quantity of NFTs for each address is limited to 10:
uint8 public constant MAX_FIGHTERS_ALLOWED = 10;
So if user with 1 NFT will have rewards for 9 rounds he will not be able to redeem it.
The problem can be severe if user has more NFTs: the more NFTs he has the less rewards needed to block redeeming process.
Manual review, Foundry
Consider providing a function for users to let them redeem NFTs for a specific round by themselves.
Context
#0 - c4-pre-sort
2024-02-22T08:43:19Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T08:43:28Z
raymondfam marked the issue as duplicate of #216
#2 - c4-judge
2024-03-11T12:48:44Z
HickupHH3 marked the issue as satisfactory
π Selected for report: t0x1c
Also found by: 0xCiphky, 0xDetermination, Draiakoo, Greed, Kalogerone, MatricksDeCoder, MidgarAudits, MrPotatoMagic, PedroZurdo, Shubham, SpicyMeatball, VAD37, Velislav4o, ZanyBonzy, btk, cats, djxploit, forkforkdog, givn, ladboy233, lanrebayode77, lil_eth, visualbits, zaevlad
59.2337 USDC - $59.23
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L158
Users can exceed daily allowance for game items by transfering them from another game account.
There is a special daily allowance system for game items to keep the balance and avoid unfair situations between players.
Users can mint some game items with some limitaions on a daily basis:
function mint(uint256 tokenId, uint256 quantity) external { require(tokenId < _itemCount); uint256 price = allGameItemAttributes[tokenId].itemPrice * quantity; require(_neuronInstance.balanceOf(msg.sender) >= price, "Not enough NRN for purchase"); require( allGameItemAttributes[tokenId].finiteSupply == false || ( allGameItemAttributes[tokenId].finiteSupply == true && quantity <= allGameItemAttributes[tokenId].itemsRemaining ) ); @> require( dailyAllowanceReplenishTime[msg.sender][tokenId] <= block.timestamp || quantity <= allowanceRemaining[msg.sender][tokenId] ); _neuronInstance.approveSpender(msg.sender, price); bool success = _neuronInstance.transferFrom(msg.sender, treasuryAddress, price); if (success) { if (dailyAllowanceReplenishTime[msg.sender][tokenId] <= block.timestamp) { _replenishDailyAllowance(tokenId); } allowanceRemaining[msg.sender][tokenId] -= quantity; if (allGameItemAttributes[tokenId].finiteSupply) { allGameItemAttributes[tokenId].itemsRemaining -= quantity; } _mint(msg.sender, tokenId, quantity, bytes("random")); emit BoughtItem(msg.sender, tokenId, quantity); } }
As you can see for each game item can be specified its own limit that is took into consideration while minting.
However user can trasfer NFT from one account to another without any limitations. For example, here is overriden function:
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); }
And it only checks if the game item can be trasfered, but does not account any limitaions.
So a malicious user can double his items with a help of a second game account.
Manual review
Consider providing a limitaion check for a game item during it transfer. A good example can be transfer checks in FighterFarm contract:
function _ableToTransfer(uint256 tokenId, address to) private view returns(bool) { return ( _isApprovedOrOwner(msg.sender, tokenId) && balanceOf(to) < MAX_FIGHTERS_ALLOWED && !fighterStaked[tokenId] ); }
Context
#0 - c4-pre-sort
2024-02-22T17:56:32Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T17:56:51Z
raymondfam marked the issue as duplicate of #43
#2 - c4-judge
2024-03-07T04:15:48Z
HickupHH3 marked the issue as satisfactory