RabbitHole Quest Protocol contest - lukris02'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: 44/173

Findings: 5

Award: $69.59

QA:
grade-b
Gas:
grade-b

🌟 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#L76-L79

Vulnerability details

Impact

It is possible to call the function withdrawFee() many times. Note that even though the function has the modifier onlyAdminWithdrawAfterEnd, actually anyone (not only admin) can call the function. So, when quest has ended and the Erc20Quest_contrcat_balance > 0 (possible if there are unclaimedTokens left and/or the owner did not withdraw remaining tokens), attacker can withdraw fees until the balance of the contract runs out. Although the attacker will not directly benefit, the cost of the attack is relatively small, and the owner of the quest contract and users who have not yet claimed the reward can lose their assets.

Proof of Concept

For example:

  • the function protocolFee() returns 5
  • 12 users did not claim reward (assume rewardAmountInWei = 1) ( => Quest contract should have balance at least 12)
  • Imagine there are 2 more tokens that can be withdrawn by owner

So, contract balance = 14.

  1. Attacker calls the function withdrawFee() 2 times. The contract balance becomes 4.
  2. Attacker can deposit 1 token and call the function withdrawFee() one more time. The contract balance becomes 0.

As a result, 12 users can not claim rewards and owner can not withdraw any tokens.

Tools Used

Manual review.

Add the state variable bool isFeeWithdrawn to Erc20Quest.sol and change the function:

