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

Findings: 2

Award: $13.92

Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Lines of code

https://github.com/rabbitholegg/quest-protocol/blob/32a9b58f040442af4c7f459fe409e40af0e54a78/contracts/RabbitHoleTickets.sol#L48 https://github.com/rabbitholegg/quest-protocol/blob/32a9b58f040442af4c7f459fe409e40af0e54a78/contracts/RabbitHoleTickets.sol#L47-L50 https://github.com/rabbitholegg/quest-protocol/blob/32a9b58f040442af4c7f459fe409e40af0e54a78/contracts/RabbitHoleTickets.sol#L92 https://github.com/rabbitholegg/quest-protocol/blob/32a9b58f040442af4c7f459fe409e40af0e54a78/contracts/RabbitHoleTickets.sol#L83 https://github.com/rabbitholegg/quest-protocol/blob/bd213e8629bb8587dd4bb35f3e9e8f8e42b40336/contracts/RabbitHoleReceipt.sol#L98

Vulnerability details

Minting can be called by anyone

Summary

Modifier is wrongly implemented, so every function that uses onlyMinter will be callable by anyone.

This affects:

Vulnerability Detail

Modifier has no if + revert / require statement within the condition, therefore, it will always pass the "checks"

modifier onlyMinter() {
    msg.sender == minterAddress;
    _;
}

This means one can mint all the tokens / NFTs he wants and claim rewards after that (Claim flow).

Impact

Minting NFTs even if it shouldn't be able. Callable as much as wanted, direct impact on the value of the protocol.

Code Snippet

https://github.com/rabbitholegg/quest-protocol/blob/32a9b58f040442af4c7f459fe409e40af0e54a78/contracts/RabbitHoleTickets.sol#L48

https://github.com/rabbitholegg/quest-protocol/blob/32a9b58f040442af4c7f459fe409e40af0e54a78/contracts/RabbitHoleTickets.sol#L47-L50

Tool used

Manual Review

Recommendation

modifier onlyMinter() {
-   msg.sender == minterAddress;
+   if(msg.sender != minterAddress) revert NOT_MINTER_ERROR;
    _;
}

#0 - c4-judge

2023-02-06T22:58:36Z

kirk-baird marked the issue as duplicate of #9

#1 - c4-judge

2023-02-14T08:36:56Z

kirk-baird marked the issue as satisfactory

Gas

Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct, where appropriate

if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key’s keccak256 hash (Gkeccak256 - 30 gas) and that calculation’s associated stack operations.

Both mapping being used in the same functions mostly consider making them a struct instead

state variables can be packed into fewer storage slots

If variables occupying the same slot are both written the same function or by the constructor, avoids a separate Gsset (20000 gas). Reads of the variables are also cheaper.

consider making this state var near one of the address types

<x> += <y> costs more gas than <x> = <x> + <y> for state variables

Using the addition operator instead of plus-equals saves gas

Not using the named return variables when a function returns, wastes deployment gas

++i / i++ should be unchecked{++i} / unchecked{i++} when it is not possible for them to overflow, as is the case when used in for-loop and while-loops

In Solidity 0.8+, there’s a default overflow check on unsigned integers. It’s possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.

internal functions only called once can be inlined to save gas

Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.

  • QuestFactory.sol#L152 function grantDefaultAdminAndCreateQuestRole(address account_) internal {

  • RabbitHoleReceipt.sol#L139 function burn(uint256 tokenId) internal override(ERC721Upgradeable, ERC721URIStorageUpgradeable) {

  • Quest.sol#L122 function calculateRewards(uint256 redeemableTokenCount) internal virtual returns (uint256) {

  • Quest.sol#L129 function transferRewards(uint256 amount) internal virtual {

  • Erc20Quest.sol#L66 function transferRewards(uint256 amount) internal override {

  • Erc20Quest.sol#L74 function calculateRewards(uint256 redeemableTokenCount) internal view override returns (uint256) {

  • Erc1155Quest.sol#L41 function transferRewards(uint256 amount) internal override {

  • Erc1155Quest.sol#L48 function calculateRewards(uint256 redeemableTokenCount) internal pure override returns (uint256) {

  • RabbitHoleReceipt.sol#L153 ) internal override(ERC721Upgradeable, ERC721EnumerableUpgradeable) {

Extra check on booleans

There are instances where a boolean variable / function is checked against a boolean unnecessarily wasting extra ~18 gas for instance

  • Checks can be simplified from variable == false to !variable.

  • Checks can be simplified from variable == true to variable.

  • QuestFactory.sol#L221 if (quests[questId_].addressMinted[msg.sender] == true) revert AddressAlreadyMinted();

  • Quest.sol#L136 return claimedList[tokenId_] == true;

  • QuestFactory.sol#L73 if (rewardAllowlist[rewardTokenAddress_] == false) revert RewardNotAllowed();

Use Custom Errors

NOTE: None of these findings where found by 4naly3er output - Gas

Source Instead of using error strings, to reduce deployment and runtime cost, you should use Custom Errors. This would save both deployment and runtime cost.

#0 - c4-judge

2023-02-06T09:11:21Z

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