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: 114/283
Findings: 4
Award: $30.36
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: Abdessamed
Also found by: 0rpse, 0xAlix2, 0xAsen, 0xCiphky, 0xlemon, 0xmystery, 0xvj, ADM, Aamir, Archime, BARW, DarkTower, Draiakoo, FloatingPragma, JCN, McToady, MrPotatoMagic, OMEN, PetarTolev, Ryonen, SpicyMeatball, Tendency, VAD37, Velislav4o, VrONTg, Zac, adamn000, ahmedaghadi, alexxander, alexzoid, bhilare_, btk, cats, d3e4, denzi_, devblixt, dimulski, evmboi32, fnanni, givn, haxatron, immeas, jesjupyter, juancito, ke1caM, klau5, korok, krikolkk, matejdb, n0kto, niser93, peter, pkqs90, radin100, shaka, sl1, soliditywala, stackachu, stakog, t0x1c, vnavascues, yotov721, zhaojohnson
0.6333 USDC - $0.63
Judge has assessed an item in Issue #1931 as 3 risk. The relevant finding follows:
[L-07] The user can choose fightersโ attributes in their favor
#0 - c4-judge
2024-03-06T03:37:34Z
HickupHH3 marked the issue as duplicate of #366
#1 - c4-judge
2024-03-06T03:37:40Z
HickupHH3 marked the issue as partial-50
๐ 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
Judge has assessed an item in Issue #1931 as 2 risk. The relevant finding follows:
[L-01] Staker Role Cannot Be Revoked
#0 - c4-judge
2024-03-18T05:07:12Z
HickupHH3 marked the issue as duplicate of #1507
#1 - c4-judge
2024-03-18T05:07:17Z
HickupHH3 marked the issue as partial-25
๐ 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
ID | Finding |
---|---|
[L-01] | Staker Role Cannot Be Revoked |
[L-02] | getFighterPoints only returns the points for figter with id 1 |
[L-03] | The Neuron Contract Uses a Deprecated AccessControl Function |
[L-04] | Unstaked transferred fighter is unusable in the same roundId |
[L-05] | Loss of NRNs on token transfer failure in RankedBattle.unstakeNRN |
[L-06] | Possible Cross-Chain Replay Attack in FighterFarm.claimFighters |
[L-07] | The user can choose fighters' attributes in their favor |
ID | Finding |
---|---|
[NC-01] | Implement _ableToTransfer in the ERC20._beforeTokenTransfer |
[NC-02] | Use OpenZeppelin's AccessControl for Role Management |
[NC-03] | Use OpenZeppelin's Ownable for Ownership Management |
[NC-04] | In Neuron Contract Utilize AccessControl Functionality |
[NC-05] | In Neuron Contract, Inherit OpenZeppelin's ERC20Burnable |
[NC-06] | Duplicated Validations Should Be Refactored to a Function |
In FighterFarm
, the staker role has permission to lock/unlock fighters, determining their transferability.
function updateFighterStaking(uint256 tokenId, bool stakingStatus) external { require(hasStakerRole[msg.sender]); fighterStaked[tokenId] = stakingStatus; ... }
function _ableToTransfer(uint256 tokenId, address to) private view returns(bool) { return ( _isApprovedOrOwner(msg.sender, tokenId) && balanceOf(to) < MAX_FIGHTERS_ALLOWED && !fighterStaked[tokenId] ); }
However, if a staker behaves maliciously, the role cannot be revoked.
function addStaker(address newStaker, bool access) external { require(msg.sender == _ownerAddress); - hasStakerRole[newStaker] = true; + hasStakerRole[newStaker] = access; }
getFighterPoints
only returns the points for figter with id 1MergingPool.getFighterPoints function should return the points for the fighters from 0 to maxId
. However, the points array returned from the function is initialized with length of 1, thus the function always reverts for maxId
> 1.
According to the NatSpec, it should return an aggregated points array.
/// @notice Retrieves the points for multiple fighters up to the specified maximum token ID.
function getFighterPoints(uint256 maxId) public view returns(uint256[] memory) { - uint256[] memory points = new uint256[](1); + uint256[] memory points = new uint256[](maxId); ... }
Neuron
Contract Uses a Deprecated AccessControl
FunctionIn Neuron
, the functions addMinter, addStaker, addSpender use the deprecated _setupRole
function.
Instead of _setupRole
, use _grantRole
.
When a user unstakes their NRNs and then decides to sell/transfer their fighter to another user, they must wait until the next round to restake and enter the ranked battle.
POC:
unstakeNRN sets hasUnstaked
to true.
stakeNRN check only if tokenId
and roundId
have been unstaked.
require(hasUnstaked[tokenId][roundId] == false, "Cannot add stake after unstaking this round");
Consider revising the logic to allow newly transferred fighters to participate in the same round.
RankedBattle.unstakeNRN
If the NRN transfer fails, the user will lose their NRNs.
function unstakeNRN(uint256 amount, uint256 tokenId) external { ... bool success = _neuronInstance.transfer(msg.sender, amount); + require(success, "Transfer failed."); - if (success) { if (amountStaked[tokenId] == 0) { _fighterFarmInstance.updateFighterStaking(tokenId, false); } emit Unstaked(msg.sender, amount); - } }
FighterFarm.claimFighters
The FighterFarm.claimFighters function does not use any nonce or chainId in the msgHash
calculation.
Utilize EIP-155 to include chainId in the msgHash
calculation.
The user can pre-calculate their fighter's attributes in advance and even select the desired ones.
The fighter's base is calculated based on the dna
, which is provided as input by the user in redeemMintPass and then passed to the _createFighterBase
method. This allows every user to control which fighter they will receive.
_ableToTransfer
in the ERC20._beforeTokenTransfer
Move the _ableToTransfer
check to the _beforeTokenTransfer
hook in FighterFarm
.
AccessControl
for Role ManagementReplace the current ownership logic in the contracts with OpenZeppelin's AccessControl. This will help reduce the contracts' complexity and size. Remove transferOwnership
, adjustAdminAccess
, _ownerAddress
, and isAdmin
.
Instances: FighterFarm, GameItems, MergingPool, RankedBattle, VoltageManager.
Ownable
for Ownership ManagementReplace the current ownership logic in the AiArenaHelper
with OpenZeppelin's Ownable
. This will help reduce the contract's complexity and size.
Neuron
Contract Utilize AccessControl
FunctionalityhasRole
checks with onlyRole
modifiers._ownerAddress
as DEFAULT_ADMIN_ROLE
.isAdmin
mapping, instead grant admins the ADMIN_ROLE
.This will decrease the contract's size and complexity.
Neuron
Contract, Inherit OpenZeppelin's ERC20Burnable
To decrease the Neuron
contract's size and complexity, inherit from ERC20Burnable
and remove the burn and burnFrom functions.
Consider the refactoring of duplicated replenish time validations in GameItems
.
Recommendations:
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 || + isReplenishTimePassed(msg.sender, tokenId) || 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) { + if (isReplenishTimePassed(msg.sender, tokenId)) { _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); } } function getAllowanceRemaining(address owner, uint256 tokenId) public view returns (uint256) { uint256 remaining = allowanceRemaining [owner][tokenId]; - if (dailyAllowanceReplenishTime[owner][tokenId] <= block.timestamp) { + if (isReplenishTimePassed(owner, tokenId)) { remaining = allGameItemAttributes[tokenId].dailyAllowance; } return remaining; } + function isReplenishTimePassed(address owner, uint256 tokenId) internal view returns(bool) { + return dailyAllowanceReplenishTime[owner][tokenId] <= block.timestamp; + }
#0 - raymondfam
2024-02-26T03:23:21Z
L-02 to #48 L-04 to #1641 L-07 to #1626
#1 - c4-pre-sort
2024-02-26T03:23:26Z
raymondfam marked the issue as sufficient quality report
#2 - HickupHH3
2024-03-18T05:09:39Z
L-1: upgraded L-2: L L-3: R L-4: L L-5: invalid L-6: R L-7: upgraded NC-01: NC NC-02,3,4: R NC-5: R NC-6: R/NC
#3 - c4-judge
2024-03-18T05:09:45Z
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
ID | Finding |
---|---|
[G-01] | Redundant attributeProbabilities Initialization in the AiArenaHelper Constructor |
[G-02] | Utilize Cached Variable in Require Statement Checks |
[G-03] | Use _spendAllowance in Neuron.burnFrom |
[G-04] | Omit Redundant Allowance Check in Neuron.claim |
[G-05] | Cache Array Length in Loops |
[G-06] | Reuse Cached Variables |
[G-07] | Avoid Modifying Storage Variables with Zero |
attributeProbabilities
Initialization in the AiArenaHelper
ConstructorThe deployment cost is unnecessarily increased due to the probabilities
being set twice in the attributeProbabilities
mapping within the constructor: initially through the addAttributeProbabilities function and then within a for loop. Therefore, it's advised to eliminate the addAttributeProbabilities
call from the constructor.
Additionally, incorporate the require(probabilities.length == 6, "Invalid number of attribute arrays");
statement in the constructor for validation.
In the AiArenaHelper.addAttributeDivisor
, utilize the cached attributesLength for the require
statement check to decrease gas costs.
In the MergingPool.pickWinner
, utilize the cached winnersLength for the require
statement check to decrease gas costs.
_spendAllowance
in Neuron.burnFrom
Utilizing ERC20._spendAllowance
can reduce gas costs due to its efficiency.
function burnFrom(address account, uint256 amount) public virtual { - require( - allowance(account, msg.sender) >= amount, - "ERC20: burn amount exceeds allowance" - ); - uint256 decreasedAllowance = allowance(account, msg.sender) - amount; + _spendAllowance(account, msg.sender, amount); _burn(account, amount); - _approve(account, msg.sender, decreasedAllowance); }
Neuron.claim
The allowance check is already performed within ERC20.transferFrom
-> ERC20._spendAllowance
, making it unnecessary.
function _spendAllowance( address owner, address spender, uint256 amount ) internal virtual { uint256 currentAllowance = allowance(owner, spender); if (currentAllowance != type(uint256).max) { require(currentAllowance >= amount, "ERC20: insufficient allowance"); unchecked { _approve(owner, spender, currentAllowance - amount); } } }
In FighterFarm.redeemMintPass, caching mintpassIdsToBurn.length
will decrease gas costs.
function redeemMintPass( uint256[] calldata mintpassIdsToBurn, uint8[] calldata fighterTypes, uint8[] calldata iconsTypes, string[] calldata mintPassDnas, string[] calldata modelHashes, string[] calldata modelTypes ) external { + uint256 mintpassIdsToBurnLength = mintpassIdsToBurn.length; require( - mintpassIdsToBurn.length == mintPassDnas.length && + mintpassIdsToBurnLength == mintPassDnas.length && mintPassDnas.length == fighterTypes.length && fighterTypes.length == modelHashes.length && modelHashes.length == modelTypes.length ); - for (uint16 i = 0; i < mintpassIdsToBurn.length; i++) { + for (uint16 i = 0; i < mintpassIdsToBurnLength; i++) { require(msg.sender == _mintpassInstance.ownerOf(mintpassIdsToBurn[i])); _mintpassInstance.burn(mintpassIdsToBurn[i]); _createNewFighter( msg.sender, uint256(keccak256(abi.encode(mintPassDnas[i]))), modelHashes[i], modelTypes[i], fighterTypes[i], iconsTypes[i], [uint256(100), uint256(100)] ); } }
In RankedBattle.updateBattleRecord, a variable stakeAtRisk for the specified tokenId
is already cached. However, _addResultPoints
also makes an external call to stakeAtRiskInstance
. To decrease gas costs, pass the stakeAtRisk
as a parameter to _addResultPoints
.
function updateBattleRecord( uint256 tokenId, uint256 mergingPortion, uint8 battleResult, uint256 eloFactor, bool initiatorBool ) external { ... uint256 stakeAtRisk = _stakeAtRiskInstance.getStakeAtRisk(tokenId); if (amountStaked[tokenId] + stakeAtRisk > 0) { - _addResultPoints(battleResult, tokenId, eloFactor, mergingPortion, fighterOwner); + _addResultPoints(battleResult, tokenId, eloFactor, mergingPortion, fighterOwner, stakeAtRisk); } ... }
Not modifying storage variables with a zero value saves gas.
Recommendation:
Update points after points > 0
check.
function _addResultPoints( uint8 battleResult, uint256 tokenId, uint256 eloFactor, uint256 mergingPortion, address fighterOwner ) private { ... if (battleResult == 0) { ... /// Add points to the fighter for this round - accumulatedPointsPerFighter[tokenId][roundId] += points; - accumulatedPointsPerAddress[fighterOwner][roundId] += points; - totalAccumulatedPoints[roundId] += points; if (points > 0) { + accumulatedPointsPerFighter[tokenId][roundId] += points; + accumulatedPointsPerAddress[fighterOwner][roundId] += points; + totalAccumulatedPoints[roundId] += points; emit PointsChanged(tokenId, points, true); } } else if (battleResult == 2) { ... if (accumulatedPointsPerFighter[tokenId][roundId] > 0) { ... - accumulatedPointsPerFighter[tokenId][roundId] -= points; - accumulatedPointsPerAddress[fighterOwner][roundId] -= points; - totalAccumulatedPoints[roundId] -= points; if (points > 0) { + accumulatedPointsPerFighter[tokenId][roundId] -= points; + accumulatedPointsPerAddress[fighterOwner][roundId] -= points; + totalAccumulatedPoints[roundId] -= points; emit PointsChanged(tokenId, points, false); } } ... } }
#0 - raymondfam
2024-02-25T21:01:58Z
G5, G6: bot 5G
#1 - c4-pre-sort
2024-02-25T21:02:07Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2024-03-19T07:57:02Z
HickupHH3 marked the issue as grade-b