The RabbitHoleReceipt token contract represents a receipt of a completed task for a quest. These tokens are minted for specific quests after a user successfully completes a task, and can be claimed just once using the claim function present in the Quest contract:
function claim() public virtual onlyQuestActive {
if (isPaused) revert QuestPaused();
uint[] memory tokens = rabbitHoleReceiptContract.getOwnedTokenIdsOfQuest(questId, msg.sender);
if (tokens.length == 0) revert NoTokensToClaim();
uint256 redeemableTokenCount = 0;
for (uint i = 0; i < tokens.length; i++) {
if (!isClaimed(tokens[i])) {
redeemableTokenCount++;
}
}
if (redeemableTokenCount == 0) revert AlreadyClaimed();
uint256 totalRedeemableRewards = _calculateRewards(redeemableTokenCount);
_setClaimed(tokens);
_transferRewards(totalRedeemableRewards);
redeemedTokens += redeemableTokenCount;
emit Claimed(msg.sender, totalRedeemableRewards);
}
This function queries the receipt token contract to determine which of the tokens the user holds was minted for that specific quest id. It marks the token ids as claimed and hands the rewards based on how many of those were unclaimed. The receipt token isn't burned or removed from the caller, it is just flagged as used internally in the quest contract.
Impact
A user can be tricked into buying a claimed receipt that doesn't entitle any reward. As receipt tokens are still in possession of the caller after they are claimed, a bad actor can claim the reward and still list the NFT in a secondary market.
In a similar way, another feasible attack would be using lending protocols. A bad actor can borrow or flash loan an unclaimed receipt, claim the rewards, and return it back.
PoC
First scenario:
Attacker mints a receipt after completing a task or buys an unclaimed receipt.
Attacker claims the rewards in the corresponding quest.
Attacker lists the receipt token in a market.
Victim buys the receipt token expecting a reward, though the receipt is already claimed.
Second scenario:
Victim lists unclaimed reward token in exchange or lending platform.
Attacker borrows or uses flash loans to get the receipt token.
Attacker claims the rewards in the corresponding quest.
Attacker returns the receipt token. Victim is left with a claimed receipt.
Recommendation
Given the current architecture of the solution, the most straightforward solution would be to either burn the receipt token or transfer it from the caller when the claim function is executed.
Lines of code
https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L96-L118
Vulnerability details
The
RabbitHoleReceipt
token contract represents a receipt of a completed task for a quest. These tokens are minted for specific quests after a user successfully completes a task, and can be claimed just once using theclaim
function present in theQuest
contract:This function queries the receipt token contract to determine which of the tokens the user holds was minted for that specific quest id. It marks the token ids as claimed and hands the rewards based on how many of those were unclaimed. The receipt token isn't burned or removed from the caller, it is just flagged as used internally in the quest contract.
Impact
A user can be tricked into buying a claimed receipt that doesn't entitle any reward. As receipt tokens are still in possession of the caller after they are claimed, a bad actor can claim the reward and still list the NFT in a secondary market.
In a similar way, another feasible attack would be using lending protocols. A bad actor can borrow or flash loan an unclaimed receipt, claim the rewards, and return it back.
PoC
First scenario:
Second scenario:
Recommendation
Given the current architecture of the solution, the most straightforward solution would be to either burn the receipt token or transfer it from the caller when the
claim
function is executed.