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: 53/173
Findings: 4
Award: $48.11
π 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
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L102-L104 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L76-L79
In Erc20Quest.sol, the withdrawFee() can be called multiple times by anyone to drain unclaimed rewards from the contract. This is due to missing tracking of previous protocol fee withdrawals and incorrect modifier.
After the end of the claim period, call the withdrawFee() multiple times to drain the contract of the unclaimed rewards. The function does not track or update the balance after transfering the protocol fee.
function withdrawFee() public onlyAdminWithdrawAfterEnd { IERC20(rewardToken).safeTransfer(protocolFeeRecipient, protocolFee()); }
And there is no access control as the onlyAdminWithdrawAfterEnd modifier only ensure the withdrawFee() can be called after end of claim period.
modifier onlyAdminWithdrawAfterEnd() { if (block.timestamp < endTime) revert NoWithdrawDuringClaim(); _; }
Track the amount of protocol fee withdrawn and add in access control if necessary.
#0 - c4-judge
2023-02-05T05:37:18Z
kirk-baird marked the issue as duplicate of #23
#1 - c4-judge
2023-02-14T08:59:48Z
kirk-baird marked the issue as satisfactory
π Selected for report: adriro
Also found by: 0xRobocop, 0xbepresent, Breeje, CodingNameKiki, HollaDieWaldfee, Kenshin, M4TZ1P, Ruhum, Tricko, badman, bin2chen, carrotsmuggler, cccz, csanuragjain, glcanvas, joestakey, lukris02, m9800, mert_eren, peakbolt, peanuts, prestoncodes, rvierdiiev, sashik_eth
21.6061 USDC - $21.61
In Erc20Quest.sol, the withdrawRemainingTokens() could withdraw more unclaimed tokens than calculated if new RabbitHoleReceipts are minted after that. This is because withdrawRemainingTokens() takes the number of RabbitHoleReceipt minted from questFactoryContract.getNumberMinted().
However, nothing stops users from minting new RabbitHoleReceipt after the claim period end. This will prevent some users from redeeming their rewards as there will be less token balance in the contract than required.
Suppose a scenario (with zero protocol fee to simplify it) - Where total participants = 100 and reward amount is 1 ETH, - Then max rewards will be 100 ETH (100 particiants *1ETH) - Erc20Quest contract balance will be = 100 ETH (assume min required starting balance).
After the end of claim period, we assume - RH receipts minted = 10 - RH receipts claimed = 9, leaving balance = 91 ETH (100ETH - 9 claims * 1 ETH) - Then withdrawRemainingTokens() will transfer out 91ETH - 1 unclaimed RH receipt (i.e. 1 ETH) = 90 ETH. - Remaining balance = 1 ETH for the 1 unclaimed receipt
However, a participant who has a legit signature could still mint a new RH receipt at this point. With the receipt, he can claim the remaining 1 ETH, taking away the unclaimed reward for the other participant.
Note: it doesnβt matter if off-chain signing is disabled after claim period. This is because participant could get a valid signature before claim period end and not mint it until the scenario above occurs.
Prevent minting of new tickets after claim period end so that the amount of minted RH receipt will be fixed and unchanged.
#0 - c4-judge
2023-02-05T05:38:49Z
kirk-baird marked the issue as duplicate of #22
#1 - c4-judge
2023-02-14T08:47:04Z
kirk-baird marked the issue as satisfactory
π Selected for report: RaymondFam
Also found by: 0xMirce, AkshaySrivastav, AlexCzm, Aymen0909, BClabs, CodingNameKiki, ElKu, HollaDieWaldfee, Josiah, KIntern_NA, MiniGlome, StErMi, adriro, bin2chen, cccz, chaduke, csanuragjain, gzeon, hihen, holme, libratus, minhquanym, omis, peakbolt, peanuts, rbserver, rvierdiiev, timongty, ubermensch, usmannk, wait, zaskoh
7.046 USDC - $7.05
Participants will not be able to claim their rewards after the quest owner calls Erc1155Quest.withdrawRemainingTokens(), which will withdraw all remaining tokens including unclaimed ones. This is different from Erc20Quest, which will retain any unclaimed rewards even after withdrawRemainingTokens() is called.
This behaviour is also different from the claim process documentation at https://user-images.githubusercontent.com/14314818/214354756-0af7e34d-746e-4429-8b55-8eb6d8bb1e31.png
Suppose the ERC1155 quest has ended and the quest owner calls withdrawRemainingTokens() to transfer out all the remaining tokens in the contract. Now, participants who still has unredeemed RH receipts will not be able to claim their rewards from the contract as the balance is zero.
function withdrawRemainingTokens(address to_) public override onlyOwner { super.withdrawRemainingTokens(to_); IERC1155(rewardToken).safeTransferFrom( address(this), to_, rewardAmountInWeiOrTokenId, IERC1155(rewardToken).balanceOf(address(this), rewardAmountInWeiOrTokenId), '0x00' ); }
Rectify the withdrawRemainingTokens() to only withdraw non-claimable rewards (based on proportion of unminted RH receipts), while leaving the unclaimed rewards in the contract.
#0 - c4-judge
2023-02-05T05:39:22Z
kirk-baird marked the issue as duplicate of #42
#1 - c4-judge
2023-02-10T05:03:11Z
kirk-baird changed the severity to QA (Quality Assurance)
#2 - c4-judge
2023-02-10T05:12:15Z
This previously downgraded issue has been upgraded by kirk-baird
#3 - c4-judge
2023-02-14T09:28:05Z
kirk-baird marked the issue as satisfactory
#4 - c4-judge
2023-02-23T23:49:21Z
kirk-baird changed the severity to 2 (Med Risk)
π Selected for report: AkshaySrivastav
Also found by: KIntern_NA, SovaSlava, Tointer, Tricko, V_B, __141345__, betweenETHlines, bin2chen, cccz, critical-or-high, glcanvas, halden, hihen, jesusrod15, ladboy233, libratus, m9800, minhquanym, omis, peakbolt, rbserver, romand, rvierdiiev, wait, zaskoh
18.6976 USDC - $18.70
As the documentation mentioned that the protocol will be deployed on multiple chains, it is possible to create a quest with the exact same questId on a different chain and generate a valid signature for any user address.
Furthermore, there is no signature expiry, anyone could look for quests with unclaimed rewards and replay signature to mint RH receipt and claim the rewards.
As shown below, the hash used for the signature only contains the msg.sender and questId_, both of which could be duplicated on a separate chain, allowing generation of an exact same signature that can be replayed on another chain.
function mintReceipt(string memory questId_, bytes32 hash_, bytes memory signature_) public { if (quests[questId_].numberMinted + 1 > quests[questId_].totalParticipants) revert OverMaxAllowedToMint(); if (quests[questId_].addressMinted[msg.sender] == true) revert AddressAlreadyMinted(); if (keccak256(abi.encodePacked(msg.sender, questId_)) != hash_) revert InvalidHash(); if (recoverSigner(hash_, signature_) != claimSignerAddress) revert AddressNotSigned(); quests[questId_].addressMinted[msg.sender] = true; quests[questId_].numberMinted++; emit ReceiptMinted(msg.sender, questId_); rabbitholeReceiptContract.mint(msg.sender, questId_); }
Add in chainId, nonce and deadline timestamp for the signature hash generation.
#0 - c4-judge
2023-02-05T05:39:06Z
kirk-baird marked the issue as duplicate of #45
#1 - c4-judge
2023-02-14T09:36:40Z
kirk-baird marked the issue as satisfactory