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: 108/283
Findings: 3
Award: $42.82
π 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#L11
The contracts of the Ai Arena ecosystem are designed with a way to change addresses. In case of some emergency, it should be possible to revoke given roles.
Neuron.sol
contract is a token contract which can give different privileges like MINTER
, SPENDER
, STAKER
roles to other contracts. The AccessControl
contract is used to implement role mechanics. However, it requires to set an DEFAULT_ADMIN_ROLE
for admin. The contract is not doing this, what leads to impossibility to revoke given roles.
POC (add it to Neuron.t.sol
contract):
function testRevokeRole() public { address minter = vm.addr(3); _neuronContract.addMinter(minter); // this will revert _neuronContract.revokeRole(_neuronContract.MINTER_ROLE(), minter); }
Running 1 test for test/Neuron.t.sol:NeuronTest [FAIL. Reason: revert: AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000] testRevokeRole()
Manual Analysis
Consider adding DEFAULT_ADMIN_ROLE
to the owner of the contract.
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); + _grantRole(DEFAULT_ADMIN_ROLE, _ownerAddress); } function transferOwnership(address newOwnerAddress) external { require(msg.sender == _ownerAddress); _ownerAddress = newOwnerAddress; _grantRole(DEFAULT_ADMIN_ROLE, newOwnerAddress); revokeRole(DEFAULT_ADMIN_ROLE, _ownerAddress); }
Access Control
#0 - c4-pre-sort
2024-02-22T05:14:42Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T05:14:49Z
raymondfam marked the issue as duplicate of #20
#2 - c4-judge
2024-03-05T10:01:27Z
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#L462
The user will know the attributes of the NFT before minting and can exploit it to get the wanted ones. He can simply create infinite number of EOAs to bruteforce attributes locally.
The _createFighterBase
is used to determine some parameters of the NFT. Parameters are determined using pseudo-randomness which allows to any user to know what they will mint. The dna
parameter is equal to to uint256(keccak256(abi.encode(msg.sender, fighters.length)))
for creating and uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId])))
for rerolling NFT.
function _createFighterBase(uint256 dna, uint8 fighterType) private view returns (uint256, uint256, uint256) { uint256 element = dna % numElements[generation[fighterType]]; uint256 weight = dna % 31 + 65; uint256 newDna = fighterType == 0 ? dna : uint256(fighterType); return (element, weight, newDna); }
Manual review
Do not rely on pseudo-randomness to determine NFT attributes. Consider using a decentralized oracle for the generation of random numbers, such asΒ Chainlinks VRF.
Other
#0 - c4-pre-sort
2024-02-24T02:01:38Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T02:01:47Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:49:26Z
HickupHH3 changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-06T03:53:22Z
HickupHH3 marked the issue as satisfactory
#4 - c4-judge
2024-03-15T02:10:54Z
HickupHH3 changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-03-22T04:23:04Z
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
Use Merkle Tree for airdropping tokens. This will save a decent amount of gas and will require only storing the root inside the contract.
function setupAirdrop(address[] calldata recipients, uint256[] calldata amounts) external { require(isAdmin[msg.sender]); require(recipients.length == amounts.length); uint256 recipientsLength = recipients.length; for (uint32 i = 0; i < recipientsLength; i++) { _approve(treasuryAddress, recipients[i], amounts[i]); } } /// @notice Claims the specified amount of tokens from the treasury address to the caller's address. /// @param amount The amount to be claimed function claim(uint256 amount) external { require(allowance(treasuryAddress, msg.sender) >= amount, "ERC20: claim amount exceeds allowance"); transferFrom(treasuryAddress, msg.sender, amount); emit TokensClaimed(msg.sender, amount); }
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L127-L145
Updating global variables inside a loop is quite expensive. Here it is possible to add the value once.
function claimNRN() external { require(numRoundsClaimed[msg.sender] < roundId, "Already claimed NRNs for this period"); uint256 claimableNRN = 0; uint256 nrnDistribution; uint32 lowerBound = numRoundsClaimed[msg.sender]; for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) { nrnDistribution = getNrnDistribution(currentRound); claimableNRN += (accumulatedPointsPerAddress[msg.sender][currentRound] * nrnDistribution) / totalAccumulatedPoints[currentRound]; // @gas we can sum up at the end - numRoundsClaimed[msg.sender] += 1; } + numRoundsClaimed[msg.sender] += roundId - lowerBound; if (claimableNRN > 0) { amountClaimed[msg.sender] += claimableNRN; // @high time bomb probably, we should transfer from the treasury _neuronInstance.mint(msg.sender, claimableNRN); emit Claimed(msg.sender, claimableNRN); } }
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L294-L311
The contract stores the address and instance, which is redundant. Simply use StakeAtRisk(_stakeAtRiskAddress)
where needed. The _stakeAtRiskInstance
can be removed.
contract RankedBattle { ... address _stakeAtRiskAddress; ... StakeAtRisk _stakeAtRiskInstance; }
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L69
#0 - raymondfam
2024-02-25T21:08:14Z
3G
#1 - c4-pre-sort
2024-02-25T21:08:18Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2024-03-19T07:54:38Z
HickupHH3 marked the issue as grade-b