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: 127/173
Findings: 1
Award: $11.33
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0xAgro, 0xSmartContract, 0xhacksmithh, 0xngndev, Aymen0909, Bnke0x0, Breeje, Deivitto, Diana, Dug, Iurii3, LethL, MiniGlome, NoamYakov, RaymondFam, ReyAdmirado, Rolezn, SAAJ, adriro, ali, arialblack14, atharvasama, c3phas, carlitox477, catellatech, chaduke, cryptonue, cryptostellar5, ddimitrov22, dharma09, doublesharp, favelanky, georgits, glcanvas, gzeon, halden, horsefacts, jasonxiale, joestakey, karanctf, lukris02, matrix_0wl, nadin, navinavu, saneryee, shark, thekmj
11.3269 USDC - $11.33
immutable
Avoids a Gusset (20000 gas)
quest-protocol/contracts/Quest.sol::19 => bool public hasStarted; quest-protocol/contracts/Quest.sol::20 => bool public isPaused; quest-protocol/contracts/Quest.sol::21 => string public questId; quest-protocol/contracts/Quest.sol::22 => uint256 public redeemedTokens; quest-protocol/contracts/QuestFactory.sol::26 => address public claimSignerAddress; quest-protocol/contracts/QuestFactory.sol::27 => address public protocolFeeRecipient; quest-protocol/contracts/QuestFactory.sol::29 => RabbitHoleReceipt public rabbitholeReceiptContract; quest-protocol/contracts/QuestFactory.sol::31 => uint public questFee; quest-protocol/contracts/QuestFactory.sol::32 => uint public questIdCount; quest-protocol/contracts/RabbitHoleReceipt.sol::31 => address public royaltyRecipient; quest-protocol/contracts/RabbitHoleReceipt.sol::32 => address public minterAddress; quest-protocol/contracts/RabbitHoleReceipt.sol::33 => uint public royaltyFee; quest-protocol/contracts/RabbitHoleReceipt.sol::35 => ReceiptRenderer public ReceiptRendererContract; quest-protocol/contracts/RabbitHoleReceipt.sol::36 => IQuestFactory public QuestFactoryContract; quest-protocol/contracts/RabbitHoleTickets.sol::22 => address public royaltyRecipient; quest-protocol/contracts/RabbitHoleTickets.sol::23 => address public minterAddress; quest-protocol/contracts/RabbitHoleTickets.sol::24 => uint public royaltyFee; quest-protocol/contracts/RabbitHoleTickets.sol::25 => TicketRenderer public TicketRendererContract
If variables occupying the same slot are both written the same function or by the constructor avoids a separate Gsset (20000 gas).
quest-protocol/contracts/QuestFactory.sol::26 => address public claimSignerAddress; quest-protocol/contracts/QuestFactory.sol::27 => address public protocolFeeRecipient; quest-protocol/contracts/RabbitHoleReceipt.sol::31 => address public royaltyRecipient; quest-protocol/contracts/RabbitHoleReceipt.sol::32 => address public minterAddress; quest-protocol/contracts/RabbitHoleTickets.sol::22 => address public royaltyRecipient; quest-protocol/contracts/RabbitHoleTickets.sol::23 => address public minterAddress;
<array>.length
should not be looked up in every loop of a for
loopEven memory arrays incur the overhead of bit tests and bit shifts to calculate the array length. Storage array length checks incur an extra Gwarmaccess (100 gas) PER-LOOP.
quest-protocol/contracts/Quest.sol::70 => for (uint i = 0; i < tokenIds_.length; i++) { quest-protocol/contracts/Quest.sol::104 => for (uint i = 0; i < tokens.length; i++) {
++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
and while
loopsquest-protocol/contracts/Quest.sol::70 => for (uint i = 0; i < tokenIds_.length; i++) { quest-protocol/contracts/Quest.sol::104 => for (uint i = 0; i < tokens.length; i++) { quest-protocol/contracts/RabbitHoleReceipt.sol::117 => for (uint i = 0; i < msgSenderBalance; i++) { quest-protocol/contracts/RabbitHoleReceipt.sol::128 => for (uint i = 0; i < msgSenderBalance; i++) {
quest-protocol/contracts/Quest.sol::70 => for (uint i = 0; i < tokenIds_.length; i++) { quest-protocol/contracts/Quest.sol::104 => for (uint i = 0; i < tokens.length; i++) { quest-protocol/contracts/RabbitHoleReceipt.sol::117 => for (uint i = 0; i < msgSenderBalance; i++) { quest-protocol/contracts/RabbitHoleReceipt.sol::128 => for (uint i = 0; i < msgSenderBalance; i++) {
++i
costs less gas than i++
, especially when it’s used in forloops (--i
/i--
too)quest-protocol/contracts/Quest.sol::70 => for (uint i = 0; i < tokenIds_.length; i++) { quest-protocol/contracts/Quest.sol::104 => for (uint i = 0; i < tokens.length; i++) { quest-protocol/contracts/RabbitHoleReceipt.sol::117 => for (uint i = 0; i < msgSenderBalance; i++) { quest-protocol/contracts/RabbitHoleReceipt.sol::128 => for (uint i = 0; i < msgSenderBalance; i++) {
uints
/ints
smaller than 32 bytes (256 bits) incurs overheadWhen using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html Use a larger size then downcast where needed
quest-protocol/contracts/ReceiptRenderer.sol::60 => generateAttribute('Reward Address', Strings.toHexString(uint160(rewardAddress_), 20)),
keccak256()
, should use immutable
rather than constant
See this issue for a detailed description of the issue
quest-protocol/contracts/QuestFactory.sol::17 => bytes32 public constant CREATE_QUEST_ROLE = keccak256('CREATE_QUEST_ROLE');
require()
/revert()
checks should be refactored to a modifier or functionquest-protocol/contracts/Quest.sol::123 => revert MustImplementInChild();
require()
or revert()
statements that check input arguments should be at the top of the functionChecks that involve constants should come before checks that involve state variables
quest-protocol/contracts/RabbitHoleReceipt.sol::162 => require(QuestFactoryContract != IQuestFactory(address(0)), 'QuestFactory not set');
revert()
/require()
strings to save deployment gasquest-protocol/contracts/RabbitHoleReceipt.sol::161 => require(_exists(tokenId_), 'ERC721URIStorage: URI query for nonexistent token'); quest-protocol/contracts/RabbitHoleReceipt.sol::162 => require(QuestFactoryContract != IQuestFactory(address(0)), 'QuestFactory not set'); quest-protocol/contracts/RabbitHoleReceipt.sol::182 => require(_exists(tokenId_), 'Nonexistent token');
payable
If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided.
quest-protocol/contracts/Erc1155Quest.sol::54 => function withdrawRemainingTokens(address to_) public override onlyOwner { quest-protocol/contracts/Erc20Quest.sol::81 => function withdrawRemainingTokens(address to_) public override onlyOwner { quest-protocol/contracts/Erc20Quest.sol::102 => function withdrawFee() public onlyAdminWithdrawAfterEnd { quest-protocol/contracts/Quest.sol::50 => function start() public virtual onlyOwner { quest-protocol/contracts/Quest.sol::57 => function pause() public onlyOwner onlyStarted { quest-protocol/contracts/Quest.sol::63 => function unPause() public onlyOwner onlyStarted { quest-protocol/contracts/Quest.sol::96 => function claim() public virtual onlyQuestActive { quest-protocol/contracts/Quest.sol::150 => function withdrawRemainingTokens(address to_) public virtual onlyOwner onlyAdminWithdrawAfterEnd {} quest-protocol/contracts/QuestFactory.sol::142 => function changeCreateQuestRole(address account_, bool canCreateQuest_) public onlyOwner { quest-protocol/contracts/QuestFactory.sol::159 => function setClaimSignerAddress(address claimSignerAddress_) public onlyOwner { quest-protocol/contracts/QuestFactory.sol::165 => function setProtocolFeeRecipient(address protocolFeeRecipient_) public onlyOwner { quest-protocol/contracts/QuestFactory.sol::172 => function setRabbitHoleReceiptContract(address rabbitholeReceiptContract_) public onlyOwner { quest-protocol/contracts/QuestFactory.sol::179 => function setRewardAllowlistAddress(address rewardAddress_, bool allowed_) public onlyOwner { quest-protocol/contracts/QuestFactory.sol::186 => function setQuestFee(uint256 questFee_) public onlyOwner { quest-protocol/contracts/RabbitHoleReceipt.sol::65 => function setReceiptRenderer(address receiptRenderer_) public onlyOwner { quest-protocol/contracts/RabbitHoleReceipt.sol::71 => function setRoyaltyRecipient(address royaltyRecipient_) public onlyOwner { quest-protocol/contracts/RabbitHoleReceipt.sol::77 => function setQuestFactory(address questFactory_) public onlyOwner { quest-protocol/contracts/RabbitHoleReceipt.sol::83 => function setMinterAddress(address minterAddress_) public onlyOwner { quest-protocol/contracts/RabbitHoleReceipt.sol::90 => function setRoyaltyFee(uint256 royaltyFee_) public onlyOwner { quest-protocol/contracts/RabbitHoleReceipt.sol::98 => function mint(address to_, string memory questId_) public onlyMinter { quest-protocol/contracts/RabbitHoleTickets.sol::54 => function setTicketRenderer(address ticketRenderer_) public onlyOwner { quest-protocol/contracts/RabbitHoleTickets.sol::60 => function setRoyaltyRecipient(address royaltyRecipient_) public onlyOwner { quest-protocol/contracts/RabbitHoleTickets.sol::66 => function setRoyaltyFee(uint256 royaltyFee_) public onlyOwner { quest-protocol/contracts/RabbitHoleTickets.sol::73 => function setMinterAddress(address minterAddress_) public onlyOwner { quest-protocol/contracts/RabbitHoleTickets.sol::83 => function mint(address to_, uint256 id_, uint256 amount_, bytes memory data_) public onlyMinter {
calldata
instead of memory
for function parametersUse calldata instead of memory for function parameters. Having function arguments use calldata instead of memory can save gas.
quest-protocol/contracts/QuestFactory.sol::193 => function getNumberMinted(string memory questId_) external view returns (uint) { quest-protocol/contracts/QuestFactory.sol::199 => function questInfo(string memory questId_) external view returns (address, uint, uint) { quest-protocol/contracts/QuestFactory.sol::210 => function recoverSigner(bytes32 hash_, bytes memory signature_) public pure returns (address) { quest-protocol/contracts/QuestFactory.sol::219 => function mintReceipt(string memory questId_, bytes32 hash_, bytes memory signature_) public { quest-protocol/contracts/RabbitHoleReceipt.sol::98 => function mint(address to_, string memory questId_) public onlyMinter { quest-protocol/contracts/RabbitHoleReceipt.sol::110 => string memory questId_, quest-protocol/contracts/RabbitHoleReceipt.sol::164 => string memory questId = questIdForTokenId[tokenId_]; quest-protocol/contracts/ReceiptRenderer.sol::82 => function generateAttribute(string memory key, string memory value) public pure returns (string memory) { quest-protocol/contracts/ReceiptRenderer.sol::100 => function generateSVG(uint tokenId_, string memory questId_) public pure returns (string memory) { quest-protocol/contracts/interfaces/IQuestFactory.sol::19 => function questInfo(string memory questId_) external view returns (address, uint, uint);
address
mappings can be combined into a single mapping
of an address
to a struct
, where appropriateSaves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot
quest-protocol/contracts/QuestFactory.sol::20 => mapping(address => bool) addressMinted; quest-protocol/contracts/QuestFactory.sol::30 => mapping(address => bool) public rewardAllowlist;
bools
for storage incurs overheadquest-protocol/contracts/Quest.sol::19 => bool public hasStarted; quest-protocol/contracts/Quest.sol::20 => bool public isPaused; quest-protocol/contracts/Quest.sol::24 => mapping(uint256 => bool) private claimedList; quest-protocol/contracts/QuestFactory.sol::20 => mapping(address => bool) addressMinted; quest-protocol/contracts/QuestFactory.sol::30 => mapping(address => bool) public rewardAllowlist;
private
rather than public
for constants, saves gasIf needed, the value can be read from the verified contract source code.
quest-protocol/contracts/QuestFactory.sol::17 => bytes32 public constant CREATE_QUEST_ROLE = keccak256('CREATE_QUEST_ROLE');
The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting. If the contract is meant to be extended, the contract should be abstract and the function signatures be added without any default implementation. If the block is an empty if-statement block to avoid doing subsequent checks in the else-if/else conditions, the else-if/else conditions should be nested under the negation of the if-statement, because they involve different classes of checks, which may lead to the introduction of errors when the code is later modified (if(x){}else if(y){...}else{...} => if(!x){if(y){...}else{...}})
quest-protocol/contracts/Erc1155Quest.sol::30 => ){} quest-protocol/contracts/Quest.sol::150 => function withdrawRemainingTokens(address to_) public virtual onlyOwner onlyAdminWithdrawAfterEnd {} quest-protocol/contracts/QuestFactory.sol::35 => constructor() initializer {}
#0 - kirk-baird
2023-02-05T02:44:04Z
The following issues are included in the public report and are not valid
This report could be improved by including estimates to how much gas is saved.
#1 - c4-judge
2023-02-05T02:44:07Z
kirk-baird marked the issue as grade-b