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: 52/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
Erc20Quest.withdrawFee can be called by anyone multiple times in order to sent more tokens to fee recepient and make Erc20Quest insolvent.
When Erc20Quest is started, then owner tops it up with amount that is enough to cover maximum amount of redeems and maximum fee for that.
Once quest is finished anyone can call Erc20Quest.withdrawFee
function in order to send protocol fee amount.
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L96-L104
function protocolFee() public view returns (uint256) { return (receiptRedeemers() * rewardAmountInWeiOrTokenId * questFee) / 10_000; } /// @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()); }
This function just sends amount, that depends on how many tokens where minted for the current quest. Even that it uses onlyAdminWithdrawAfterEnd
modifier doesn't restrict anyone else from calling it because modifier doesn't check if caller is owner.
Pls, note, that there is no any check that fee was already sent and this function can be called as many times as contract still has enough funds to send to protocolFeeRecipient. That means that by calling this function more than 1 time, fee recepient will receive more fees, then he should and contract can become insolvent(depends on amount of minted receipts for the quest).
Why i think this is high severity here is because if this function was only callable by admin, then he should be malicious to do that. But in this case anyone who is interested can make contract insolvent and receipt holders will not be able to redeem them.
VsCode
Create boolean variable that is set, once fee is paid.
#0 - c4-judge
2023-02-03T04:35:57Z
kirk-baird marked the issue as satisfactory
#1 - c4-judge
2023-02-03T05:11:04Z
kirk-baird marked the issue as primary issue
#2 - c4-sponsor
2023-02-07T20:25:43Z
waynehoover marked the issue as disagree with severity
#3 - waynehoover
2023-02-07T20:26:35Z
The funds get transferred back to us in this scenario, so there is no loss of funds here.
#4 - kirk-baird
2023-02-14T08:52:02Z
While the loss of funds is not direct as they are transferred to protocolFeeRecipient
. However, this may be called multiple times by anyone I believe this severely impacts the protocol functionality and will require calculations and a significant amount of gas to redistributed the funds to the correct owners.
Hence, I still consider this a valid High.
#5 - c4-judge
2023-02-14T08:55:16Z
kirk-baird marked issue #587 as primary and marked this issue as a duplicate of 587
🌟 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
QuestFactory.mintReceipt should not be able to mint token for quest that is not started yet
QuestFactory.mintReceipt function is called by users in order to mint new receipt token for them for the specific quest. https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L219-L229
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_); }
As you can see this function do some validations, but it's not checking that quest has started.
There are 2 problems that i see here. 1.Quest is toped up by owner, but isn't started yet. So it has tokens inside. If owner changed his mind he can just wait till finish of quest and call withdrawRemainingTokens function which will send all amount to him. But in case if QuestFactory will mint any tokens before quest is started, then owner will not be able to withdraw all amount, even that he didn't start the quest. 2.Quest is not toped up and not started. Factory mints receipts for this quest for any reason. But this receipts are not backed with redeemable amount inside Quest contract. If owner will not start it, then noone will receive nothing.
I understand that it will be another service that allows users to start quest and will generate hashes for them that can be changed to receipt, but i believe that the check should be present on chain that will prevent any actions in case if smth will happen and users will be able to receive hashes before quest is actually started.
VsCode
Add check that auction is started in order to mint receipts.
#0 - c4-judge
2023-02-05T03:36:54Z
kirk-baird marked the issue as duplicate of #22
#1 - c4-judge
2023-02-14T08:48:21Z
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
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L219-L229 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L81-L87
Late caller of QuestFactory.mintReceipt can lose his redeem amount.
When user participate in quest he receives signed message from indexer service that then can be changed to receipt by using QuestFactory.mintReceipt function. This function will increment amount of receipts minted for that quest. Currently there is no any time restrictions when user can call this function.
Each quest has start and end time. Once end time has passed then owner can call withdrawRemainingTokens
function. Let's check how it works inside Erc20Quest contract.
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L81-L87
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); } /// @notice Call the QuestFactory contract to get the amount of receipts that have been minted /// @return The amount of receipts that have been minted for the given quest function receiptRedeemers() public view returns (uint256) { return questFactoryContract.getNumberMinted(questId); }
As you can see this function will check in Factory how many tokens were minted for this quest in order to leave that amount inside contract and not transfer it to owner.
So here is problem scenario. 1.Quest is started. User participated and received signed message that he can change for receipt token. 2.For some reasons he doesn't call QuestFactory.mintReceipt for a long time, so quest has finished. 3.Owner wants to withdraw remaining tokens and calls Erc20Quest.withdrawRemainingTokens. Because user didn't call QuestFactory.mintReceipt, amount of minted receipts was not increased for the quest, so owner withdraws more. 4.User calls QuestFactory.mintReceipt. 5.In case if all redeemers already redeemed their receipts for the rewards, then user will not be able to get funds. Otherwise he will do that, but someone else, who will be the last will not be able to redeem.
Why i believe this is high severity, because it's very likely that user will call QuestFactory.mintReceipt function not right after they received their signed message. So this increases the amount of people who will call function when quest is already finished. And because of that the amount of people who will receive nothing increases.
VsCode
Think about some window period after the quest end, when users still can change their signed message to receipt. After that period revert when function is called. And also do not allow owner to withdraw funds till that window is finished.
#0 - c4-judge
2023-02-07T01:00:15Z
kirk-baird marked the issue as duplicate of #42
#1 - c4-judge
2023-02-10T05:03:12Z
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:27:15Z
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
Signed message can be reused on another chain to mint quest receipt.
When user participate in contest he receives signed message from off chain service, that allows him to mint receipt and claim rewards. Currently this signed message contains account and quest. Then signer is checked to be claimSignerAddress.
Because protocol is planned to be used on different chains, such check is not enough.
In case if same claimSignerAddress
will be used on different chains, then user will be able to reuse his signed message from one chain on another one without participating in contests.
Scenario. 1.On chainA quest with id 1 is started. User participated and received signed message that he exchanged for receipt. 2.On chainB quest with id 1 is started. User just used same signed message and exhanged it for receipt for free.
VsCode
Add chainId
field to the signed message to restrict replay on another chain.
#0 - c4-judge
2023-02-05T02:51:37Z
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:39:00Z
kirk-baird marked the issue as satisfactory