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: 61/173
Findings: 2
Award: $40.31
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L219 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L81
The quest participants can mint receipts of a certain quest once they completed on-chain tasks. Also the quest creators put the endTime of the quest in the quest creation.
The quest creator uses the endTime because they are allow to pay rewards for those on-chain completed tasks for a certain period of time. If the Rabbithole protocol allows participants to mint receipts even after the Quest.endTime
then the quest creator will lost rewards tokens they don't wanted to lost.
The quest creator can withdraw the remaining tokens after the quest ends but if the protocol allows mint receipts even if the quest has ended then the quest creator will withdraw less tokens. The quest creator uses an endTime parameter because it is the range time they want to pay rewards for completed on-chain tasks, so mint receipts after the endTime will affect the quest creators rewards.
There is an off-chain protection because the participant needs a valid sig hash in order to mint the receipt but there should be a on-chain validation in order to protect the quest creators rewards.
As you can see in the mintReceipt
function, the participant can mint receipts even if the quest has ended.
File: QuestFactory.sol 219: function mintReceipt(string memory questId_, bytes32 hash_, bytes memory signature_) public { 220: if (quests[questId_].numberMinted + 1 > quests[questId_].totalParticipants) revert OverMaxAllowedToMint(); 221: if (quests[questId_].addressMinted[msg.sender] == true) revert AddressAlreadyMinted(); 222: if (keccak256(abi.encodePacked(msg.sender, questId_)) != hash_) revert InvalidHash(); 223: if (recoverSigner(hash_, signature_) != claimSignerAddress) revert AddressNotSigned(); 224: 225: quests[questId_].addressMinted[msg.sender] = true; 226: quests[questId_].numberMinted++; 227: emit ReceiptMinted(msg.sender, questId_); 228: rabbitholeReceiptContract.mint(msg.sender, questId_); 229: }
VSCode
Add a validation in the mintReceipt()
function where it is only possible to mint a receipt if the block.timestamp
is in the Quest.startTime/Quest.endTime
range.
#0 - c4-judge
2023-02-05T03:36:36Z
kirk-baird marked the issue as duplicate of #22
#1 - c4-judge
2023-02-14T08:47:35Z
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/Quest.sol#L99 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L109
The RabbitHole participant can have many receipts from all the quest he has participated. The RabbitHoleReceipt.sol::getOwnedTokenIdsOfQuest() function helps to get the receipts which are owned by the user per questId.
If the participant accumulate a lot of RabbitHoleReceipts the for statement which iterates through all receipts from the participant can run out of gas.
The Quest.sol::claim() function will be reverted for users who have many receipts causing the rewards may be trapped for the participants and the quest creator because the ERC20Quest.sol::withdrawRemainingTokens() is only able to withdraw the non claimable tokens.
The RabbitHoleReceipt.sol::getOwnedTokenIdsOfQuest()
function calculates the user balance in the line 113. Then the user balance is used in the for statement in the line 117.
The participant balance could be a large amount of receipts because the user can participate in many quests then the for
statement could be reverted by insufficient gas.
File: RabbitHoleReceipt.sol 109: function getOwnedTokenIdsOfQuest( 110: string memory questId_, 111: address claimingAddress_ 112: ) public view returns (uint[] memory) { 113: uint msgSenderBalance = balanceOf(claimingAddress_); 114: uint[] memory tokenIdsForQuest = new uint[](msgSenderBalance); 115: uint foundTokens = 0; 116: 117: for (uint i = 0; i < msgSenderBalance; i++) { 118: uint tokenId = tokenOfOwnerByIndex(claimingAddress_, i); 119: if (keccak256(bytes(questIdForTokenId[tokenId])) == keccak256(bytes(questId_))) { 120: tokenIdsForQuest[i] = tokenId; 121: foundTokens++; 122: } 123: }
I created a basic test where you can see the receipt is not burned after the rewards claim, then the participant is accumulating many receipts:
it('the receipt is not burned after the reward was claimed', async () => { // Receipts are not burned after the rewards claim() // 1. Mint a new Receipt for the firstAddress await deployedRabbitholeReceiptContract.mint(firstAddress.address, questId) // 2. Check the firstAddress receipt balance expect(await deployedRabbitholeReceiptContract.balanceOf(firstAddress.address)).to.equal(1) await deployedQuestContract.start() await ethers.provider.send('evm_increaseTime', [86400]) // 3. Claim the rewards as the firstAddress await deployedQuestContract.connect(firstAddress).claim() // 4. After the rewards was claimed, the receipt is not burned then the user is accumulating many receipts expect(await deployedRabbitholeReceiptContract.balanceOf(firstAddress.address)).to.equal(1) })
VSCode
If the rewards was claimed then burn the participant receipt.
#0 - c4-judge
2023-02-05T04:52:34Z
kirk-baird marked the issue as duplicate of #135
#1 - c4-judge
2023-02-14T09:17:06Z
kirk-baird changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-02-14T09:18:48Z
kirk-baird marked the issue as satisfactory