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: 14/283
Findings: 7
Award: $328.93
π 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
Non-transferable ERC1155 tokens in the GameItems contract can be bypassed via safeBatchTransferFrom
implemented in openzeppelin/contracts/token/ERC1155/ERC1155.sol, compromising item transfer restrictions and potentially disrupting the game's economy.
GameItems enforces item transferability checks in safeTransferFrom but forgot to override safeBatchTransferFrom
which allow any user to transfer any ERC1155 token from this contract, allowing restricted items to be transferred in batches.
Add this test to GameItem.t.sol :
function testBypassNonTransferrable() public { address alice = address(8); _gameItemsContract.createGameItem("PowerPlus", "https://ipfs.io/ipfs/", true, false, 10_000, 1 * 10 ** 18, 10); _fundUserWith4kNeuronByTreasury(msg.sender); vm.startPrank(msg.sender); _gameItemsContract.mint(0, 2); //paying 2 $NRN for 2 batteries _gameItemsContract.mint(1, 2); assertEq(_gameItemsContract.balanceOf(msg.sender, 0), 2); assertEq(_gameItemsContract.balanceOf(msg.sender, 1),2); (,,bool transferrable,,,) = _gameItemsContract.allGameItemAttributes(1); assertEq(transferrable,false); // 1. safeTransferFrom Revert due to transferrable set to false vm.expectRevert(); _gameItemsContract.safeTransferFrom(msg.sender,_ownerAddress,1,2,""); uint256[] memory ids = new uint256[](2); ids[0] = 0; ids[1] = 1; uint256[] memory values = new uint256[](2); values[0] = 1; values[1] = 2; // 2. safeBatchTransferFrom doesn't revert because not overriden _gameItemsContract.safeBatchTransferFrom(msg.sender,alice,ids,values,""); vm.stopPrank(); }
VSCode
Override safeBatchTransferFrom()
in GameItems to include transferable checks for batch transfers of Game Items
Token-Transfer
#0 - c4-pre-sort
2024-02-22T03:51:20Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T03:51:27Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:27:57Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:53:19Z
HickupHH3 marked the issue as satisfactory
π Selected for report: haxatron
Also found by: 0xAlix2, 0xCiphky, 0xStriker, 0xaghas, 0xbranded, 0xlamide, 0xmystery, 0xvj, 14si2o_Flint, Aamir, AgileJune, Aymen0909, DanielArmstrong, DarkTower, Draiakoo, EagleSecurity, Giorgio, Krace, KupiaSec, MidgarAudits, MrPotatoMagic, PoeAudits, Ryonen, SpicyMeatball, Topmark, Tychai0s, VAD37, Varun_05, VrONTg, WoolCentaur, _eperezok, alexxander, alexzoid, aslanbek, blutorque, btk, cartlex_, d3e4, devblixt, evmboi32, fnanni, immeas, jesjupyter, juancito, ke1caM, klau5, ktg, lil_eth, merlinboii, nuthan2x, peter, petro_1912, pkqs90, pynschon, radin100, sandy, sashik_eth, shaka, sl1, soliditywala, t0x1c, ubl4nk, visualbits, vnavascues
1.1225 USDC - $1.12
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L85 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L110 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L470
numElements mapping is used in fighterFarm.sol contract to reference number of elements per generation, we can define 2 types of NFT : the dendroid and the champions tokens, this mapping is initialized within fighterFarm constructor :
numElements[0] = 3;
When a fighter is created without a custom attribute or rerolled, _createFighterBase
is called and numElements
mapping is used to determine the element of an NFT that will be minted for fighter that have no customAttribute :
// _createNewFighter : if (customAttributes[0] == 100) { (element, weight, newDna) = _createFighterBase(dna, fighterType); } // reroll() : function reRoll(uint8 tokenId, uint8 fighterType) public { ..... uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId]))); (uint256 element, uint256 weight, uint256 newDna) = _createFighterBase(dna, fighterType); ..... } } // _createFighterBase() : 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); }
However numElements
mapping is never adjusted or incremented, rendering any call to numElements[i]
where i > 0
returning 0 , which is a problem if a generation is incremented using incrementGeneration():
function incrementGeneration(uint8 fighterType) external returns (uint8) { require(msg.sender == _ownerAddress); generation[fighterType] += 1; maxRerollsAllowed[fighterType] += 1; return generation[fighterType]; }
Because as soon as generation[fighterType]
is incremented, numElements[i]
where i > 0
returns 0
and dna % numElements[generation[fighterType]]
revert rendering the creation of new fighter when a generation of a fighterType is bigger than 1 not possible
And looking at fighterFarm.t.sol where generations are incremented it is a intended case to increment a fighter generation.
Add these tests to fighterFarm.t.sol and you will see they revert with FAIL. Reason: Division or modulo by 0
:
function testRevertWhenGenerationIncremented1() public { // 1. It works testClaimFighters(); _fighterFarmContract.incrementGeneration(0); _fighterFarmContract.incrementGeneration(1); // 2. It doesn't work anymore testClaimFighters(); } function testRevertWhenGenerationIncremented2() public { // 1. It works testRedeemMintPass(); _fighterFarmContract.incrementGeneration(0); _fighterFarmContract.incrementGeneration(1); // 2. It doesn't work anymore testRedeemMintPass(); }
VSCode
Set a value for numElements
when a generation is incremented
Context
#0 - c4-pre-sort
2024-02-22T18:37:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T18:37:36Z
raymondfam marked the issue as duplicate of #45
#2 - c4-judge
2024-03-07T07:01:12Z
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
29.1474 USDC - $29.15
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/Neuron.sol#L68
The Neuron contract's omission of the DEFAULT_ADMIN_ROLE setup, as recommended by OpenZeppelin's Access Control best practices, limits the contract's access control flexibility. This oversight restricts the ability to dynamically manage role-based permissions, complicating future updates or adjustments to role assignments
In the Neuron contract, roles such as MINTER_ROLE, SPENDER_ROLE, and STAKER_ROLE are utilized without establishing a DEFAULT_ADMIN_ROLE that has overarching control over these roles.
Usually, the DEFAULT_ADMIN_ROLE
is set to the contract deployer or an initial admin account during contract initialization to allow comprehensive role management:
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
The absence of this setup step means the contract lacks a clear administrative hierarchy, potentially leading to difficulties in role management and permission adjustments.
VSCode
set up DEFAULT_ADMIN_ROLE
in Neuron.sol constructor
Access Control
#0 - c4-pre-sort
2024-02-22T05:04:35Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T05:04:46Z
raymondfam marked the issue as duplicate of #20
#2 - c4-judge
2024-03-05T05:11:21Z
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#L214
The minting process in the described contract relies on a pseudo-random DNA generation mechanism that can be exploited by users, particularly when minting NFT fighters.
Due to the fact that _safeMint()
is used , a malicious user could use a contract to try to create a fighter using points from MergingPool or a signature, then sees which DNA the fighter he minted has and make the transaction revert if the DNA is not good enough for him/her by making
IERC721Receiver(to).onERC721Received
revert when _safeMint() is called on openzeppelin ERC721 contract.
Since the DNA, which significantly influences the NFT's rarity and value, is generated based on predictable parameters (msg.sender
, fighters.length
), users can compute them off-chain or intentionally revert transactions if the resulting NFT does not meet their desired rarity criteria. This vulnerability allows for an unfair advantage, where a user can manipulate the system to generate NFTs with potentially higher value without penalty, undermining the fairness and economic balance of the game.
The DNA for new fighters is generated on claimFighters()
and mintFromMergingPool
using:
uint256(keccak256(abi.encode(msg.sender, fighters.length)))
This process, combined with the fact that transactions can be reverted by a contract caller via the _checkOnERC721Received
function if not satisfied with the minted NFT's attributes, provides a loophole for gaming the system.
Malicious users could simply wait other users minting NFTs and retries to have better DNA for an NFT that will represent more value
VSCode
I don't really know how to mitigate as anyone could refuse an NFT, maybe prevent a contract from using your protocol if it's not 100% needed
Context
#0 - c4-pre-sort
2024-02-24T01:37:46Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:38:12Z
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:49:55Z
HickupHH3 marked the issue as satisfactory
#4 - c4-judge
2024-03-14T14:45:06Z
HickupHH3 marked the issue as not a duplicate
#5 - c4-judge
2024-03-14T14:45:31Z
HickupHH3 marked the issue as duplicate of #376
#6 - c4-judge
2024-03-16T10:37:53Z
HickupHH3 changed the severity to 2 (Med Risk)
#7 - c4-judge
2024-03-20T00:28:48Z
HickupHH3 marked the issue as duplicate of #1017
#8 - c4-judge
2024-03-22T04:21:02Z
HickupHH3 marked the issue as duplicate of #376
π Selected for report: klau5
Also found by: 0xAlix2, 0xCiphky, 0xDetermination, 0xG0P1, 0xMosh, 0xabhay, 14si2o_Flint, AlexCzm, Aymen0909, CodeWasp, DanielArmstrong, FloatingPragma, Giorgio, JCN, Jorgect, Kalogerone, KmanOfficial, Kow, KupiaSec, McToady, SpicyMeatball, VAD37, WoolCentaur, ZanyBonzy, alexxander, alexzoid, almurhasan, blutorque, csanuragjain, denzi_, dipp, djxploit, evmboi32, handsomegiraffe, haxatron, immeas, jesjupyter, ke1caM, klau5, lanrebayode77, lil_eth, merlin, merlinboii, nuthan2x, peanuts, shaflow2, shaka, sl1, solmaxis69, stakog, swizz, t0x1c, tallo, ubermensch, vnavascues, yotov721
0.5044 USDC - $0.50
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/StakeAtRisk.sol#L102-L104
The reclaimNRN
function's current logic in StakeAtRisk contract poses a risk of transaction reversion due to decrement operations on a potentially zero-valued variable. This flaw can block the update process for battle outcomes, preventing a user from a win in a battle => leading to a loss of points increment which represents funds so the impact is a loss of funds
Here is the reclaimNRN function which is called when a winner wins a battle :
function reclaimNRN(uint256 nrnToReclaim, uint256 fighterId, address fighterOwner) external { require(msg.sender == _rankedBattleAddress, "Call must be from RankedBattle contract"); require( stakeAtRisk[roundId][fighterId] >= nrnToReclaim, "Fighter does not have enough stake at risk" ); bool success = _neuronInstance.transfer(_rankedBattleAddress, nrnToReclaim); if (success) { stakeAtRisk[roundId][fighterId] -= nrnToReclaim; totalStakeAtRisk[roundId] -= nrnToReclaim; // @audit if fighter just transferred it revert amountLost[fighterOwner] -= nrnToReclaim; emit ReclaimedStake(fighterId, nrnToReclaim); } }
The process start with GameServer address that calls RankedBattle.sol which then calls StakeAtRisk.sol#ReclaimNRN()
in case curStakeAtRisk > 0
in RankedBattle.sol#addResultPoints()
:
/// RankedBattle.sol#addResultPoints() : if (curStakeAtRisk > 0) { _stakeAtRiskInstance.reclaimNRN(curStakeAtRisk, tokenId, fighterOwner); amountStaked[tokenId] += curStakeAtRisk; }
However having curStakeAtRisk > 0
doesn't indicate that amountLost[fighterOwner] > 0
, in case of a fighter has losen, unstake and transfer it's fighter NFT there is a possibility that :
stakeAtRisk[roundId][fighterId] > 0; totalStakeAtRisk[roundId] > 0; amountLost[fighterOwner] == 0;
Then if a new owner wins a battle within the same round it will revert. And it will happen most of the time a new owner buy a new fighter ID because in a game when you buy a new toy (here a fighter NFT), you wants to try it immediately
Add this test to rankedBattle.t.sol
function testWinnerRevert() public { address Bob = vm.addr(3); address Alice = vm.addr(4); // 1. Bob wins an NFT _mintFromMergingPool(Bob); _fundUserWith4kNeuronByTreasury(Bob); _fundUserWith4kNeuronByTreasury(Alice); vm.startPrank(Bob); _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, 0); vm.stopPrank(); // 2. Bob loses vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 0, 2, 1500, false); uint256 amountStakedLeft = 3_000 * 10 ** 18- - 3_000 * 10 ** 15; // 3. Bob is angry against the game => unstake and sell token to Alice vm.startPrank(Bob); _rankedBattleContract.unstakeNRN(amountStakedLeft,0); _fighterFarmContract.safeTransferFrom(Bob,Alice,0); vm.stopPrank(); // 4. Alice win a battle but call revert due to underflow vm.prank(address(_GAME_SERVER_ADDRESS)); vm.expectRevert(); _rankedBattleContract.updateBattleRecord(0, 0, 0, 1500, true); }
VSCode
Only decrement the mappings if amountLost[fighterOwner]
is > 0
Context
#0 - c4-pre-sort
2024-02-24T04:24:05Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T04:25:26Z
raymondfam marked the issue as duplicate of #1641
#2 - c4-judge
2024-03-12T03:34:04Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-12T04:01:25Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-12T04:03:31Z
HickupHH3 changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-03-12T06:57:12Z
HickupHH3 marked the issue as satisfactory
#6 - c4-judge
2024-03-13T09:58:39Z
HickupHH3 marked the issue as partial-50
π Selected for report: Timenov
Also found by: 0x11singh99, 0xblackskull, CodeWasp, MidgarAudits, MrPotatoMagic, Rolezn, Sabit, SovaSlava, andywer, btk, josephdara, lil_eth, merlinboii, sobieski, vnavascues
238.8948 USDC - $238.89
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L185
AI Arena protocol implements each time a function to give special privileges like in MergingPool.sol where the admin role can be set to true
or false
in an easy way by an owner :
function adjustAdminAccess(address adminAddress, bool access) external { require(msg.sender == _ownerAddress); isAdmin[adminAddress] = access; }
However in GameItems.sol there is a function lack to revoke burning address privileges which poses a security risk.
Because when an address is allowed to be a burningAddress
, it is an indefinitely access to burn game items without any way to revoke it, if the burning address is no longer trusted there is no way to revoke this right.
The contract includes setAllowedBurningAddresses
to grant burning privileges but lacks a corresponding mechanism to revoke these privileges, leading to a permanent, irrevocable trust in designated addresses :
function setAllowedBurningAddresses(address newBurningAddress) public { require(isAdmin[msg.sender]); allowedBurningAddresses[newBurningAddress] = true; }
VSCode
Add a parameter to setAllowedBurningAddresses
to allow admin to set or unset burning privileges, similar to adjustAdminAccess()
Access Control
#0 - c4-pre-sort
2024-02-22T19:26:49Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T19:26:57Z
raymondfam marked the issue as duplicate of #47
#2 - c4-judge
2024-03-08T03:28:17Z
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#L291
When created, GameItem tokens have a dailyAllowance
which represent how many tokens can be minted to a single user each day , however this daily allowance can be easily bypass by transferring these tokens to another wallet and then incrementing the number of tokens that can be mint each days and bypassing dailyAllowance which is a core metric of the protocol
function transferrableResetAllowance() public { address Alice = vm.addr(4); address Alice2 = vm.addr(3); // 1 Alice mints 10 Voltages _fundUserWith4kNeuronByTreasury(Alice); vm.startPrank(Alice); _gameItemsContract.adjustTransferability(0, true); _gameItemsContract.mint(0, 10); // Alice has no more allowance then cannot mint Voltage anymore assertEq(_gameItemsContract.getAllowanceRemaining(Alice, 0), 0); vm.expectRevert(); _gameItemsContract.mint(0, 1); _gameItemsContract.safeTransferFrom(Alice,Alice2,0,10,""); // 2. Alice transfer tokens and voltage to it's other address in order to mint more _neuronContract.transferFrom(Alice,Alice2, 3_000 * 10 ** 18); vm.stopPrank(); vm.startPrank(Alice2); // 3. Alice2 mints 10 Voltage and get 20 Voltages at all, bypassing the limit _gameItemsContract.mint(0, 10); assertEq(_gameItemsContract.getAllowanceRemaining(Alice2, 0), 0); assertEq(_gameItemsContract.balanceOf(Alice2, 0), 20); vm.stopPrank(); // 4. Alice2 can then retransfer these tokens to Alice and bypass the daily allowance }
VSCode
Refactor the daily allowance threshold to only allow a user to have a maximum balance of a particular game item could be a solution
Context
#0 - c4-pre-sort
2024-02-22T18:03:50Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T18:03:58Z
raymondfam marked the issue as duplicate of #43
#2 - c4-judge
2024-03-07T04:17:08Z
HickupHH3 marked the issue as satisfactory