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: 27/173
Findings: 4
Award: $159.60
🌟 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
After a quest finishes, there will be some tokens left over. Beside the protocolFee, some of these tokens belong to the quest owner, and some of them belong to participants who have not claimed their reward.
However, withdrawFee() can be called multiple times, and protocolFee() does not decrease. So, anyone can call the withdrawFee function multiple times to drain all the tokens from the Erc20Quest contract to the protocolFeeRecipient.
Add the following test to Erc20Quest.spec.ts. It successfully calls withdrawFee twice, getting twice the protocolFee it's supposed to.
describe('withdrawFee()', async () => { it('withdrawFee can be called multiple times', async () => { const beginningContractBalance = await deployedSampleErc20Contract.balanceOf(deployedQuestContract.address) await deployedFactoryContract.connect(firstAddress).mintReceipt(questId, messageHash, signature) await deployedQuestContract.start() await ethers.provider.send('evm_increaseTime', [86400]) expect(await deployedSampleErc20Contract.balanceOf(protocolFeeAddress)).to.equal(0) await deployedQuestContract.connect(firstAddress).claim() expect(await deployedSampleErc20Contract.balanceOf(firstAddress.address)).to.equal(1 * 1000) expect(beginningContractBalance).to.equal(totalRewardsPlusFee * 100) await ethers.provider.send('evm_increaseTime', [100001]) await deployedQuestContract.withdrawFee() expect(await deployedSampleErc20Contract.balanceOf(deployedQuestContract.address)).to.equal( totalRewardsPlusFee * 100 - 1 * 1000 - 200 ) expect(await deployedSampleErc20Contract.balanceOf(protocolFeeAddress)).to.equal(200) await deployedQuestContract.withdrawFee() expect(await deployedSampleErc20Contract.balanceOf(deployedQuestContract.address)).to.equal( totalRewardsPlusFee * 100 - 1 * 1000 - 400 ) expect(await deployedSampleErc20Contract.balanceOf(protocolFeeAddress)).to.equal(400) await ethers.provider.send('evm_increaseTime', [-100001]) await ethers.provider.send('evm_increaseTime', [-86400]) }) })
VSCode, hardhat
Maintain a counter initialized to 0 (say protocolFeeCount). Set it to receiptRedeemers() at the end of withdrawFee().
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L97
Then, in protocolFee(), this line should be changed to return ((receiptRedeemers() - protocolFeeCount)* rewardAmountInWeiOrTokenId * questFee) / 10_000;
#0 - c4-judge
2023-02-05T05:05:43Z
kirk-baird marked the issue as duplicate of #23
#1 - c4-judge
2023-02-14T09:00:07Z
kirk-baird marked the issue as satisfactory
🌟 Selected for report: carlitox477
Also found by: 0xRobocop, 0xbepresent, ArmedGoose, Atarpara, IllIllI, Tointer, UdarTeam, adriro, betweenETHlines, cryptojedi88, evan, glcanvas, gzeon, horsefacts, ladboy233, libratus, lukris02, luxartvinsec, manikantanynala97, minhquanym, mookimgo, p4st13r4, simon135, thekmj, trustindistrust
18.6976 USDC - $18.70
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L117-L123 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L99-L108
There are multiple unbounded for loops in the claim() function of Quest.sol.
The number of iterations that the loop runs depends on the caller's receipt balance. Since receipts can only be claimed once, claimed receipts are basically useless.
However, claimed receipts are not automatically burned, so a user can end up with many of these as they complete more quests. Eventually, the user will have to spend pay a significant amount of gas fee every time they claim their reward.
A malicious user can even speed up this process by transferring their claimed receipts to the victim.
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L99 Claim() calls getOwnedTokenIdsOfQuest().
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L117-L123 getOwnedTokenIdsOfQuest() loops through all the receipt tokens owned by the caller. Note that there are also other problematic for loops, but this is the one with the most number of iterations.
VSCode
The claim function wastes a lot of gas iterating through all the receipts owned by the sender since many of them are for other quests or have already been claimed.
One potential solution is to let the user specify the receipt they would like to claim (i.e. claim(uint tokenId)
)
Note that now the function would now need to check that
#0 - c4-judge
2023-02-05T05:01:41Z
kirk-baird marked the issue as duplicate of #135
#1 - c4-judge
2023-02-14T09:18:15Z
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 withdrawFee() has been called, then the value of nonClaimableTokens
in withdrawRemainingTokens() is incorrect. When this happens, the quest owner will get less tokens then they are supposed to when they call withdrawRemainingTokens()
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L85 Consider this line of code. Let the current balance = b.
If withdrawFee() is called immediately before withdrawRemainingTokens(), the caller (quest owner) will get b - protocolFee() - protocolFee() - unclaimedTokens
tokens back (since balance after withdrawFee is b - protocolFee()
)
If withdrawRemainingTokens() is not preceded by withdrawFee(), the caller (quest owner) will get b - protocolFee() - unclaimedTokens
tokens back
VSCode
Maintain a counter initialized to 0 (say protocolFeeCount). Set it to receiptRedeemers() at the end of withdrawFee().
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L97
Then, in protocolFee(), this line should be changed to return ((receiptRedeemers() - protocolFeeCount)* rewardAmountInWeiOrTokenId * questFee) / 10_000;
#0 - c4-judge
2023-02-05T05:07:56Z
kirk-baird marked the issue as duplicate of #42
#1 - c4-judge
2023-02-06T08:23:44Z
kirk-baird marked the issue as not a duplicate
#2 - c4-judge
2023-02-06T08:23:57Z
kirk-baird marked the issue as duplicate of #61
#3 - c4-judge
2023-02-14T10:01:12Z
kirk-baird marked the issue as satisfactory
#4 - c4-judge
2023-02-20T09:31:50Z
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)