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: 164/283
Findings: 5
Award: $10.15
π 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/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/token/ERC721/ERC721.sol#L161-L184
The FighterFarm
contract implements the OpenZeppelin version of the ERC721 and ERC721 Enumerable contracts. In all versions of the OpenZeppelin documentation it states for ERC721 the "Required interface of an ERC721 compliant contract." involves the following safeTransferFrom signatures:
safeTransferFrom(from, to, tokenId)
and safeTransferFrom(from, to, tokenId, data)
The FighterFarm contract only overrides the safeTransferFrom(from, to, tokenId)
signature function which has a call to _ableToTransfer()
which makes sure that the fighter NFT is not staked or being sent to an recipient who owns 10 fighter NFTs already.
As safeTransferFrom(from, to, tokenId, data)
will be apart of the contract in order to be a fully compliant ERC721 contract, without an override a user could transfer NFTs using this method and circumvent these restrictions.
With no safeTransferFrom(from, to, tokenId, data)
override present in FighterFarm.sol
, the user will be able to use the safeTransferFrom(from, to, tokenId, data)
defined in the OpenZeppelin ERC721 contract when calling it in a transaction to the FighterFarm
contract bypassing any game specific restrictions.
Manual Code Review
Implement the following override function in FighterFarm.sol
/// @notice Safely transfers an NFT from one address to another. /// @dev Add a custom check for an ability to transfer the fighter. /// @param from Address of the current owner. /// @param to Address of the new owner. /// @param tokenId ID of the fighter being transferred. /// @param data Arbitrary data that is passed. 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-23T05:43:29Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-23T05:43:40Z
raymondfam marked the issue as duplicate of #739
#2 - c4-judge
2024-03-11T02:50:38Z
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/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/token/ERC1155/ERC1155.sol#L131-L146 https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L289-L303
GameItems.sol
implements/inherits the OpenZeppelin implementation of the ERC1155 standard. The required compliant interface for ERC1155 involves the function signature safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] values, bytes data)
The game/protocol allows for items in the game to be created which has an transferable attribute which denotes whether the item can be traded or not. The safeTransferFrom
function, which is a part of the ERC1155 compliant implementation, has been overridden in order to ensure a check if in place to only allow items which allowed to be traded, to be transferred to another address. - (require(allGameItemAttributes[tokenId].transferable);
).
As the safeBatchTransferFrom
function has not been overridden, it allows for an address to send non-transferrable items to another address using this function in the standard ERC1155 form.
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L289-L303
There is no overridden form of safeBatchTransferFrom in the contract, meaning if the function is called, it will use the parent implementation from OpenZeppelin ERC1155 contract.
Manual Code Review - VS Code
Implement the following overridden safeBatchTransferFrom
in GameItems.sol
/// @notice Safely transfers multiple NFTs from one address to another. /// @dev Added a check to see if the game item is transferable. //@audit - Ok function safeBatchTransferFrom( address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data ) public override(ERC1155) { for (uint256 i = 0; i < ids.length; ++i) { require(allGameItemAttributes[ids[i]].transferable, "At least one item is not transferable"); } super.safeBatchTransferFrom(from, to, ids, amounts, data); }
Token-Transfer
#0 - c4-pre-sort
2024-02-22T04:27:55Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-22T04:28:02Z
raymondfam marked the issue as duplicate of #18
#2 - c4-pre-sort
2024-02-26T00:29:27Z
raymondfam marked the issue as duplicate of #575
#3 - c4-judge
2024-03-05T04:56:58Z
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/RankedBattle.sol#L294-L311
If the user has not claimed NRN rewards for several rounds. When the user eventually goes to claim the rewards, the transaction could revert due to consuming too much gas making the rewards stuck in the contract for that specific user.
User decides to play the game after large amount of rounds has taken place and then eventually accumulate points to claim or accumulated points in an early round and not until many rounds later do they decide to claim NRN.
claimNRN()
Manual Code Review, VS Code
I suggest adding a maximum traversal limit parameter on the function to allow the for loop to go from the lower bound to lower bound + limit, in order to ensure the function can never experience DoS from the loop being unbounded.
It may be beneficial to have a mapping that records the first round that a user became eligible for rewards when recording the amounts of points a user is eligible for. This can then be referenced when claimNRN()
is called in order to determine what is the initial lower bound that the for loop within claimNRN()
should start from for a specific msg.sender and the numRoundsClaimed[msg.sender] would be added to this value in order to determine lowerBound.
Another suggestion, could be making rewards from after a certain amount of rounds unclaimable - e.g. unclaimed rewards after 10 rounds is no longer considered (which is 10 weeks - if 1 round lasts about a week). This allows the protocol to ensure that the for loop is always limited to a certain number of iterations eliminating the risk of DoS. The require statement for the function would need to be modified to ensure whether all NRN distributions for a user up to the latest round has been claimed.
DoS
#0 - c4-pre-sort
2024-02-25T02:28:49Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-25T02:28:59Z
raymondfam marked the issue as duplicate of #1541
#2 - c4-judge
2024-03-11T13:01:21Z
HickupHH3 marked the issue as duplicate of #216
#3 - c4-judge
2024-03-12T02:45:12Z
HickupHH3 marked the issue as partial-50
#4 - c4-judge
2024-03-21T02:57:48Z
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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/StakeAtRisk.sol#L132-L134 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/RankedBattle.sol#L322-L349
A fighter NFT may have stake at risk whilst a round is in progress, if the owner has fights where it loses and they are staking NRN against the fighter and they have no points. The more losses, the more NRN is staked at risk for the NFT. If the user doesn't win matches in an attempt to recover all of that stake at risk before the round ends, that NRN staked at risk is lost forever.
If the user at some point during the current round unstakes their whole amount of NRN that is no longer at risk, the NFT will also be unstaked. The user will be free to transfer this NFT to another address given that the NFT is no longer staked and the recipient owns less than 10 Fighter NFTs. The user who receives this NFT will now have an NFT that has stake at risk. The impact of this is that if a user now stakes NRN and loses a fight, their staked NRN is automatically subject to a higher penalty than expected (as the loss is a percentage of your staked + staked at risk on the asset) leading to potential loss for the user.
When the battle record is updated for the user (RankedBattle.updateBattleRecord()
), it gets the stake at risk for the asset. _stakeAtRiskInstance.stakeAtRisk()
returns the data that is held in a mapping that references the Fighter NFT Token Id and the current roundId
. This mapping does not take in account the current msg.sender at all, so stake at risk is transferred to another user.
Manual Code Review - VS Code
I suggest blocking transfers for NFTs that has stake at risk for a current round.
If this is something the game/protocol doesn't want to do, an alternative suggestion is modifying the stakeAtRisk mapping to include the referencing of msg.sender within the data structure. This allows the new address to not have a stakeAtRisk associated with using the NFT. If the original owner chooses to transfer the NFT to another address, they accept that the stakeAtRisk is potentially permanently lost, if they do not become the owner of the NFT again before the round ends are able to win a portion or all of the StakeAtRisk
back.
Other
#0 - c4-pre-sort
2024-02-24T04:54:48Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-24T04:54:58Z
raymondfam marked the issue as duplicate of #1641
#2 - c4-pre-sort
2024-02-24T06:02:10Z
raymondfam marked the issue as insufficient quality report
#3 - c4-pre-sort
2024-02-24T06:02:18Z
raymondfam marked the issue as not a duplicate
#4 - c4-pre-sort
2024-02-24T06:02:26Z
raymondfam marked the issue as duplicate of #1581
#5 - raymondfam
2024-02-24T06:04:34Z
It should cause an underflow when updating record as elicited in #1641.
#6 - c4-judge
2024-03-13T10:08:27Z
HickupHH3 marked the issue as satisfactory
#7 - c4-judge
2024-03-13T10:11:54Z
HickupHH3 marked the issue as duplicate of #1641
π 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
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L416-L500
if (battleResult == 0) { /// If the user won the match /// If the user has no NRNs at risk, then they can earn points if (stakeAtRisk == 0) { points = stakingFactor[tokenId] * eloFactor; } /// Divert a portion of the points to the merging pool uint256 mergingPoints = (points * mergingPortion) / 100; points -= mergingPoints; _mergingPoolInstance.addPoints(tokenId, mergingPoints); ... }
If stakeAtRisk is 0, it means the user is eligible to gain some points based on how much they have staked, but initially points is initialised to 0 within the function so there is no need to do calculate mergingPoints if points will be 0. It leads to unnecessary execution of _mergingPoolInstance.addPoints
which under all circumstances emits an pointsAdded event which is state data that doesn't need to be indexed as no points were actually added to the mergingPool.
Recommendation: Wrap the merging points calculation points and addition code within an if statement - if(points > 0)
.
--
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L348
The totalBattles value globally tracks the number of fights that has taken place. A fight between two users is one battle. RankedBattle.UpdateBattleRecord
is called twice for one fight. Once for the initiator and once for the opponent. The code incorrectly increments total battles by 1 for each call for the respective users in the fight. This would give the indication that two separate battles took place, when it's the same battle.
Recommendation: Move totalBattles += 1;
to within the if (initiatorBool) {
statement and increment the number of battles done globally across the game when the battle record update is sent to the blockchain for the initiator.
--
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/GameItems.sol#L147-L176
The missing require statement on the quantity allows for unchecked execution of the mint function leading to 0 value transfers and mint and the emitting of the boughtItem event with a quantity - despite no harm to the protocol. It is against best practice for recording unnecessary state data.
Recommendation: add an require statement at the top of the function before main exeuction - require(amount > 0, "Mint at least one item");
--
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/RankedBattle.sol#L491-L498
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/StakeAtRisk.sol#L115-L127
When the user in question has lost the fight, if they have no points it is intended that stake at risk is deducted instead. If all of their users initially proposed staked, is now at risk, there is no more NRN to add to the staked at risk balance for the fighter NFT for that current round. The function will perform a 0 transfer which will result in success and then call stakeAtRisk.updateAtRiskRecords
which will emit an IncreasedStakeAtRisk
event. Although it has no huge consequences, it is against best practice as the stake at risk has not increased and has in fact stayed the same.
Recommendation: Modify the if statement before calling stakeAtRisk.updateStakeAtRisk
from if (success)
to if (success && curStakeAtRisk > 0)
At L494 in RankedBattle.sol
--
https://github.com/code-423n4/2024-02-ai-arena/blob/main/src/VoltageManager.sol#L105-L112
spendVoltage can be operated by the msg.sender themselves or via an authorised spender such as the Ranked Battle contract. It is too be spent for actions in the game such as the user who initiates a battle regardless of the outcome. The user is able to directly post a transaction to the blockchain with this function and spend any amount of voltage. Although there is no benefit to a user doing this, it is still unintended behaviour where a user could accidentally spend all voltage and have to wait until their replenish time before they use the game again.
Recommendation: Don't allow spend voltage to be called directly by the msg.sender as there is no need. Any interaction with the protocol can handle the voltage spending directly as authorised spenders on behalf of users of the game.
require(spender == msg.sender || allowedVoltageSpenders[msg.sender]);
to require(allowedVoltageSpenders[msg.sender]);
instead.
#0 - raymondfam
2024-02-26T04:29:48Z
Adequate amount of L and NC entailed.
#1 - c4-pre-sort
2024-02-26T04:29:52Z
raymondfam marked the issue as sufficient quality report
#2 - HickupHH3
2024-03-05T02:32:25Z
#1244: R #1253: R #2007: R #1240: L
#3 - c4-judge
2024-03-15T06:57:28Z
HickupHH3 marked the issue as grade-b