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: 196/283
Findings: 5
Award: $3.35
π 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
While staking NRN tokens along with fighter NFT, an external call sent to the FighterFarm contract to lock the user fighter NFT so that its cannot be transferred until the user unstake.
File: FighterFarm.sol
function updateFighterStaking(uint256 tokenId, bool stakingStatus) external { require(hasStakerRole[msg.sender]); fighterStaked[tokenId] = stakingStatus; if (stakingStatus) { emit Locked(tokenId); } else { emit Unlocked(tokenId); } }
So if the tokenId is locked and user try to transfer it, the _ableToTransfer
will revert the call. This validation is applied to following transfer functions,
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L338`safe
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L355
However, it failed to do so for another transfer function of ERC721 which accept additional data
parameter.
function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory data ) public virtual override { require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved"); _safeTransfer(from, to, tokenId, data); }
An attacker can use his fighter NFT simultaneously to participate in multiple battle, multiplying their rewards.
Add following file to test/FighterFarm.t.sol, and
Run forge test --mt testTransferLockedNFT
function testTransferLockedNFT() public { address aliceAccount01 = makeAddr("pppppp"); address aliceAccount02 = makeAddr("dddddd"); _mintFromMergingPool(aliceAccount01); assertEq(_fighterFarmContract.ownerOf(0), aliceAccount01); _fighterFarmContract.addStaker(_ownerAddress); _fighterFarmContract.updateFighterStaking(0, true); assertEq(_fighterFarmContract.fighterStaked(0), true); vm.prank(aliceAccount01); _fighterFarmContract.safeTransferFrom(aliceAccount01, aliceAccount02, 0, ""); assertEq(_fighterFarmContract.ownerOf(0), aliceAccount02); assertEq(_fighterFarmContract.balanceOf(aliceAccount01), 0); }
Manual reviiew
Add _ableToTransfer
check,
function safeTransferFrom( address from, address to, uint256 tokenId, bytes memory data ) public override(ERC721, IERC721) { require(_ableToTransfer(tokenId, to)); _safeTransfer(from, to, tokenId, data); }
ERC721
#0 - c4-pre-sort
2024-02-23T04:26:08Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-23T04:26:16Z
raymondfam marked the issue as duplicate of #54
#2 - c4-pre-sort
2024-02-23T04:47:12Z
raymondfam marked the issue as duplicate of #739
#3 - c4-pre-sort
2024-02-23T04:51:14Z
raymondfam marked the issue as sufficient quality report
#4 - c4-judge
2024-03-11T02:36:37Z
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
GameItems follows EIP1155 standard. While createGameItem
, tokens can be set non-transferable by setting transferable
to false
. The transfer function such as safeTransferFrom
validate the following check to transfer only when its allowed,
function safeTransferFrom( address from, address to, uint256 tokenId, uint256 amount, bytes memory data ) public override(ERC1155) { require(allGameItemAttributes[tokenId].transferable); // @audit super.safeTransferFrom(from, to, tokenId, amount, data); }
However, its fail to put that check over safeBatchTransferFrom
function, which defined over ERC1155.sol. Hence, anyone can transfer the game items even they are set freeze.
Add file to test/GameItems.t.sol and run forge test --mt testGameItemsTransferViaBatch
function testGameItemsTransferViaBatch() public { _fundUserWith4kNeuronByTreasury(_ownerAddress); _gameItemsContract.mint(0, 5); assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 5); // item set non-transferable, for test purpose _gameItemsContract.adjustTransferability(0, false); uint256[] memory ids = new uint256[](1); ids[0] = 0; uint256[] memory amounts = new uint256[](1); amounts[0] = 5; _gameItemsContract.safeBatchTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, ids, amounts, ""); assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, 0), 5); assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 0); }
Manual review
Add following check for safeBatchTransferFrom
also,
require(allGameItemAttributes[tokenId].transferable);
Invalid Validation
#0 - c4-pre-sort
2024-02-22T03:52:52Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T03:52:58Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:27:58Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:53:20Z
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
A player can reroll
with fighterType parameter other than the one they own, allowing them to roll to maximum allowed on fighterType.
Default state:
uint8[2] public maxRerollsAllowed = [3, 3]; uint8[2] public generation = [0, 0];
Say generation for fighterType Champion
and Dendroid
are sets 0 and 2 respectively, where maxRerollsAllowed
has changed to 3 and 5.
function incrementGeneration(uint8 fighterType) external returns (uint8) { require(msg.sender == _ownerAddress); generation[fighterType] += 1; maxRerollsAllowed[fighterType] += 1; return generation[fighterType]; }
If a Alice own Champion, she is expected to reRoll
upto 3 times, since numRerolls[tokenId]==0
and maxRerollsAllowed[0]==3
, but here she can upto 5 times by passing Dendroid(1) fighterType in reRoll which she does not own.
Allowing her to call reRoll
more than the expected allowed times, increases her chance of getting more rare fighter NFT.
https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L372 https://github.com/code-423n4/2024-02-ai-arena/blob/cd1a0e6d1b40168657d1aaee8223dc050e15f8cc/src/FighterFarm.sol#L129-L134
Manual
Change code to below,
function reRoll(uint8 tokenId) public { require(msg.sender == ownerOf(tokenId)); uint8 fighterType = fighters[tokenId].dendroidBool ? 1 : 0; require(numRerolls[tokenId] < maxRerollsAllowed[fighterType]); ...SNIP... }
Other
#0 - c4-pre-sort
2024-02-22T01:20:11Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T01:20:17Z
raymondfam marked the issue as duplicate of #306
#2 - c4-judge
2024-03-05T04:31:51Z
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
FighterFarm::reRoll() allows players to roll a fighter with random traits, the number of times a player can do depends on maxRerollsAllowed[fighterType]
and the number of times they already reRolled. Maximum reRolls can be extend if the generation increases, which allows fighter to roll to own higher generation traits.
function incrementGeneration(uint8 fighterType) external returns (uint8) { require(msg.sender == _ownerAddress); generation[fighterType] += 1; maxRerollsAllowed[fighterType] += 1; return generation[fighterType]; }
However, extending a generation will cause reRoll
function to always revert.
Generally, when a fighter NFT reRoll, a new fighter base has created via _createFighterBase
which returns element, weight and newDna,
function _createFighterBase( uint256 dna, uint8 fighterType ) private view returns (uint256, uint256, uint256) { uint256 element = dna % numElements[generation[fighterType]]; // @audit 0x12 uint256 weight = dna % 31 + 65; // [1, 30] + 65 => [66, 95] uint256 newDna = fighterType == 0 ? dna : uint256(fighterType); return (element, weight, newDna); }
The numElements[generation]
changed only once while deploying the contract, and the default is set to 3 elements for the 0th generation. It cannot be updated later for other generations. Also, once the generation increments, there is no "go back" button, since generation[fighterType]
will always be greater than 0, and numElements
is not defined for generations other than 0. Therefore, it returns 0. In Solidity, modulo by 0 causes a panic error, leading to transaction reversal.
Add below test to test/FighterFarm.t.sol and run forge test --mt testModuloByZero
function testModuloByZero() public { address player = makeAddr("playooor"); uint8 tokenId = 0; uint8 fighterType = 0; // Champion(0) _mintFromMergingPool(player); _fundUserWith4kNeuronByTreasury(player); _neuronContract.addSpender(address(_fighterFarmContract)); assertEq(_fighterFarmContract.ownerOf(tokenId), player); assertEq(_fighterFarmContract.numRerolls(tokenId), 0); _fighterFarmContract.incrementGeneration(fighterType); vm.prank(player); vm.expectRevert(); _fighterFarmContract.reRoll(tokenId, fighterType); }
Manual review
Add a function to change elements count for respective generation of fighterType,
DoS
#0 - c4-pre-sort
2024-02-22T18:27:29Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T18:27:38Z
raymondfam marked the issue as duplicate of #45
#2 - c4-judge
2024-03-07T06:53:30Z
HickupHH3 changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-07T06:56:57Z
HickupHH3 marked the issue as satisfactory
π 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
When player stakenNRN
, they have to lock their fighter NFT along with $NRN tokens. The function internally calls updateFighterStaking
marking the status locked for tokenId
, restricting any transfer during the battle.
An attacker can circumvent the NFT locking mechanism, and can freely participate without needing to lock onto NFT. Let's see, how.
stakeNRN
locking their fighter NFT which changes amountStaked[tokenId] >0
and fighterStaked[tokenId]==true
stakeAtRisk[tokenId][roundId] >0
, and finally battle records updatedunstakeNRN
before the updateBattleRecord
occurs, the new state is amountStaked[tokenId] ==0
amountStaked[tokenId] >0
, since there is stakeAtRisk[tokenId][roundId] >0
and some stake has been put out of risk by an update call.setNewRound
, sweeping all the stake puts at risk in the last round.amountStaked[tokenId] > 0
and to lock the fighter it is required to be 0. The if check will bypassed.Add below file to test/RankedBattle.t.sol and run forge test --mt testBypasssNFTLock
function testBypasssNFTLock() public { address player = makeAddr("playooor"); address attacker = makeAddr("attackur"); _mintFromMergingPool(player); _fundUserWith4kNeuronByTreasury(player); _mintFromMergingPool(attacker); _fundUserWith4kNeuronByTreasury(attacker); vm.prank(player); _rankedBattleContract.stakeNRN(1000e18, 0); vm.prank(attacker); _rankedBattleContract.stakeNRN(1000e18, 1); // player wins some points vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); // attacker lost, non-zero stake at risk vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(1, 50, 2, 1500, true); uint256 attackerNRNBalance = _rankedBattleContract.amountStaked(1); vm.prank(attacker); _rankedBattleContract.unstakeNRN(attackerNRNBalance, 1); assertEq(_rankedBattleContract.amountStaked(1), 0); // staked amount == 0 assertEq(_stakeAtRiskContract.getStakeAtRisk(1), 1e18); // stakeAtRisk[round][1] == 1e18 // attacker wins, amountStaked[1] non-zero vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); assertGt(_rankedBattleContract.amountStaked(1), 0); // stake amount > 0 // new rounds begins _rankedBattleContract.setNewRound(); assertEq(_rankedBattleContract.roundId(), 1); // attacker can stake now, without locking their fighter vm.prank(attacker); _rankedBattleContract.stakeNRN(1000e18, 1); bool lockedStatus = _fighterFarmContract.fighterStaked(1); assertEq(lockedStatus, false); }
Manual review
Modify few lines in stakeNRN
function,
- if (amountStaked[tokenId] == 0) { + if (_fighterFarmInstance.fighterStaked(tokenId) == false) { _fighterFarmInstance.updateFighterStaking(tokenId, true); }
Invalid Validation
#0 - c4-pre-sort
2024-02-25T03:58:05Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-25T03:58:18Z
raymondfam marked the issue as duplicate of #833
#2 - c4-judge
2024-03-13T10:12:54Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-13T11:32:33Z
HickupHH3 marked the issue as duplicate of #1641
#4 - c4-judge
2024-03-14T06:23:19Z
HickupHH3 marked the issue as satisfactory