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: 29/173
Findings: 4
Award: $149.45
🌟 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#L100-L104 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L75-L79
The withdrawFee
for the Erc20Quest
contract can be called after the quest ended which it allows to send the protocol fee to the protocolFeeRecipient
however it doesn't check if it was already called. If the contract's token balance is multiple of the protocol fee it's possible for anyone to call witdrawFee
and grief all the people who didn't claim yet because when they will try the contract's token balance will be insufficient.
Assume the quest ended, the contract has 100 tokens, the protocol fee is 10, unclaimed tokens 90 (for simplicity we assume all the receipts were minted and all contract's token balance should go in rewards). A user calls withdrawFee
10 times and the unclaimed receipts won't be able to be claimed.
/// @notice Sends the protocol fee to the protocolFeeRecipient /// @dev Only callable when the quest is ended function withdrawFee() public onlyAdminWithdrawAfterEnd { IERC20(rewardToken).safeTransfer(protocolFeeRecipient, protocolFee()); }
modifier onlyAdminWithdrawAfterEnd() { if (block.timestamp < endTime) revert NoWithdrawDuringClaim(); _; }
Validate that withdrawFee
can be called only once.
#0 - c4-judge
2023-02-05T04:42:30Z
kirk-baird marked the issue as duplicate of #23
#1 - c4-judge
2023-02-14T09:00:13Z
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
Erc1155Quest's withdrawRemainingTokens
function allows the owner to claim all the remaining tokens, the unclaimed tickets will become worthless since they won't be claimable. Note this functionality for the Erc20Quest correctly take in consideration the unclaimed ticket remaining.
The quest ended and there are 100 unclaimed tickets corresponding to 100 ERC1155 token rewards. The owner calls withdrawRemainingTokens
and withdraw all the remaining tokens, the unclaimed tickets can't be claim anymore.
function withdrawRemainingTokens(address to_) public override onlyOwner { super.withdrawRemainingTokens(to_); IERC1155(rewardToken).safeTransferFrom( address(this), to_, rewardAmountInWeiOrTokenId, IERC1155(rewardToken).balanceOf(address(this), rewardAmountInWeiOrTokenId), '0x00' ); }
In withdrawRemainingTokens
the correct amount of tokens to withdraw should be contract's ERC1155 tokens balance - unclaimed tokens (questFactoryContract.getNumberMinted(questId) - redeemedTokens).
#0 - c4-judge
2023-02-05T04:47:39Z
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:14Z
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: carrotsmuggler
Also found by: AkshaySrivastav, ElKu, HollaDieWaldfee, Iurii3, KmanOfficial, adriro, bin2chen, evan, hansfriese, hl_, mert_eren, omis, peanuts
122.948 USDC - $122.95
The withdrawRemainingTokens
function leaves tokens (for an amount equal to protocolFee
) in the contract when withdraFee
is called earlier due to non tracking if withdraFee was already called.
Assume balance = 150, unclaimedTokens = 0, protocolFee = 50. withdrawFee is called and balance becomes 100. When calling withdrawRemainingTokens
nonClaimableTokens will be 100 - 50 = 50. 50 tokens are lost in the contract.
function withdrawRemainingTokens(address to_) public override onlyOwner { super.withdrawRemainingTokens(to_); uint unclaimedTokens = (receiptRedeemers() - redeemedTokens) * rewardAmountInWeiOrTokenId; uint256 nonClaimableTokens = IERC20(rewardToken).balanceOf(address(this)) - protocolFee() - unclaimedTokens; IERC20(rewardToken).safeTransfer(to_, nonClaimableTokens); } function withdrawFee() public onlyAdminWithdrawAfterEnd { IERC20(rewardToken).safeTransfer(protocolFeeRecipient, protocolFee()); }
Keep track if withdrawFee
is called and if yes don't subtracts protocolFee when computing nonClaimableTokens.
#0 - c4-judge
2023-02-05T04:45:43Z
kirk-baird marked the issue as duplicate of #42
#1 - c4-judge
2023-02-06T08:21:43Z
kirk-baird marked the issue as not a duplicate
#2 - c4-judge
2023-02-06T08:21:50Z
kirk-baird marked the issue as duplicate of #61
#3 - c4-judge
2023-02-14T10:01:18Z
kirk-baird marked the issue as satisfactory
#4 - c4-judge
2023-02-20T09:32:38Z
kirk-baird changed the severity to 3 (High Risk)
#5 - c4-judge
2023-02-23T23:48:11Z
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
A user can claim the same receipt across different chains under certain conditions because the chainId
is not included in the hashed data.
The hashed data contains only the msg.sender
and questId_
, if the following conditions hold true the same signature can be reused in different chains:
claimSignerAddress
is the same addressquestId_
and there are tokens in the contractfunction 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_); }
As a result a user can complete a quest in a single chain and get the rewards in all of them.
Use the EIP-712 standard and validate the chainId
#0 - c4-judge
2023-02-05T04:40:13Z
kirk-baird marked the issue as duplicate of #45
#1 - c4-judge
2023-02-14T09:36:08Z
kirk-baird changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-02-14T09:36:59Z
kirk-baird marked the issue as satisfactory