RabbitHole Quest Protocol contest - Bauer's results

A protocol to distribute token rewards for completing on-chain tasks.

General Information

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

RabbitHole

Findings Distribution

Researcher Performance

Rank: 172/173

Findings: 1

Award: $0.75

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L102-L104 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L96-L118 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Erc20Quest.sol#L81-L87

Vulnerability details

Impact

Function call withdrawfee() can be called by anyone and will result in the user not being able to cliam

Proof of Concept

function withdrawFee() public onlyAdminWithdrawAfterEnd { IERC20(rewardToken).safeTransfer(protocolFeeRecipient, protocolFee()); } modifier onlyAdminWithdrawAfterEnd() { if (block.timestamp < endTime) revert NoWithdrawDuringClaim(); _; }

Function call withdrawFee() have only one modifier onlyAdminWithdrawAfterEnd() which prevents reward withdrawal until the Quest has ended . However, it can be called by anyone, if the bad actor call withdrawFee() many times after endtime, the protocol may transfer remaining tokens to receipt which result in the user not being able to cliam

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); }

And if withdrawFee() is called before the withdrawRemainingTokens() call,the nonClaimableTokens is not calculated correctly. It should be

uint256 nonClaimableTokens = IERC20(rewardToken).balanceOf(address(this)) - unclaimedTokens;

Tools Used

Vscode

function withdrawFee() public onlyOwner onlyAdminWithdrawAfterEnd { IERC20(rewardToken).safeTransfer(protocolFeeRecipient, protocolFee()); }

#0 - c4-judge

2023-02-05T05:23:33Z

kirk-baird marked the issue as duplicate of #23

#1 - c4-judge

2023-02-14T08:54:38Z

kirk-baird changed the severity to 3 (High Risk)

#2 - c4-judge

2023-02-14T08:59:59Z

kirk-baird marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter