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: 35/283
Findings: 6
Award: $178.47
π 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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L291-L303
There are some tokens representing GameItems
that must not be transfered. The issue allows a user to bypass this restriction and be able to transfer these tokens.
In the GameItems.sol
contract, in order to create a new item, an admin can call the createGameItem()
function.
The transferability of these tokens can be set using the transferable
parameter and is enforced by the safeTransferFrom()
function that has been overridden.
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L208-L216
function createGameItem( string memory name_, string memory tokenURI, bool finiteSupply, bool transferable, uint256 itemsRemaining, uint256 itemPrice, uint16 dailyAllowance )
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L291-L303
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); }
However this restriction can be bypassed using the safeBatchTransferFrom
from ERC1155
, giving the ability to transfer all GameItems
(even those that should not be)
Manual analysis
Override the safeBatchTransferFrom()
function and make sure all the tokens given in the array parameter are not transferable
Token-Transfer
#0 - c4-pre-sort
2024-02-22T04:32:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T04:32:33Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:29:36Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:47:38Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-05T04:58:01Z
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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L370-L391
If a user has minted a fighter after the 255th, he won't ever be able to reRoll()
it.
This completely breaks the dedicated functionality of the protocol
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L370-L391
The FighterFarm::reRoll()
function allows a user to reroll his fighter to change his attributes.
Here is the function declaration :
function reRoll(uint8 tokenId, uint8 fighterType) public
The tokenId
represents the fighter to reroll.
As you can see, this parameter is a uint8
meaning it can only go up to 255.
If a user owns the NFT number 260 for example, he won't be able to reroll it and utilize the functionality.
Manual analysis
Change the type of the tokenId
parameter of the reRoll()
function from uint8
to uint256
like such :
function reRoll(uint256 tokenId, uint8 fighterType) public
Error
#0 - c4-pre-sort
2024-02-22T02:35:57Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T02:36:04Z
raymondfam marked the issue as duplicate of #68
#2 - c4-judge
2024-03-05T02:00:51Z
HickupHH3 marked the issue as satisfactory
π 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
7.2869 USDC - $7.29
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L93-L96 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L101-L104 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L109-L112
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L93-L96
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L101-L104
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L109-L112
Once a role is given to an address, it will persist forever with no way to revoke it.
If either RankedBattle.sol
, GameItems.sol
, FighterFarm.sol
is set to a new address (after an upgrade for example), their old implementation will still possess the roles meaning they are still able to perform privileged actions.
These privileged actions include minting, approving, staking NRN tokens
In some scenarios, these roles could also be temporarly given to an address but there would be no way to remove their privileges
In order to revoke a role given to an address, the admin of this specific role has to call the revokeRole(bytes32 role, address account)
function from Openzeppelin AccessControl.sol
function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { _revokeRole(role, account); }
This function checks that the msg.sender
has the required privileges to manage the role (that he is admin of this role) using the modifier onlyRole(getRoleAdmin(role))
function getRoleAdmin(bytes32 role) public view virtual override returns (bytes32) { return _roles[role].adminRole; } // ... modifier onlyRole(bytes32 role) { _checkRole(role); _; } // ... function _checkRole(bytes32 role) internal view virtual { _checkRole(role, _msgSender()); } // ... function _checkRole(bytes32 role, address account) internal view virtual { if (!hasRole(role, account)) { revert( string( abi.encodePacked( "AccessControl: account ", Strings.toHexString(uint160(account), 20), " is missing role ", Strings.toHexString(uint256(role), 32) ) ) ); } }
If this condition is not met, the transaction will revert which, in the case of the Neuron.sol
contract, always will because neither the admin role nor the admin are set.
Manual analysis
First, add the admin roles in the storage
bytes32 public constant MINTER_ADMIN_ROLE = keccak256("MINTER_ADMIN_ROLE"); bytes32 public constant SPENDER_ADMIN_ROLE = keccak256("SPENDER_ADMIN_ROLE"); bytes32 public constant STAKER_ADMIN_ROLE = keccak256("STAKER_ADMIN_ROLE");
Second, set role admin in the constructor
_setRoleAdmin(MINTER_ROLE, MINTER_ADMIN_ROLE); _setRoleAdmin(SPENDER_ROLE, SPENDER_ADMIN_ROLE); _setRoleAdmin(STAKER_ROLE, STAKER_ADMIN_ROLE);
Finally, implement functions to set the role admin
function assignMinterAdmin(address minterAdmin) external { require(msg.sender == _ownerAddress); _setupRole(MINTER_ADMIN_ROLE, minterAdmin); } function assignSpenderAdmin(address spenderAdmin) external { require(msg.sender == _ownerAddress); _setupRole(SPENDER_ADMIN_ROLE, spenderAdmin); } function assignStakerAdmin(address stakerAdmin) external { require(msg.sender == _ownerAddress); _setupRole(STAKER_ADMIN_ROLE, stakerAdmin); }
Now the role admin can use the following functions
revokeRole(MINTER_ROLE, oldMinter); revokeRole(SPENDER_ROLE, oldSpender); revokeRole(STAKER_ROLE, oldStaker);
Access Control
#0 - c4-pre-sort
2024-02-22T05:14:02Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T05:14:09Z
raymondfam marked the issue as duplicate of #20
#2 - c4-judge
2024-03-05T07:31:04Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-05T09:58:35Z
HickupHH3 marked the issue as not a duplicate
#4 - c4-judge
2024-03-05T10:00:08Z
HickupHH3 marked the issue as duplicate of #1507
#5 - c4-judge
2024-03-05T10:00:13Z
HickupHH3 marked the issue as partial-25
π 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-L311
Users can lose the NRN tokens they earned as rewards
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L294-L311
When users win a match, they may earn points which are stored in the accumulatedPointsPerAddress
mapping.
These points can later on be used to claim NRN tokens using the RankedBattle::claimNRN()
function.
This function goes through every round that the caller didn't claim rewards, in a for loop.
If the user didn't claim his rewards for a very long time and kept accumulating them, there is a risk that the loop consumes all the gas available and makes the transaction revert.
As a result, the user won't be able to claim his rewards ever.
Manual analysis
Add a new function to allow users to claim their rewards for a particular round.
DoS
#0 - c4-pre-sort
2024-02-25T02:29:12Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-25T02:29:23Z
raymondfam marked the issue as duplicate of #1541
#2 - c4-judge
2024-03-11T13:01:35Z
HickupHH3 marked the issue as duplicate of #216
#3 - c4-judge
2024-03-12T02:45:21Z
HickupHH3 marked the issue as partial-50
#4 - c4-judge
2024-03-21T02:59:50Z
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#L370-L391
User can predict the attributes of their fighters and get an unfair advantage compared to other players.
He could reroll them so they all have a diamond attribute, which is particularly rare, or get the best element and weight.
The reRoll()
function is used to modify the attributes of a particular fighter.
These attributes are determined using the internal _createFighterBase()
function which takes the dna
parameter to generate attributes.
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L370-L391
function reRoll(uint8 tokenId, uint8 fighterType) public { require(msg.sender == ownerOf(tokenId)); require(numRerolls[tokenId] < maxRerollsAllowed[fighterType]); require(_neuronInstance.balanceOf(msg.sender) >= rerollCost, "Not enough NRN for reroll"); _neuronInstance.approveSpender(msg.sender, rerollCost); bool success = _neuronInstance.transferFrom(msg.sender, treasuryAddress, rerollCost); if (success) { numRerolls[tokenId] += 1; uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId]))); (uint256 element, uint256 weight, uint256 newDna) = _createFighterBase(dna, fighterType); fighters[tokenId].element = element; fighters[tokenId].weight = weight; fighters[tokenId].physicalAttributes = _aiArenaHelperInstance.createPhysicalAttributes( newDna, generation[fighterType], fighters[tokenId].iconsType, fighters[tokenId].dendroidBool ); _tokenURIs[tokenId] = ""; } }
Since the dna
parameter is computed based upon the value of msg.sender
, in case it changes, the returned value will be completely different.
A user can simulate the reRoll()
function and if the outcome is not the one he expected, he can transfer his fighter to another address he controls and repeat the process until the rerolled fighter is the one he wants (a fighter with diamond
attribute for example).
Manual analysis
When performing RNG, a smart contract should rely on an external party to make the number generated unpredictable.
Use an oracle to generate the dna
number that is used to derivate the fighter's attributes
ERC721
#0 - c4-pre-sort
2024-02-24T01:57:09Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:57:21Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:52:30Z
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:53Z
HickupHH3 marked the issue as duplicate of #376
π 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#L147-L177
The restrictions regarding the mint()
function in the GameItems.sol
contract can by bypassed, allowing a user to own more tokens than the allowanceRemaining
and the dailyAllowanceReplenishTime
should enforce.
The mint()
function makes sure the caller can only claim a defined amount of tokens daily using the dailyAllowanceReplenishTime
and the allowanceRemaining
mappings
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L147-L177
require( dailyAllowanceReplenishTime[msg.sender][tokenId] <= block.timestamp || quantity <= allowanceRemaining[msg.sender][tokenId] );
However, this restriction can be bypassed, allowing a user to obtain more tokens than the daily amount allowed by mint()
using 1 wallet and transfering the token to another wallet.
In order to exploit the issue, a user can do the following (infinitely) :
Assume the user has a main wallet
mint()
function with the maximum of quantity
allowed by allGameItemAttributes
safeTransferFrom()
function to transfer all the tokens minted to the main walletManual analysis
In the safeTransferFrom()
overridden function, add more checks regarding the receiver's allowanceRemaining
and dailyAllowanceReplenishTime
mappings
Invalid Validation
#0 - c4-pre-sort
2024-02-22T18:12:58Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T18:13:06Z
raymondfam marked the issue as duplicate of #43
#2 - c4-judge
2024-03-07T04:21:55Z
HickupHH3 marked the issue as satisfactory