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: 204/283
Findings: 5
Award: $2.43
π 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#L333-L365 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L533-L545
The FighterFarm.sol
contract is ERC721. When overriding the transfer function, only transferFrom(address from, address to, uint256 tokenId)
and safeTransferFrom(address from, address to, uint256 tokenId)
were overrided, but safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
was not, causing the _ableToTransfer
check could be bypassed.
In ERC721.sol:
/** * @dev See {IERC721-safeTransferFrom}. */ 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); }
As shown above, there is a public function safeTransferFrom (with 4 parameters) in ERC721. This function can be called directly in the FighterFarm.sol
contract, but there is no override of this function in the FighterFarm.sol
contract, which means there is no _ableToTransfer
check.
function _ableToTransfer(uint256 tokenId, address to) private view returns(bool) { return ( _isApprovedOrOwner(msg.sender, tokenId) && balanceOf(to) < MAX_FIGHTERS_ALLOWED && !fighterStaked[tokenId] ); }
The absence of this check means that the fighter NFT with staked tokens could be transferred, or the user has more fighter NFTs than the MAX_FIGHTERS_ALLOWED
limit.
The lack of _ableToTransfer
check in safeTransferFrom (with 4 parameters) will cause at least the following two effects:
MAX_FIGHTERS_ALLOWED
limit, this will break the protocol's assumptions and lead to unfair or unexpected consequences.Just add the testTransferringFighterWhileStakedSuccess
function to FighterFarm.t.sol
and run, the following code demonstrates transferring the fighter NFT with staked tokens by calling safeTransferFrom (with 4 parameters).
function testTransferringFighterWhileStakedSuccess() public { _mintFromMergingPool(_ownerAddress); _fighterFarmContract.addStaker(_ownerAddress); _fighterFarmContract.updateFighterStaking(0, true); assertEq(_fighterFarmContract.fighterStaked(0), true); assertEq(_fighterFarmContract.ownerOf(0), _ownerAddress); _fighterFarmContract.safeTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, 0, ""); assertEq(_fighterFarmContract.ownerOf(0), _DELEGATED_ADDRESS); }
Manual Review, Foundry
Consider the following mitigation measures:
Override safeTransferFrom
(with 4 parameters) in FighterFarm.sol
, add _ableToTransfer
check.
Or override the internal function _transfer
.
ERC721
#0 - c4-pre-sort
2024-02-23T04:32:46Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-23T04:32:54Z
raymondfam marked the issue as duplicate of #54
#2 - c4-pre-sort
2024-02-23T04:47:16Z
raymondfam marked the issue as duplicate of #739
#3 - c4-pre-sort
2024-02-23T04:52:40Z
raymondfam marked the issue as sufficient quality report
#4 - c4-judge
2024-03-11T02:39:44Z
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#L289-L303
The GameItems.sol
contract is ERC1155. When overriding the transfer function, only safeTransferFrom
was overrided, but safeBatchTransferFrom
was not, causing the allGameItemAttributes[tokenId].transferable
check could be bypassed.
In ERC1155.sol:
/** * @dev See {IERC1155-safeBatchTransferFrom}. */ function safeBatchTransferFrom( address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data ) public virtual override { require( from == _msgSender() || isApprovedForAll(from, _msgSender()), "ERC1155: caller is not token owner nor approved" ); _safeBatchTransferFrom(from, to, ids, amounts, data); }
As shown above, there is a public function safeBatchTransferFrom
in ERC721. This function can be called directly in the GameItems.sol
contract, but there is no override of this function in the GameItems.sol
contract, which means there is no allGameItemAttributes[tokenId].transferable
check. Ultimately, it will result in untransferable items being transferred.
By calling the safeBatchTransferFrom
function in ERC1155, we can transfer the voltage or other proprietary items, which will break the developer's assumptions and lead to unexpected situations.
Just add the testSafeBatchTransferFrom
function to GameItems.t.sol
and run, the following code demonstrates transferring the untransferable (transferable=false) item by calling safeBatchTransferFrom
.
function testSafeBatchTransferFrom() public { _fundUserWith4kNeuronByTreasury(_ownerAddress); _gameItemsContract.mint(0, 1); _gameItemsContract.adjustTransferability(0, false); uint256[] memory ids = new uint256[](1); ids[0] = 0; uint256[] memory amount = new uint256[](1); amount[0] = 1; // Before transfer assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, 0), 0); assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 1); // transfer _gameItemsContract.safeBatchTransferFrom(_ownerAddress, _DELEGATED_ADDRESS, ids, amount , ""); // After transfer assertEq(_gameItemsContract.balanceOf(_DELEGATED_ADDRESS, 0), 1); assertEq(_gameItemsContract.balanceOf(_ownerAddress, 0), 0); }
Manual Review, Foundry
Consider overriding safeBatchTransferFrom
in GameItems.sol
, adding allGameItemAttributes[tokenId].transferable
check.
Token-Transfer
#0 - c4-pre-sort
2024-02-22T03:46:19Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T03:46:26Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:27:50Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:52:23Z
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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L482-L484
Users can stake a small amount of NRN tokens first, and after winning a game, stake more NRN tokens. In this case, if the fighter loses the game, he will only lose a small number of points accumulated before. If the fighter wins, he will get a lot of points.
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); } }
The code related to the vulnerability is as shown above. If the fighter has a positive point balance for this round, points
will not be greater than accumulatedPointsPerFighter
. If accumulatedPointsPerFighter
is very small, gains and losses are obviously completely different, which will result in users taking very low risks to obtain huge rewards.
Users can make a risk-free profit on a certain fighter NFT every round.
Consider the following scenario. Alice owns multiple fighter NFTs. For each NFT, Alice does the following operations:
points
and then unstake all NRN tokensSince Alice has many fighter NFTs, she can perform the above operations on each NFT every round. Although each NFT can only be operated once, as long as the number of NFTs is large enough, huge risk-free profit can be achieved.
The following code demonstrates the above scenario, Just add the testRiskFreeScenario
function to RankedBattle.t.sol
and run.
function testRiskFreeScenario() public { address player = vm.addr(3); _mintFromMergingPool(player); _fundUserWith4kNeuronByTreasury(player); // stake DUST and win vm.prank(player); _rankedBattleContract.stakeNRN(1, 0); vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); assertEq(_rankedBattleContract.accumulatedPointsPerFighter(0, 0) == 750, true); // stake a large amount of NRN token vm.prank(player); _rankedBattleContract.stakeNRN(3_000 * 10 ** 18, 0); // lose the game vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 2, 1500, true); // only lose points assertEq(_rankedBattleContract.accumulatedPointsPerFighter(0, 0) == 0, true); // the token is not lost assertEq(_rankedBattleContract.amountStaked(0), 3_000 * 10 ** 18 + 1); }
Manual Review
When losing a game, besides deducting the remaining points, there are other penalties to consider(especially when the point is not enough).
Other
#0 - c4-pre-sort
2024-02-22T15:46:06Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-22T15:46:13Z
raymondfam marked the issue as duplicate of #38
#2 - c4-judge
2024-03-07T02:49:49Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-07T02:58:22Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-07T03:14:41Z
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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L519-L535
The calculation formula of RankedBattle.sol#_getStakingFactor
is sqrt((amountStaked[tokenId] + stakeAtRisk) / 10**18)
. If the result is 0, it will be explicitly assigned a value of 1. That is to say, when amountStaked[tokenId]
is 1 wei and 3.99 ether, the results are the same.
uint256 stakingFactor_ = FixedPointMathLib.sqrt( (amountStaked[tokenId] + stakeAtRisk) / 10**18 ); if (stakingFactor_ == 0) { stakingFactor_ = 1; }
The above code shows that when stakingFactor_
is 0, the protocol assigns it a value of 1. When amountStaked[tokenId]
is 1 wei, the calculated stakingFactor_
is 0, so it is explicitly assigned a value of 1. When amountStaked[tokenId]
is 3.99 ether, the calculated stakingFactor_
is less than 2, so the result is also 1. Obviously the payouts for staking 1 wei and 3.99 ether are completely different, but their rewards are the same.
As long as the staked amount is within the [0,4 ether) range, there will be no difference in returns, which is obviously unfair.
Just add testStake1WeiAndStakeSmallerthen4ether
function to RankedBattle.t.sol
and run. The following code shows the comparison of the results of staking 3.99 ether and staking 1 wei.
function testStake1WeiAndStakeSmallerthen4ether() public { address player = vm.addr(3); address alice = vm.addr(4); _mintFromMergingPool(player); _mintFromMergingPool(alice); _fundUserWith4kNeuronByTreasury(player); _fundUserWith4kNeuronByTreasury(alice); // stake 3.99 ether vm.prank(player); _rankedBattleContract.stakeNRN(3.99 * 10 ** 18, 0); vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(0, 50, 0, 1500, true); assertEq(_rankedBattleContract.accumulatedPointsPerFighter(0, 0) == 750, true); // stake 1 wei vm.prank(alice); _rankedBattleContract.stakeNRN(1, 1); vm.prank(address(_GAME_SERVER_ADDRESS)); _rankedBattleContract.updateBattleRecord(1, 50, 0, 1500, true); // accumulatedPointsPerFighter results were the same assertEq(_rankedBattleContract.accumulatedPointsPerFighter(0, 0) == _rankedBattleContract.accumulatedPointsPerFighter(1, 0), true); }
Manual Review, Foundry
_getStakingFactor
needs to be redesigned. The following suggestions are available for reference:
Math
#0 - c4-pre-sort
2024-02-24T08:12:03Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2024-02-24T08:12:16Z
raymondfam marked the issue as duplicate of #38
#2 - c4-judge
2024-03-07T02:49:49Z
HickupHH3 changed the severity to 2 (Med Risk)
#3 - c4-judge
2024-03-07T02:58:22Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-07T03:45:00Z
HickupHH3 marked the issue as satisfactory
π 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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/MergingPool.sol#L149-L163
claimRewards
only supports claiming all previously unclaimed rewards at once. mintFromMergingPool
will be called for each reward. If there are too many rewards, it will cause out of gas and the user will not be able to claim any of his rewards.
uint32 lowerBound = numRoundsClaimed[msg.sender]; @> for (uint32 currentRound = lowerBound; currentRound < roundId; currentRound++) { numRoundsClaimed[msg.sender] += 1; winnersLength = winnerAddresses[currentRound].length; for (uint32 j = 0; j < winnersLength; j++) { if (msg.sender == winnerAddresses[currentRound][j]) { _fighterFarmInstance.mintFromMergingPool( msg.sender, modelURIs[claimIndex], modelTypes[claimIndex], customAttributes[claimIndex] ); claimIndex += 1; } } }
The code is as shown above. The user cannot specify the upper bound of the loop rountId
. Too many loops may eventually lead to out of gas.
Once out of gas occurs, it means that the user can no longer claim any of his rewards.
Consider the following scenario:
MergingPool. sol # claimRewards
, but due to too many loops, it results in an out of gas.Manual Review
It is recommended to add an input parameter to specify the upper bound of the loop, so that the user can claim part of his own reward instead of all at once.
Loop
#0 - c4-pre-sort
2024-02-23T23:58:46Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T23:59:00Z
raymondfam marked the issue as duplicate of #1541
#2 - c4-judge
2024-03-11T13:01:17Z
HickupHH3 marked the issue as duplicate of #216
#3 - c4-judge
2024-03-12T02:35:55Z
HickupHH3 marked the issue as partial-50
#4 - c4-judge
2024-03-21T02:56:42Z
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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L379
FighterFarm.sol#reroll
is used to roll a new fighter. Calling this function requires a rerollCost, and for the same tokenId, the number of reroll function calls is limited. In this function, it uses msg.sender
, tokenId
and numRerolls[tokenId]
as traits to calculate dna for subsequent generation of element
, weight
, physicalAttributes
. Since these traits are predictable, dna is not random and users can control dna to get the element
, weight
and physicalAttributes
they want.
In FighterFarm.sol:
/// @notice Rolls a new fighter with random traits. /// @param tokenId ID of the fighter being re-rolled. /// @param fighterType The fighter type. 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].physicalAttributes = _aiArenaHelperInstance.createPhysicalAttributes( newDna, generation[fighterType], fighters[tokenId].iconsType, fighters[tokenId].dendroidBool ); _tokenURIs[tokenId] = ""; } }
In the marked line, dna
is generated by calculating msg.sender
, tokenId
and numRerolls[tokenId]
. These traits are obviously not random, but are described as random traits in the function's @notice
. Users can calculate the NFT attributes after reRoll based on msg.sender
, tokenId
, numRerolls[tokenId]
, which obviously leads to unfair game.
For a certain NFT, users can reRoll out the attributes they want through this vulnerability and obtain rarer attributes, which is extremely unfair to other users.
Consider the following scenario:
Manual Review
In the GameFi project, you should use off-chain random numbers provided by the oracle project, such as Chainlink VRF, and avoid using on-chain pseudo-random numbers.
Other
#0 - c4-pre-sort
2024-02-24T01:33:54Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:34:03Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:48:53Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-15T02:10:55Z
HickupHH3 changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-03-20T01:04:21Z
HickupHH3 marked the issue as duplicate of #376
π 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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L324
The mintFromMergingPool
function is used by the winning user to claim the new NFT. In the process of creating the NFT, msg.sender
and fighters.length
are used as traits, where msg.sender
is MergingPool's address, and fighters. length
can be observed, so dna can be predicted. Users can obtain the fighter NFT that meets the attributes they want by predicting dna.
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))), modelHash, modelType, 0, 0, customAttributes ); }
When creating a new fighter NFT, the user can determine whether to call claimRewards
by observing fighters.length
. You can wait until the attributes you are satisfied with are calculated through fighters.length before calling claimRewards
.
Users can obtain fighter NFTs with the DNA they want by observing fighters.length
, which leads to unfair games.
Consider the following scenario:
MergingPool.sol#claimRewards
keccak256(abi.encode(msg.sender, fighters.length))
fighters.length
to change and calculates the dna. Once the dna is what she wants, Alice can immediately call MergingPool.sol#claimRewards
Manual Review
In the GameFi project, you should use off-chain random numbers provided by the oracle project, such as Chainlink VRF, and avoid using on-chain pseudo-random numbers.
Other
#0 - c4-pre-sort
2024-02-24T01:34:17Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:34:26Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:48:56Z
HickupHH3 marked the issue as satisfactory
#3 - c4-judge
2024-03-06T03:49:26Z
HickupHH3 changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-15T02:10:55Z
HickupHH3 changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-03-20T01:04:22Z
HickupHH3 marked the issue as duplicate of #376
π 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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/FighterFarm.sol#L214
FighterFarm.sol#claimFighters
enables users to claim a pre-determined number of fighters. In the process of creating the NFT, msg.sender
and fighters.length
are used as traits to generate dna. Although msg.sender
is fixed, fighters.length
can be observed, so dna is predictable. Users can obtain the fighter NFT that meets the attributes they want by predicting dna.
for (uint16 i = 0; i < totalToMint; i++) { _createNewFighter( msg.sender, @> uint256(keccak256(abi.encode(msg.sender, fighters.length))), modelHashes[i], modelTypes[i], i < numToMint[0] ? 0 : 1, 0, [uint256(100), uint256(100)] ); }
When creating a new fighter NFT, the user can determine whether to call claimFighters
by observing fighters.length
. You can wait until the attributes you are satisfied with are calculated through fighters.length before calling claimFighters
.
Users can obtain fighter NFTs with the DNA they want by observing fighters.length
, which leads to unfair games.
Consider the following scenario:
FighterFarm.sol#claimFighters
keccak256(abi.encode(msg.sender, fighters.length))
fighters.length
to change and calculates the dna. Once the dna is what she wants, Alice can immediately call FighterFarm.sol#claimFighters
Manual Review
Use off-chain random numbers provided by the oracle project, such as Chainlink VRF, instead of using on-chain pseudo-random numbers.
Other
#0 - c4-pre-sort
2024-02-24T01:35:07Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T01:35:19Z
raymondfam marked the issue as duplicate of #53
#2 - c4-judge
2024-03-06T03:49:26Z
HickupHH3 changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-06T03:49:50Z
HickupHH3 marked the issue as satisfactory
#4 - c4-judge
2024-03-15T02:10:55Z
HickupHH3 changed the severity to 2 (Med Risk)
#5 - c4-judge
2024-03-20T01:04:25Z
HickupHH3 marked the issue as duplicate of #376