Platform: Code4rena
Start Date: 25/01/2023
Pot Size: $36,500 USDC
Total HM: 11
Participants: 173
Period: 5 days
Judge: kirk-baird
Total Solo HM: 1
Id: 208
League: ETH
Rank: 28/173
Findings: 4
Award: $152.23
π Selected for report: 0
π Solo Findings: 0
π Selected for report: adriro
Also found by: 0xRobocop, 0xmrhoodie, 0xngndev, AkshaySrivastav, ArmedGoose, Atarpara, Bauer, CodingNameKiki, ElKu, Garrett, HollaDieWaldfee, IllIllI, Iurii3, KIntern_NA, KmanOfficial, Lotus, M4TZ1P, MiniGlome, Ruhum, SovaSlava, bin2chen, bytes032, carrotsmuggler, cccz, chaduke, codeislight, cryptonue, doublesharp, evan, fs0c, glcanvas, gzeon, hansfriese, hihen, hl_, holme, horsefacts, ladboy233, lukris02, mahdikarimi, manikantanynala97, martin, mert_eren, mrpathfindr, omis, peakbolt, peanuts, prestoncodes, rbserver, rvierdiiev, sashik_eth, timongty, tnevler, trustindistrust, usmannk, wait, yixxas, zadaru13, zaskoh
0.7512 USDC - $0.75
If someone calls withdrawFee many time before, there won't be enough tokens on the contract to claim by participants
Function WithdrawFee() is a public function, so anyone may call it. While it has modifier onlyAdminWithdrawAfterEnd that would probably intended to limit it's usage to only Admin, actually this modifier only checks if the end time passed
/// @notice Prevents reward withdrawal until the Quest has ended modifier onlyAdminWithdrawAfterEnd() { if (block.timestamp < endTime) revert NoWithdrawDuringClaim(); _; }
In this case after the end of the quest anyone can call this function and withdraw all funds from the Quest to protocolFeeRecipient. Moreover, as there is no check if the protocolFee was already withdraw anyone may drain quest contract after it ending by calling this function many times. And thus participant won't be able to claim their rewards
Add check if protocol fee was collected before executing this function
#0 - c4-judge
2023-02-06T08:28:55Z
kirk-baird marked the issue as duplicate of #23
#1 - c4-judge
2023-02-14T08:54:38Z
kirk-baird changed the severity to 3 (High Risk)
#2 - c4-judge
2023-02-14T08:56:36Z
kirk-baird marked the issue as satisfactory
π Selected for report: carrotsmuggler
Also found by: AkshaySrivastav, ElKu, HollaDieWaldfee, Iurii3, KmanOfficial, adriro, bin2chen, evan, hansfriese, hl_, mert_eren, omis, peanuts
122.948 USDC - $122.95
If ProtocolFee were collected from ERC20 quest before calling withdrawRemainingTokens function, the amount of tokens to withdraw by withdrawRemainingTokens will be calculated wrong and tokens will be stuck on the contract.
Smart contract designed in a such way that creator of the quest may withdraw unused funds from the Quest after its ending. The unused funds is calculates as Total balance on the contract less Unclaimed by participant less ProtocolFee.
uint256 nonClaimableTokens = IERC20(rewardToken).balanceOf(address(this)) - protocolFee() - unclaimedTokens;
While amount of unclaimed tokens calculated at the moment of withdrawal, nature of ProtocolFee is a bit different. ProtocolFee is static number calculated as percentage of the total amount of granted rewards. Contract does not check whether they were already withdrew from the contract or not.
So, if ProtocolFee were collected, their amount already subtracted from the balance of the ERC20 quest contract. And thus while calculating nonClaimableTokens they would be subtracted second time and won't be withdrew from contract. As these tokens are "free" and not claimable by participant, they would be stuck there forever.
Before calculating nonClaimableTokens check whether protocolFee was already collected or not
#0 - c4-judge
2023-02-06T08:28:23Z
kirk-baird marked the issue as duplicate of #61
#1 - c4-judge
2023-02-14T10:00:41Z
kirk-baird marked the issue as satisfactory
#2 - c4-judge
2023-02-23T23:48:11Z
kirk-baird changed the severity to 2 (Med Risk)
π Selected for report: CodingNameKiki
Also found by: 0x1f8b, 0x4non, 0x5rings, 0xAgro, 0xMirce, 0xRobocop, 0xSmartContract, 0xackermann, AkshaySrivastav, ArmedGoose, Aymen0909, BClabs, Breeje, Dewaxindo, Diana, HollaDieWaldfee, IceBear, IllIllI, Iurii3, Josiah, Krayt, PaludoX0, Phenomana, PrasadLak, RaymondFam, Rolezn, SaeedAlipoor01988, SaharDevep, SleepingBugs, adriro, arialblack14, bin2chen, brevis, btk, carlitox477, carrotsmuggler, catellatech, chaduke, chrisdior4, cryptonue, cryptostellar5, csanuragjain, ddimitrov22, fellows, glcanvas, halden, hl_, horsefacts, jat, joestakey, kenta, libratus, lukris02, luxartvinsec, manikantanynala97, martin, matrix_0wl, nadin, nicobevi, oberon, peanuts, prestoncodes, rbserver, sakshamguruji, sashik_eth, sayan, seeu, thekmj, tnevler, trustindistrust, tsvetanovv, xAriextz, zadaru13, zaskoh
17.196 USDC - $17.20
Creator of the quest has a right to stop (pause forever) quest. In case there is an unclaimed reward at that time, participant won't be able to claim it. While quest creator role is limited it should not be a big problem. However, in case of opening it for broader public, current approach should be reconsidered.
Under the RabbitHoleReceipt.sol contract there are two separate state variables - minterAddress and QuestFactoryContract assigned by relevant function.
RabbitHoleReceipt.sol#L77 RabbitHoleReceipt.sol#L83
However, for proper functioning of the mintReceipt function minterAddress must be set to QuestFactoryContract address.
To avoid confusion we may combine setQuestFactory and setMinterAddress in one function that uses QuestFactory address as an argument
RabbitHoleReceipt.sol does not initialize ERC721EnumerableUpgradeable RabbitHoleTickets.sol does not initialize IERC2981Upgradeable
super.withdrawRemainingTokens(to_) may be changed to modifier onlyAdminWithdrawAfterEnd to be clearer
Erc20Quest.sol#L82 Erc1155Quest.sol#L55
modifier onlyAdminWithdrawAfterEnd() actually does not check if admin calls it, it should be renamed to onlyAfterEnd() Quest.sol#L76
wrong variable name, should be claimingAddress_Balance instead of msgSenderBalance RabbitHoleReceipt.sol#L113
WithdrawRemainingTokens on ERC1155 Quest does not check if there are remaining unclaimed tokens by participants and withdraw full balance on the contract. In this case it must be used only after all participants claim their rewards that may require additional checks from the team.
#0 - c4-judge
2023-02-05T05:50:34Z
kirk-baird marked the issue as grade-b
#1 - c4-sponsor
2023-02-08T02:36:37Z
jonathandiep marked the issue as sponsor acknowledged
π Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0xAgro, 0xSmartContract, 0xhacksmithh, 0xngndev, Aymen0909, Bnke0x0, Breeje, Deivitto, Diana, Dug, Iurii3, LethL, MiniGlome, NoamYakov, RaymondFam, ReyAdmirado, Rolezn, SAAJ, adriro, ali, arialblack14, atharvasama, c3phas, carlitox477, catellatech, chaduke, cryptonue, cryptostellar5, ddimitrov22, dharma09, doublesharp, favelanky, georgits, glcanvas, gzeon, halden, horsefacts, jasonxiale, joestakey, karanctf, lukris02, matrix_0wl, nadin, navinavu, saneryee, shark, thekmj
11.3269 USDC - $11.33
If the second check would be passed (case quests[questId_].addressMinted[msg.sender] == false) the first would be always passed as well. May leave the second one only.
isPaused is false by default and there is no way to un-pause it before calling start() function
There is a mapping between timestamp and receipt token id. However it is not clear how we would use it without any view function to return information from it. Consider deleting or adding view function
There is no check if caller provided right bool canCreateQuest_ as an argument. We may accidentally grant/revoke role second time and lose gas.
We can add check by updating if statement from
if (canCreateQuest_)
to
if (canCreateQuest_ && hasRole(CREATE_QUEST_ROLE, account_) )
While creating quest any sting may be used as it's ID. Using the same ID second time will be reverted as QuestIdUsed(). However, there is no direct way to retrieve if current current Quest Id used or not (it is possible to use questInfo() function, but it will retrieve (0, 0, 0) rather then bool if the current id is available.
It might be useful to create view function that checks if current quest id is available.
Using the addition operator instead of plus-equals saves gas
Make it outside and only use it inside.
Quest.sol#L70 Quest.sol#L101 Quest.sol#L104
Quest.sol#L70 Quest.sol#L104 Quest.sol#L106 QuestFactory.sol#L226 RabbitHoleReceipt.sol#L117 RabbitHoleReceipt.sol#L121 RabbitHoleReceipt.sol#L128 RabbitHoleReceipt.sol#L131
#0 - c4-judge
2023-02-05T05:47:33Z
kirk-baird marked the issue as grade-b