function withdrawFee() public onlyAdminWithdrawAfterEnd { if(isFeeWithdrawn) revert FeeWithdrawn(); //+ isFeeWithdrawn = true; //+ IERC20(rewardToken).safeTransfer(protocolFeeRecipient, protocolFee()); }

#0 - c4-judge

2023-02-06T09:04:47Z

kirk-baird marked the issue as duplicate of #23

#1 - c4-judge

2023-02-14T08:55:49Z

kirk-baird marked the issue as satisfactory

Awards

21.6061 USDC - $21.61

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
duplicate-601

External Links

Lines of code

https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/QuestFactory.sol#L219-L229

Vulnerability details

Impact

The function mintReceipt has no check that block.timestamp < quest endTime. So, it is possible to mint receipts after the quest has ended which can lead to loss of assets. If the owner of the quest contract withdraws the remaining tokens immediately after the end of the quest, assets can be lost by users, who did not claim rewards, or the protocol.

Proof of Concept

Several scenarios are possible:

  1. The quest has ended (but quest.numberMinted < quest.totalParticipants), all users claimed rewards (unclaimedTokens = 0), protocol withdrawn fees, owner withdrawn remaining tokens. Contract balance = 0.
    User mints Receipt and can not claim rewards because there are no tokens left on quest contract balance.
  2. The quest has ended (but quest.numberMinted < quest.totalParticipants), not all users claimed rewards, protocol withdrawn fees, owner withdrawn remaining tokens.
    User mints Receipt and claim rewards but one of the users who has not yet claimed a reward will not be able to receive a reward because there are not enough tokens.
  3. The quest has ended (but quest.numberMinted < quest.totalParticipants), all users claimed rewards (unclaimedTokens = 0), protocol did not withdraw fees, owner withdrawn remaining tokens. User mints Receipt and claim rewards but protocol will not be able to receive fees because there are not enough tokens.

Tools Used

Manual review.

Restrict the ability to mintReceipt when block.timestamp > quest.endTime.

#0 - c4-judge

2023-02-06T09:18:23Z

kirk-baird marked the issue as duplicate of #22

#1 - c4-judge

2023-02-14T08:42:51Z

kirk-baird changed the severity to 2 (Med Risk)

#2 - c4-judge

2023-02-14T08:43:33Z

kirk-baird marked the issue as satisfactory

Awards

18.6976 USDC - $18.70

Labels

bug
2 (Med Risk)
satisfactory
duplicate-552

External Links

Lines of code

https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/Quest.sol#L96-L118 https://github.com/rabbitholegg/quest-protocol/blob/8c4c1f71221570b14a0479c216583342bd652d8d/contracts/RabbitHoleReceipt.sol#L109-L135

Vulnerability details

Impact

If a user has many ERC-721 tokens and needs to claim a lot of quest tokens, the claim() function can be DOSed due to out-of-gas error. User's rewards will be locked until they sell some tokens.

Proof of Concept

The function claim() contains gas consuming loop, and also it calls two gas consuming functions: _setClaimed (has 1 loop) and getOwnedTokenIdsOfQuest() (has 2 loops).

Tools Used

Manual review.

Consider allowing users to specify what token ids they want to claim and just check that they own these tokens instead of iterating and filtering all user's tokens.

#0 - c4-judge

2023-02-06T22:31:05Z

kirk-baird marked the issue as duplicate of #135

#1 - c4-judge

2023-02-14T09:17:36Z

kirk-baird marked the issue as satisfactory

QA Report for RabbitHole Quest Protocol contest

Overview

During the audit, 8 low and 7 non-critical issues were found.

β„–TitleRisk RatingInstance Count
L-1Misleading modifierLow1
L-2Owner can renounce ownershipLow1
L-3Event should be emittedLow19
L-4Quest startTime is not checkedLow1
L-5ECDSAUpgradeable.recover return value is not checkedLow2
L-6Max limit can be setLow1
L-7Sign changeLow1
L-8Checks can be addedLow3
NC-1Unused named return variablesNon-Critical2
NC-2Order of FunctionsNon-Critical1
NC-3Order of LayoutNon-Critical3
NC-4Missing leading underscoresNon-Critical2
NC-5TyposNon-Critical1
NC-6Open TODOsNon-Critical1
NC-7Maximum line length exceededNon-Critical1

Low Risk Findings(8)

L-1. Misleading modifier

Description

The modifier onlyAdminWithdrawAfterEnd() does not check that only admin can call a fucntion, actually anyone can call it.

Instances
modifier onlyAdminWithdrawAfterEnd() { if (block.timestamp < endTime) revert NoWithdrawDuringClaim(); _; }
Recommendation

Add a check in modifier or rename it.

L-2. Owner can renounce ownership

Description

Openzeppelin's Ownable.sol implements renounceOwnership() function which leaves the contract without an owner and removes any functionality that is only available to the them.

Instances
Recommendation

Consider reimplementing the function to disable it.

L-3. Event should be emitted

Description

The governor functions that change critical parameters should emit events. Events allow capturing the changed parameters so that off-chain tools/interfaces can register such changes and allow users to evaluate them and consider if they would like to engage/exit based on how they perceive the changes as affecting the trustworthiness of the protocol or profitability of the implemented financial services. The alternative of directly querying on-chain contract state for such changes is not considered practical for most users/usages.

Instances
Recommendation

Consider adding events for these functions.

L-4. Quest startTime is not checked

Description

The function mintReceipt() does not check that quest has started.

Instances
Recommendation

Consider adding the check.

L-5. ECDSAUpgradeable.recover return value is not checked

Description

ECDSAUpgradeable.recover may return address(0) if invoked with an invalid signature. So, it is possible to mintReceipt() with invalid signature, if claimSignerAddress accidentally will be set to address(0). claimSignerAddress can be equal to address(0) if it is stay unset in initialize() or incorrectly set in setClaimSignerAddress() function.

Instances
Recommendation

ECDSAUpgradeable.recover return value must be checked in the function recoverSigner() to ensure that it is not zero address.

L-6. Max limit can be set

Description

royaltyFee is unlimited.

Instances
Recommendation

It is better to limit the maximum royaltyFee to increase transparency.

L-7. Sign change

Description

It is possible that some users will participate in the quest at the last second, so the < sign needs to be change to <=.

Instances
Recommendation

Consider changing to: if (block.timestamp <= endTime) revert NoWithdrawDuringClaim();.

L-8. Checks can be added

Description

In Quest constructor it can be checked that:

  • uint256 totalParticipants_> 0,
  • endTime_ - startTime_ > minimum duration,
  • endTime_ - startTime_ < maximum duration.
Instances
Recommendation

Consider adding more check.

Non-Critical Risk Findings(7)

NC-1. Unused named return variables

Description

Both named return variable(s) and return statement are used.

Instances
Recommendation

To improve clarity use only named return variables.
For example, change:

function functionName() returns (uint id) { return x;

to

function functionName() returns (uint id) { id = x;

NC-2. Order of Functions

Description

According to Style Guide, ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier.
Functions should be grouped according to their visibility and ordered:

  1. constructor
  2. receive function (if exists)
  3. fallback function (if exists)
  4. external
  5. public
  6. internal
  7. private
Instances
Recommendation

Reorder functions where possible.

NC-3. Order of Layout

Description

According to Order of Layout, inside each contract, library or interface, use the following order:

  1. Type declarations
  2. State variables
  3. Events
  4. Modifiers
  5. Functions
Instances
Recommendation

Place modifiers before constructor.

NC-4. Missing leading underscores

Description

Internal and private state variables and functions should have a leading underscore:

Instances
Recommendation

Add leading underscores where needed.

NC-5. Typos

Instances

NC-6. Open TODOs

Instances
Recommendation

Resolve issues.

NC-7. Maximum line length exceeded

Description

According to Style Guide, maximum suggested line length is 120 characters. Longer lines make the code harder to read.

Instances
Recommendation

Make the lines shorter.

#0 - kirk-baird

2023-02-06T23:24:18Z

L-5 is invalid as the zero check is performed by OpenZeppelin here

#1 - c4-sponsor

2023-02-07T22:03:34Z

waynehoover marked the issue as sponsor confirmed

#2 - c4-judge

2023-02-14T09:50:51Z

kirk-baird marked the issue as grade-b

Gas Optimizations Report for RabbitHole Quest Protocol contest

Overview

During the audit, 2 gas issues were found.
Total savings ~2415.

β„–TitleInstance CountSaved
G-1Extra sstore12100
G-2Use unchecked blocks for incrementing9315

Gas Optimizations Findings(2)

G-1. Extra sstore

Description

There is no need to make isPaused false because it can not be true before quest start.

Instances
Saved

2100 gas

G-2. Use unchecked blocks for incrementing

Description

In Solidity 0.8+, there’s a default overflow and underflow check on unsigned integers. In the loops or similar cases, "i" will not overflow because the loop will run out of gas before that or max value never be reached.

Instances
Recommendation

Change:

for (uint256 i; i < n; ++i) { // ... }

to:

for (uint256 i; i < n;) { // ... unchecked { ++i; } }
Saved

This saves ~30-40 gas per iteration.
So, ~35*9 = 315

#0 - c4-judge

2023-02-15T21:53:34Z

kirk-baird marked the issue as grade-b

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