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: 47/173
Findings: 3
Award: $50.14
š Selected for report: 0
š Solo Findings: 0
š Selected for report: adriro
Also found by: 0xRobocop, 0xbepresent, Breeje, CodingNameKiki, HollaDieWaldfee, Kenshin, M4TZ1P, Ruhum, Tricko, badman, bin2chen, carrotsmuggler, cccz, csanuragjain, glcanvas, joestakey, lukris02, m9800, mert_eren, peakbolt, peanuts, prestoncodes, rvierdiiev, sashik_eth
21.6061 USDC - $21.61
mintReceipt
currently does not have any check that the questId
passed as an argument corresponds to an active quest.
As the signature will be valid without a deadline, a user can call mintReceipt
after the end of a quest, when there is no more funds in the Quest
contract - ie when all users have already claimed their rewards and the owner has called ERC20Quest.withdrawRemainingTokens()
.
The user has hence minted a useless token.
As per the gas reports, the cost of mintReceipt
is around $40. This is a non-negligeable amount and the function should ensure the user does not mint an obsolete token
Medium
Manual Review
Add a check in mintReceipt
to ensure Quest(quests[questId_].questAddress).endTime() > block.timestamp
#0 - c4-judge
2023-02-06T23:37:13Z
kirk-baird marked the issue as duplicate of #22
#1 - c4-judge
2023-02-14T08:42:29Z
kirk-baird marked the issue as satisfactory
š Selected for report: CodingNameKiki
Also found by: 0x1f8b, 0x4non, 0x5rings, 0xAgro, 0xMirce, 0xRobocop, 0xSmartContract, 0xackermann, AkshaySrivastav, ArmedGoose, Aymen0909, BClabs, Breeje, Dewaxindo, Diana, HollaDieWaldfee, IceBear, IllIllI, Iurii3, Josiah, Krayt, PaludoX0, Phenomana, PrasadLak, RaymondFam, Rolezn, SaeedAlipoor01988, SaharDevep, SleepingBugs, adriro, arialblack14, bin2chen, brevis, btk, carlitox477, carrotsmuggler, catellatech, chaduke, chrisdior4, cryptonue, cryptostellar5, csanuragjain, ddimitrov22, fellows, glcanvas, halden, hl_, horsefacts, jat, joestakey, kenta, libratus, lukris02, luxartvinsec, manikantanynala97, martin, matrix_0wl, nadin, nicobevi, oberon, peanuts, prestoncodes, rbserver, sakshamguruji, sashik_eth, sayan, seeu, thekmj, tnevler, trustindistrust, tsvetanovv, xAriextz, zadaru13, zaskoh
17.196 USDC - $17.20
Issue | |
---|---|
L-01 | Additional sanity checks |
L-02 | Wrong comment |
Issue | |
---|---|
N-01 | Redundant computation |
N-02 | Scientific notation can be used |
N-03 | Use named imports |
N-04 | Offset can be misleading |
N-05 | Open TODOs |
N-06 | Events should be emitted in key setters |
N-07 | Avoid floating pragmas |
Add a sanity check in setRoyaltyFee
: royaltyFee_ < 10000
.
RabbitHoleReceipt.sol
93: function setRoyaltyFee(uint256 royaltyFee_) public onlyOwner { 94: royaltyFee = royaltyFee_; 95: emit RoyaltyFeeSet(royaltyFee_); 96: }
RabbitHoleReceipt.sol
177: /// @dev See {IERC165-royaltyInfo} //@audit - IERC2981 178: /// @param tokenId_ the token id 179: /// @param salePrice_ the sale price 180: function royaltyInfo(
isPaused
is already false when start
is called.
Quest.sol
50: function start() public virtual onlyOwner { 51: isPaused = false; //@audit can be removed 52: hasStarted = true; 53: }
QuestFactory.sol
186: function setQuestFee(uint256 questFee_) public onlyOwner { 187: if (questFee_ > 10_000) revert QuestFeeTooHigh(); //@audit - change to 1e4 188: questFee = questFee_; 189: }
QuestFactory.sol
10: import '@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol'; //@audit import {ECDSAUpgradeable}
questIdCount
should not be initialized to 1
in initialize
, as it means querying this variable will always return the actual number of quests with an offset of 1.
QuestFactory.sol
49: questIdCount = 1; //@audit consider removing this line
IQuest.sol
4: // TODO clean this whole thing up
Emit an event in key setters
QuestFactory.sol
159: function setClaimSignerAddress(address claimSignerAddress_) public onlyOwner { 160: claimSignerAddress = claimSignerAddress_; 161: }
All the contracts in scope
2: pragma solidity ^0.8.15;
#0 - c4-judge
2023-02-06T23:26:07Z
kirk-baird marked the issue as grade-b
#1 - c4-sponsor
2023-02-07T22:00:35Z
waynehoover marked the issue as sponsor confirmed
š 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
timestampForTokenId
can be removedsetQuestFactory
and setMinter
can be combined in one functionA few optimizations allow to save significant amounts of gas upon
QuestFactory.createQuest()
andQuestFactory.mintReceipt()
calls, ie the most important user-facing functions of the protocol. The total gas saved on users function calls is41,852
The total gas saved on admin function calls is21,000
The total gas saved on deployment is81,841
Getters are automatically defined for public storage variables and constants.
The functions Quest.getRewardAmount()
and Quest.getRewardToken()
are hence redundant and can be replaced to save gas upon deploying new Quests - ie when calling QuestFactory.createQuest()
139: /// @dev Returns the reward amount 140: function getRewardAmount() public view returns (uint256) { 141: return rewardAmountInWeiOrTokenId; 142: } 143: 144: /// @dev Returns the reward token address 145: function getRewardToken() public view returns (address) { 146: return rewardToken; 147: }
Manual Analysis, Hardhat
-139: /// @dev Returns the reward amount -140: function getRewardAmount() public view returns (uint256) -{ -141: return rewardAmountInWeiOrTokenId; -142: } -143: -144: /// @dev Returns the reward token address -145: function getRewardToken() public view returns (address) { -146: return rewardToken; -147: }
And in RabbitHoleReceipt.sol
-166: IQuest questContract = IQuest(questAddress); +166: Quest questContract = Quest(questAddress); 167: 168: bool claimed = questContract.isClaimed(tokenId_); -169: uint rewardAmount = questContract.getRewardAmount(); +169: uint rewardAmount = questContract.rewardAmountInWeiOrTokenId(); -170: address rewardAddress = questContract.getRewardToken(); +170: address rewardAddress = questContract.rewardToken();
Note that you must also remove the two getters from IQuest()
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
QuestFactory | Deployment | 5313516 | ||
--------------------- | ------------------- | -------- | -------- | -------- |
QuestFactory | createQuest | 1365913 | 1508398 | 1437156 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
QuestFactory | Deployment | 5266581 | ||
--------------------- | ------------------- | -------- | -------- | -------- |
QuestFactory | createQuest | 1346235 | 1488720 | 1417478 |
This saves on average:
46,935
gas upon deployment for QuestFactory.sol
19,678
gas on every QuestFactory.createQuest
calltimestampForTokenId
can be removedtimestampForTokenId
is used when minting a receipt in mint()
to save the timestamp at which the token was minted.
This information can also be retrieved by checking the timestamp of the block in which the event Transfer(address(0), tokenId, ..)
(emitted upon minting) was included in.
Given that timestampForTokenId
is not used anywhere else, consider removing it to save gas upon deployment and on calls to mintReceipt()
.
35: mapping(uint => uint) public timestampForTokenId;
Manual Analysis, Hardhat
-35: mapping(uint => uint) public timestampForTokenId; ... 104: questIdForTokenId[newTokenID] = questId_; -105: timestampForTokenId[newTokenID] = block.timestamp; 106: _safeMint(to_, newTokenID);
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RabbitHoleReceipt | Deployment | 2775990 | ||
--------------------- | ------------------- | -------- | -------- | -------- |
QuestFactory | mintReceipt | 276604 | 288104 | 282354 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RabbitHoleReceipt | Deployment | 2759180 | ||
--------------------- | ------------------- | -------- | -------- | -------- |
QuestFactory | mintReceipt | 254430 | 265930 | 260180 |
This saves on average:
16,810
gas upon deployment for RabbitHoleReceipt
22,174
gas on every QuestFactory.mintReceipt
callsetQuestFactory
and setMinter
can be combined in one functionAs per QuestFactory.mintReceipt
comments:
215: /// @dev mint a RabbitHole Receipt. Note: this contract must be set as Minter on the receipt contract
This means that when the owner calls RabbitHoleReceipt.setQuestFactory
, they must follow it with a call to RabbitHoleReceipt.setMinter
.
Given that the base number of gas needed for any transaction is 21,000 gas, combining these two setters into one can save 21,000
gas every time the owner need to change the QuestFactory
address.
76: /// @dev set the quest factory contract 77: /// @param questFactory_ the address of the quest factory contract 78: function setQuestFactory(address questFactory_) public onlyOwner { 79: QuestFactoryContract = IQuestFactory(questFactory_); 80: } 81: 82: /// @dev set the minter address 83: /// @param minterAddress_ the address of the minter 84: function setMinterAddress(address minterAddress_) public onlyOwner { 85: minterAddress = minterAddress_; 86: emit MinterAddressSet(minterAddress_); 87: }
Manual Analysis,
76: /// @dev set the quest factory contract 77: /// @param questFactory_ the address of the quest factory contract 78: function setQuestFactory(address questFactory_) public onlyOwner { 79: QuestFactoryContract = IQuestFactory(questFactory_); + minterAddress = questFactory_; + emit MinterAddressSet(questFactory_); 80: } 81: -82: /// @dev set the minter address -83: /// @param minterAddress_ the address of the minter -84: function setMinterAddress(address minterAddress_) public onlyOwner -{ -85: minterAddress = minterAddress_; -86: emit MinterAddressSet(minterAddress_); -87: }
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RabbitHoleReceipt | Deployment | 2775990 |
Contract | Method | Min | Max | Avg |
---|---|---|---|---|
RabbitHoleReceipt | Deployment | 2757896 |
This saves on average:
18,096
gas upon deployment for RabbitHoleReceipt
21,000
gas on every setQuestFactory
call and setMinter
call (as they are combined in one)#0 - c4-judge
2023-02-14T09:54:35Z
kirk-baird marked the issue as grade-b
#1 - joestakey
2023-02-24T11:23:21Z
Hi @kirk-baird , sorry for messaging a little past the post-QA deadline but I just saw this today. Just wondering why is this report is graded b when it has higher gas savings on function calls (41,852 gas in total) than the grade a report selected as primary (#288 , 2436 gas in total)?
#2 - kirk-baird
2023-02-24T23:45:55Z
Yep that's a good question @joestakey. In general we only count hot paths towards to "total gas saved" path. So contracts like QuestFactory
and RabbitHoleReceipt
which are only deployed once we don't count the gas savings towards the total. Note it's still worth including these in your report and they add value. Note Quest
will be deployed often since a new one is deployed for each quest and so I would consider that a hot path.
Similarly, onlyOwner
functions e.g. setQuestFactory()
and setMinter()
that are generally called once or very irregularly will have less value than hot paths such as claim()
or mint()
.
Finally the number of unqiue classes of issues and rarity of each issue contributes to the quality of report. #288 has 14 different classes of issues two of which are not raise by other wardens.
To summarise, we can't do a simple quantitative comparison for who has the most gas saved but is subjective based on criteria such as