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: 13/283
Findings: 9
Award: $330.82
π Selected for report: 0
π Solo Findings: 0
π Selected for report: CodeWasp
Also found by: 0x13, 0xAlix2, 0xAsen, 0xCiphky, 0xE1, 0xLogos, 0xaghas, 0xlemon, 0xlyov, 0xvj, ADM, Aamir, BARW, Bauchibred, DMoore, DanielArmstrong, Draiakoo, Fulum, GhK3Ndf, Josh4324, Kalogerone, KmanOfficial, Krace, KupiaSec, Limbooo, MidgarAudits, MrPotatoMagic, PedroZurdo, Pelz, Tendency, _eperezok, adam-idarrha, al88nsk, alexxander, alexzoid, aslanbek, blutorque, btk, c0pp3rscr3w3r, cartlex_, cats, d3e4, deadrxsezzz, denzi_, devblixt, dimulski, erosjohn, evmboi32, fnanni, grearlake, hulkvision, immeas, israeladelaja, jaydhales, jesjupyter, jnforja, juancito, klau5, korok, ktg, ladboy233, matejdb, merlinboii, novamanbg, nuthan2x, oualidpro, peanuts, petro_1912, pkqs90, pynschon, radev_sw, rouhsamad, sashik_eth, shaka, sobieski, soliditywala, stackachu, tallo, thank_you, ubl4nk, vnavascues, web3pwn, xchen1130, zhaojohnson
0.1044 USDC - $0.10
The FighterFarm.sol
which inherits from ERC721 has overridden the safeTransferFrom(from, to, tokenId)
and added requirements which lie in _ableToTransfer()
.
However, the ERC721 has another safeTransferFrom(from, to, tokenId, data)
which has not been overridden.
This means that a user can transfer their fighter and disregard the requirements inside of _ableToTransfer()
. A malicious user could therefore, circumvent the requirements of not being able to send more than 10 fighters to a user balanceOf(to) < MAX_FIGHTERS_ALLOWED
and being able to transfer a figher even though it is staked !fighterStaked[tokenId]
.
The following POC can be implemented in the FighterFarm.t.sol.
function test_TransferringFighterWhileStakedSucceeds() public { _mintFromMergingPool(_ownerAddress); _fighterFarmContract.addStaker(_ownerAddress); //Stake the fighter, which means that it should not be transferrable _fighterFarmContract.updateFighterStaking(0, true); assertEq(_fighterFarmContract.fighterStaked(0), true); // Reverts with the overridden transferFrom function vm.expectRevert(); _fighterFarmContract.transferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0); assertEq(_fighterFarmContract.ownerOf(0) != _DELEGATED_ADDRESS, true); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); // Succeeds with the safeTransferFrom function not overridden _fighterFarmContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0, ""); // The fighter is now owned by the delegated address despite it being staked assertEq(_fighterFarmContract.ownerOf(0), _DELEGATED_ADDRESS); assertEq(_fighterFarmContract.ownerOf(0) != _ownerAddress, true); }
Manual review
Override the safeTransferFrom(from, to, tokenId, data)
function and either add a revert inside of the overridden function or add the _ableToTransfer()
there.
Other
#0 - c4-pre-sort
2024-02-23T05:42:49Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T05:42:58Z
raymondfam marked the issue as duplicate of #739
#2 - c4-judge
2024-03-11T02:50:35Z
HickupHH3 marked the issue as satisfactory
π 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
When creating an item, the admin can decide to make the item non-transferable by setting transferable = false. This requirement is checked in the overridden function safeTransferFrom()
in GameItems.sol
through require(allGameItemAttributes[tokenId].transferable
). However, the contract have not overridden the ERC1155 function safeBatchTransferFrom()
and implemented the same restriction.
A malicious user could transfer an item that is supposed to be non-transferable, and gain an unfair competitive advantage.
The following POC can be tested by putting the function in the GameItems.t.sol
file.
function test_transferNonTransferable() public { _fundUserWith4kNeuronByTreasury(_ownerAddress); _gameItemsContract.mint(0, 1); // Item adjusted to not be transferable _gameItemsContract.adjustTransferability(0, false); // The safeTransferFrom should fail because the game item is not transferable. vm.expectRevert(); _gameItemsContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0, 1, ""); // The safeBatchTransferFrom will succeed, since it is not being overwritten uint256[] memory ids = new uint256[](1); uint256[] memory amounts = new uint256[](1); ids[0] = 0; amounts[0] = 1; _gameItemsContract.safeBatchTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, ids, amounts, ""); // The _DELEGATED_ADDRESS is now in possession of the game item and not the _ownerAddress assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, 0), 1); assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 0); }
Manual review
Override safeBatchTransferFrom()
and add require(allGameItemAttributes[tokenId].transferable
) such as below:
function safeBatchTransferFrom( address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data ) public override(ERC1155) { require(allGameItemAttributes[tokenId].transferable); super.safeBatchTransferFrom(from, to, ids, amounts, data); }
Other
#0 - c4-pre-sort
2024-02-22T04:23:32Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T04:23:39Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:29:23Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:56:52Z
HickupHH3 marked the issue as satisfactory
π Selected for report: t0x1c
Also found by: 0rpse, 0xAadi, 0xBinChook, 0xCiphky, 0xDetermination, 14si2o_Flint, AC000123, Aamir, Abdessamed, Blank_Space, CodeWasp, DanielArmstrong, DarkTower, Draiakoo, Honour, Kalogerone, Krace, McToady, Merulez99, MidgarAudits, MrPotatoMagic, PedroZurdo, Silvermist, Tychai0s, VAD37, Velislav4o, VrONTg, WoolCentaur, YouCrossTheLineAlfie, ZanyBonzy, alexxander, aslanbek, btk, csanuragjain, d3e4, dimulski, djxploit, erosjohn, evmboi32, fnanni, forgebyola, forkforkdog, handsomegiraffe, immeas, israeladelaja, juancito, ktg, n0kto, neocrao, ni8mare, okolicodes, peanuts, petro_1912, shaflow2, shaka, swizz, ubermensch, ubl4nk, yotov721
2.0593 USDC - $2.06
Within the function updateBattlerRecord()
, there is an internal call to _addResultPoints()
, which calculates curStakeAtRisk
. However, curStakeAtRisk
is rounded down, potentially allowing a player with no points remaining to unstake until they have only 999 NRNs staked. This enables the player to avoid risking their NRNs if they lose a match, allowing them to recover points in a 'risk-free' manner, as no NRNs will be put at risk if they lose.
Such strategies decrease the amount of NRNs sent to StakeAtRisk.sol
and subsequently the amount sent to the treasury when a new round is initiated.
Add the following test to RankedBattle.t.sol
:
function testRoundingDownInFavorOfPlayer() public { address player = vm.addr(3); _mintFromMergingPool(player); _fundUserWith4kNeuronByTreasury(player); //Stake 999 NRNs with 0 points vm.prank(player); _rankedBattleContract.stakeNRN(999, 0); emit log_uint(_rankedBattleContract.accumulatedPointsPerAddress(player, 0)); assertEq(_rankedBattleContract.amountStaked(0), 999); // player lost the match with 0 accumulated points vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); // 0 NRNs placed at risk assertEq(_rankedBattleContract.amountStaked(0), 999); }
Manual review
The rounding down is in favor of the user if he lost a match, however it should always be in favor of the system. Make sure curStakeAtRisk
is never 0 if the player lost the match by adding the following check right after else if (battleResult == 2)
L472:
if (curStakeAtRisk == 0) { curStakeAtRisk = 1; }
Math
#0 - c4-pre-sort
2024-02-22T17:09:48Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T17:09:59Z
raymondfam marked the issue as duplicate of #38
#2 - c4-judge
2024-03-07T02:58:21Z
HickupHH3 changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-07T03:37:18Z
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
In the FighterFarm.sol
contract the numElements[0]
is set to 3 in the constructor (representing the 3 elements in the game). However, whenever the owner decides to call incrementGeneration(fighterType)
, users that try to create a new fighter of that generation with customAttributes (e.g. customAttributes[0] == 100
) will revert in _createFighterBase
. The same will happen if a user decides to try and reRoll()
an existing fighter to a new fighter of that generation. The revert will happen in this line:
uint256 element = dna % numElements[generation[fighterType]];
Since numElements only have a mapping from 0 (numElements[0] = 3
). The new incremented generation will try to access numElements[1]
which will default to 0, thus causing a division by 0 issue.
The impact is severe since this means that users will not be able to create or reRoll to fighters of the incremented generation with customAttributes.
_createNewFighter() fails
function test_createNewFighterInNewGenerationReverts() public { // Mint a fighter successfully in generation 0 vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(_ownerAddress, "_neuralNetHash", "original", [uint256(100), uint256(100)]); assertEq(_fighterFarmContract.balanceOf(_ownerAddress), 1); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); // Increment AI champion to generation 1 and try to mint it with custom attributes _fighterFarmContract.incrementGeneration(0); vm.prank(address(_mergingPoolContract)); vm.expectRevert(); _fighterFarmContract.mintFromMergingPool(_ownerAddress, "_neuralNetHash", "original", [uint256(100), uint256(100)]); }
reRoll() fails
function test_reRollNewGenerationReverts() public { uint8 tokenId = 0; // fund user with 4k NRN for reroll _fundUserWith4kNeuronByTreasury(_ownerAddress); // Mint a fighter successfully in generation 0 vm.prank(address(_mergingPoolContract)); _fighterFarmContract.mintFromMergingPool(_ownerAddress, "_neuralNetHash", "original", [uint256(1), uint256(80)]); assertEq(_fighterFarmContract.balanceOf(_ownerAddress), 1); assertEq(_fighterFarmContract.ownerOf(tokenId), _ownerAddress); // Increment AI champion to generation 1 _fighterFarmContract.incrementGeneration(0); _neuronContract.addSpender(address(_fighterFarmContract)); // reRoll fails vm.expectRevert(); _fighterFarmContract.reRoll(tokenId, 0); }
Manual review
When incrementing the generation - make sure to create a mapping for numElements as well.
+ function incrementGeneration(uint8 fighterType, uint8 _num) external returns (uint8) { require(msg.sender == _ownerAddress); generation[fighterType] += 1; maxRerollsAllowed[fighterType] += 1; + numElements[fighterType] = _num return generation[fighterType]; }
Math
#0 - c4-pre-sort
2024-02-22T19:02:22Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T19:02:32Z
raymondfam marked the issue as duplicate of #45
#2 - c4-judge
2024-03-08T03:17:09Z
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
Neuron.sol
implements the AccessControl.sol
from OpenZeppelin and utilizes the lib to set up the MINTER_ROLE
, STAKER_ROLE
, and SPENDER_ROLE
for different contracts. However, there's currently no way to revoke these roles nor grant new roles. The reason being that the DEFAULT_ADMIN_ROLE
is never set.
In case, the addresses do get compromised this means that a malicious user could call spend, stake, and most critically mint unlimited NRN.
Manual review
Set the contract deployer as the DEFAULT_ADMIN_ROLE
such as below:
constructor() { ... _grantRole(DEFAULT_ADMIN_ROLE, msg.sender) }
Access Control
#0 - c4-pre-sort
2024-02-22T05:09:57Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T05:10:06Z
raymondfam marked the issue as duplicate of #20
#2 - c4-judge
2024-03-05T07:35:41Z
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
In the FighterFarm.sol
contract the ownerAddress can add a new address to the hasStakerRole
mapping. The addresses that are added to this mapping can then call the updateFighterStaking()
function to update the fighter staking status.
However, if the account for that address becomes compromised there's currently no way to revoke the access - meaning that a malicious user could arbitrarily update the staking status of fighters which could lead to unforseen consequences (e.g. unlocking a fighter even though NRN are being staked and so on).
Manual review
Consider changing the function to allow for a boolean such as :
function addStaker(address newStaker, bool allowed) external { require(msg.sender == _ownerAddress); hasStakerRole[newStaker] = allowed; }
This follows the same pattern as adjustAllowedVoltageSpenders
.
Access Control
#0 - c4-pre-sort
2024-02-24T06:23:40Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T06:23:48Z
raymondfam marked the issue as duplicate of #20
#2 - c4-judge
2024-03-05T10:02:31Z
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
In the functions claimRewards()
and claimNRN()
the user can batch claim their rewards in the MergingPool.sol
and RankedBattle.sol
respectively. However, there are no customizeable upper bound to the for-loops. Right now the loop goes from the numRoundsClaimed
to the current roundId.
function setUpPickWinners(uint256 rounds) internal { // Set the winners to two, since that is required per period _mintFromMergingPool(_ownerAddress); _mintFromMergingPool(_DELEGATED_ADDRESS); uint256[] memory _winners = new uint256[](2); _winners[0] = 0; _winners[1] = 1; // Winners are picked for each round for(uint256 i = 0; i < rounds; i++) { _mergingPoolContract.pickWinner(_winners); } } function test_ClaimRewardsRunsOutofGas() public { // Pick the winners isolated from the claimRewards function uint256 rounds = 10; setUpPickWinners(rounds); // Winners preparation to claim rewards string[] memory _modelURIs = new string[](rounds); string[] memory _modelTypes = new string[](rounds); uint256[2][] memory _customAttributes = new uint256[2][](rounds); for(uint256 i = 0; i < rounds; i++) { _modelURIs[i] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; _modelTypes[i] = "original"; _customAttributes[i] = [uint256(1), uint256(80)]; } // winner tries claims rewards but runs out of gas vm.expectRevert(); _mergingPoolContract.claimRewards(_modelURIs, _modelTypes, _customAttributes); }
Manual review
Add a parameter to decide the upper bound for the loop such as below.
function claimRewards( string[] calldata modelURIs, string[] calldata modelTypes, - uint256[2][] calldata customAttributes + uint256[2][] calldata customAttributes, + uint256 _roundId ) external { + require(_roundId <= roundId, "_roundId cannot be higher than roundId"); uint256 winnersLength; uint32 claimIndex = 0; - uint32 lowerBound = numRoundsClaimed[msg.sender]; - for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) { + uint32 lowerBound = numRoundsClaimed[msg.sender]; + for (uint32 currentRound = lowerBound; currentRound < _roundId; currentRound++) { ... } }
Loop
#0 - c4-pre-sort
2024-02-24T00:02:01Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T00:02:09Z
raymondfam marked the issue as duplicate of #1541
#2 - c4-judge
2024-03-11T13:01:30Z
HickupHH3 marked the issue as duplicate of #216
#3 - c4-judge
2024-03-12T02:36:08Z
HickupHH3 marked the issue as partial-50
#4 - c4-judge
2024-03-21T02:57:55Z
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
In the FighterFarm.sol contract, the reRoll()
function allows the user to re-roll their fighter with new attributes up until maxRerollsAllowed
(which is currently set at three times). The new dna are based on the msg.sender
, tokenId
and numRerolls[tokenId]
parameters as shown below:
uint256 dna = uint256(keccak256(abi.encode(msg.sender, tokenId, numRerolls[tokenId])));
This means that a user can simulate the new dna off-chain (for example in Remix) since both the msg.sender
and numRerolls[tokenId]
relies on user input.
This can be abused by the user in order to try and create a rare NFT in a certain and deterministic way. Below is a step-by-step way to do it:
1- Set up an off-chain environment with Remix and implement the functions in order to simulate the dna and thus the rarity of your fighter.
2- Start by calling reRoll() with your fighter and EOA address. Do that three (3) times and see if you get a good fighter.
3- If you didn't get a fighter to your liking, create a new EOA and send the fighter over to this EOA together with some NRN.
4- Repeat step 2 and step 3 until you get a rare fighter.
5- Now, use that specific EOA together with the number of re-rolls required and execute it on-chain to get your rare fighter.
Manual review
Use off-chain randomization for the fighter DNA, similar to how the redeemMintPass()
function does it with the param mintPassDnas
.
Other
#0 - c4-pre-sort
2024-02-24T01:51:15Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:51:23Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:52:10Z
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:40Z
HickupHH3 marked the issue as duplicate of #376
π 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
In the GameItems.sol
contracts the admin can add a new address to the setAllowedBurningAddresses
mapping. The addresses that are added to this mapping can then call the burn()
function to burn certain items from accounts.
However, if the account for that address becomes compromised there's currently no way to revoke the access - meaning that a malicious user could arbitrarily burn game items from all the accounts (e.g. griefing a user).
Manual review
Consider changing the function to allow for a boolean such as :
function setAllowedBurningAddresses(address newBurningAddress, bool allowed) public { require(isAdmin[msg.sender]); allowedBurningAddresses[newBurningAddress] = allowed; //@audit no way to rescind burning addresses? }
This would follow the same pattern as the function adjustAllowedVoltageSpenders
.
Access Control
#0 - c4-pre-sort
2024-02-22T19:30:52Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T19:30:59Z
raymondfam marked the issue as duplicate of #47
#2 - c4-judge
2024-03-08T03:30:22Z
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
If a player has spent their daily allowance, they can easily bypass it by minting from another EOA they control and transferring the token to their initial address. A player would be able to cheat in the game, for example if the GameItem is a healing potion capped at 1 potion per day, the player would be allowed to take far more than 1 potion and wins all the matches which gives him a huge advantage on other players.
Add the following lines at the end of the setup()
function of RankedBattle.t.sol
:
_neuronContract.addSpender(address(_gameItemsContract)); _gameItemsContract.instantiateNeuronContract(address(_neuronContract)); _gameItemsContract.createGameItem("Battery", "https://ipfs.io/ipfs/", true, true, 10_000, 1 * 10 ** 18, 10); _gameItemsContract.setAllowedBurningAddresses(address(_voltageManagerContract));
Add the following test to RankedBattle.t.sol
:
function testBypassDailyAllowance() public { address player = vm.addr(3); address newEoa = vm.addr(4); _mintFromMergingPool(player); assertEq(_gameItemsContract.getAllowanceRemaining(player, 0), 10); _fundUserWith4kNeuronByTreasury(player); _fundUserWith4kNeuronByTreasury(newEoa); vm.prank(player); _gameItemsContract.mint(0, 10); assertEq(_gameItemsContract.getAllowanceRemaining(player, 0), 0); // transfer fighter to newEoa vm.prank(player); _fighterFarmContract.safeTransferFrom(player, newEoa, 0); // check if newEoa has daily allowance assertEq(_gameItemsContract.getAllowanceRemaining(newEoa, 0), 10); // mint more gameItems vm.prank(newEoa); _gameItemsContract.mint(0, 10); assertEq(_gameItemsContract.getAllowanceRemaining(newEoa, 0), 0); // send gameItems back to player vm.prank(newEoa); _gameItemsContract.safeTransferFrom(newEoa, player, 0, 10, ""); uint256 balanceOfItems = _gameItemsContract.balanceOf(player, 0); assertEq(balanceOfItems, 20); }
Now the player has more than the daily allowances.
Manual review
Checking that the from
address holds an NFT in safeTransferFrom()
then lock the fighter to make it non transferable if allowanceRemaining = 0 would make such practices less feasible:
safeTransferFrom()
require(FighterFarm.balanceOf(from) > 0); if (allowanceRemaining[msg.sender][tokenId] == 0) { FighterFarm.lockFighter();}
safeTransferFrom()
require(!fighterLocked[tokenId]);
Add a function to lock fighters:function lockFighter(uint256 tokenId, bool action) external { require(msg.sender == _gameItemsInstance); fighterLocked[tokenId] = action; }
Other
#0 - c4-pre-sort
2024-02-25T08:58:13Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-25T08:58:20Z
raymondfam marked the issue as duplicate of #43
#2 - c4-judge
2024-03-07T06:32:37Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-07T06:36:53Z
HickupHH3 changed the severity to 2 (Med Risk)