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: 74/173
Findings: 2
Award: $28.53
π Selected for report: 0
π Solo Findings: 0
π 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
Number | Issue details | Instances |
---|---|---|
L-1 | Use ofblock.timestamp | 5 |
L-2 | Lock pragmas to specific compiler version. | 10 |
Total: 2 issues.
Number | Issue details | Instances |
---|---|---|
NC-1 | Constants should be defined rather than using magic numbers | 6 |
NC-2 | Use scientific notation (e.g.1e18 ) rather than large multiples of 10 (e.g. 1000000) | 5 |
NC-3 | Lines are too long. | 3 |
Total: 3 issues.
block.timestamp
Block timestamps have historically been used for a variety of applications, such as entropy for random numbers (see the Entropy Illusion for further details), locking funds for periods of time, and various state-changing conditional statements that are time-dependent. Miners have the ability to adjust timestamps slightly, which can prove to be dangerous if block timestamps are used incorrectly in smart contracts.
Use oracle instead of block.timestamp
File: 2023-01-rabbithole/quest-protocol/contracts/Quest.sol
35: if (endTime_ <= block.timestamp) revert EndTimeInPast(); 36: if (startTime_ <= block.timestamp) revert StartTimeInPast(); 77: if (block.timestamp < endTime) revert NoWithdrawDuringClaim(); 90: if (block.timestamp < startTime) revert ClaimWindowNotStarted();
File: 2023-01-rabbithole/quest-protocol/contracts/RabbitHoleReceipt.sol
102: timestampForTokenId[newTokenID] = block.timestamp;
Pragma statements can be allowed to float when a contract is intended for consumption by other developers, as in the case with contracts in a library or EthPM package. Otherwise, the developer would need to manually update the pragma in order to compile locally.
Ethereum Smart Contract Best Practices - Lock pragmas to specific compiler version. solidity-specific/locking-pragmas
File: 2023-01-rabbithole/quest-protocol/contracts/Erc1155Quest.sol
2: pragma solidity ^0.8.15;
File: 2023-01-rabbithole/quest-protocol/contracts/Erc20Quest.sol
2: pragma solidity ^0.8.15;
File: 2023-01-rabbithole/quest-protocol/contracts/Quest.sol
2: pragma solidity ^0.8.15;
File: 2023-01-rabbithole/quest-protocol/contracts/QuestFactory.sol
2: pragma solidity ^0.8.15;
File: 2023-01-rabbithole/quest-protocol/contracts/RabbitHoleReceipt.sol
2: pragma solidity ^0.8.15;
File: 2023-01-rabbithole/quest-protocol/contracts/RabbitHoleTickets.sol
2: pragma solidity ^0.8.15;
File: 2023-01-rabbithole/quest-protocol/contracts/ReceiptRenderer.sol
2: pragma solidity ^0.8.15;
File: 2023-01-rabbithole/quest-protocol/contracts/TicketRenderer.sol
2: pragma solidity ^0.8.15;
File: 2023-01-rabbithole/quest-protocol/contracts/interfaces/IQuest.sol
2: pragma solidity ^0.8.15;
File: 2023-01-rabbithole/quest-protocol/contracts/interfaces/IQuestFactory.sol
2: pragma solidity ^0.8.15;
Even assembly
can benefit from using readable constants instead of hex/numeric literals
You should declare the variable instead of magic number.
File: 2023-01-rabbithole/quest-protocol/contracts/Erc20Quest.sol
53: return (maxTotalRewards() * questFee) / 10_000; 97: return (receiptRedeemers() * rewardAmountInWeiOrTokenId * questFee) / 10_000;
File: 2023-01-rabbithole/quest-protocol/contracts/QuestFactory.sol
187: if (questFee_ > 10_000) revert QuestFeeTooHigh();
File: 2023-01-rabbithole/quest-protocol/contracts/RabbitHoleReceipt.sol
129: if (tokenIdsForQuest[i] > 0) { 184: uint256 royaltyPayment = (salePrice_ * royaltyFee) / 10_000;
File: 2023-01-rabbithole/quest-protocol/contracts/RabbitHoleTickets.sol
113: uint256 royaltyPayment = (salePrice_ * royaltyFee) / 10_000;
1e18
) rather than large multiples of 10 (e.g. 1000000)Use scientific notation (e.g.1e18
) rather than large multiples of 10, that could lead to errors.
Use scientific notation instead of large multiples of 10.
File: 2023-01-rabbithole/quest-protocol/contracts/Erc20Quest.sol
53: return (maxTotalRewards() * questFee) / 10_000; 97: return (receiptRedeemers() * rewardAmountInWeiOrTokenId * questFee) / 10_000;
File: 2023-01-rabbithole/quest-protocol/contracts/QuestFactory.sol
187: if (questFee_ > 10_000) revert QuestFeeTooHigh();
File: 2023-01-rabbithole/quest-protocol/contracts/RabbitHoleReceipt.sol
184: uint256 royaltyPayment = (salePrice_ * royaltyFee) / 10_000;
File: 2023-01-rabbithole/quest-protocol/contracts/RabbitHoleTickets.sol
113: uint256 royaltyPayment = (salePrice_ * royaltyFee) / 10_000;
Usually lines in source code are limited to 80 characters. Today's screens are much larger so it's reasonable to stretch this in some cases. Since the files will most likely reside in GitHub, and GitHub starts using a scroll bar in all cases when the length is over 164 characters, the lines below should be split when they reach that length
Reduce number of characters per line to improve readability.
File: 2023-01-rabbithole/quest-protocol/contracts/Erc20Quest.sol
56: /// @notice Starts the quest by marking it ready to start at the contract level. Marking a quest ready to start does not mean that it is live. It also requires that the start time has passed 57: /// @dev Requires that the balance of the rewards in the contract is greater than or equal to the maximum amount of rewards that can be claimed by all users and the protocol
File: 2023-01-rabbithole/quest-protocol/contracts/interfaces/IQuestFactory.sol
16: event QuestCreated(address indexed creator, address indexed contractAddress, string indexed questId, string contractType, address rewardTokenAddress, uint256 endTime, uint256 startTime, uint256 totalParticipants, uint256 rewardAmountOrTokenId);
#0 - GarrettJMU
2023-02-08T14:55:26Z
Good call on no magic numbers.
#1 - c4-sponsor
2023-02-08T14:55:30Z
GarrettJMU marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-16T07:35:29Z
kirk-baird marked the issue as grade-b
π 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
Number | Issue details | Instances |
---|---|---|
G-1 | ++i/i++ should be unchecked{++i} /unchecked{i++} when it is not possible for them to overflow, for example when used in for and while loops | 4 |
G-2 | Using storage instead of memory for structs/arrays saves gas. | 2 |
G-3 | Internal functions only called once can be inlined to save gas. | 4 |
G-4 | >= costs less gas than > . | 3 |
G-5 | <x> += <y> costs more gas than <x> = <x> + <y> for state variables | 1 |
Total: 5 issues. Note: The findings for issue with id 1 are different from the c4udit output.
++i/i++
should be unchecked{++i}
/unchecked{i++}
when it is not possible for them to overflow, for example when used in for
and while
loopsIn 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. Example for loop:
for (uint i = 0; i < length; i++) { // do something that doesn't change the value of i }
In this example, the for loop post condition, i.e., i++
involves checked arithmetic, which is not required. This is because the value of i is always strictly less than length <= 2**256 - 1
. Therefore, the theoretical maximum value of i to enter the for-loop body is 2**256 - 2
. This means that the i++
in the for loop can never overflow. Regardless, the overflow checks are performed by the compiler.
Unfortunately, the Solidity optimizer is not smart enough to detect this and remove the checks. You should manually do this by:
for (uint i = 0; i < length; i = unchecked_inc(i)) { // do something that doesn't change the value of i } function unchecked_inc(uint i) returns (uint) { unchecked { return i + 1; } }
Or just:
for (uint i = 0; i < length;) { // do something that doesn't change the value of i unchecked { i++; } }
Note that itβs important that the call to unchecked_inc
is inlined. This is only possible for solidity versions starting from 0.8.2
.
Gas savings: roughly speaking this can save 30-40 gas per loop iteration. For lengthy loops, this can be significant! (This is only relevant if you are using the default solidity checked arithmetic.)
Use the unchecked
keyword
File: 2023-01-rabbithole/quest-protocol/contracts/Quest.sol
70: for (uint i = 0; i < tokenIds_.length; i++) { 104: for (uint i = 0; i < tokens.length; i++) {
File: 2023-01-rabbithole/quest-protocol/contracts/RabbitHoleReceipt.sol
117: for (uint i = 0; i < msgSenderBalance; i++) { 128: for (uint i = 0; i < msgSenderBalance; i++) {
When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declaring the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct(src)
Use storage for struct/array
File: 2023-01-rabbithole/quest-protocol/contracts/RabbitHoleReceipt.sol
114: uint[] memory tokenIdsForQuest = new uint[](msgSenderBalance); 125: uint[] memory filteredTokens = new uint[](foundTokens);
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 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
solidity if(x){}else if(y){...}else{...} => if(!x){if(y){...}else{...}}
Empty blocks should be removed or emit something (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.
File: 2023-01-rabbithole/quest-protocol/contracts/Quest.sol
150: function withdrawRemainingTokens(address to_) public virtual onlyOwner onlyAdminWithdrawAfterEnd {}
Not inlining costs 20 to 40 gas because of two extra JUMP
instructions and additional stack operations needed for function calls.
Check this out: link
Note: To sum up, when there is an internal function that is only called once, there is no point for that function to exist.
Remove the function and make it inline if it is a simple function.
File: 2023-01-rabbithole/quest-protocol/contracts/Erc1155Quest.sol
41: function _transferRewards(uint256 amount_) internal override {
File: 2023-01-rabbithole/quest-protocol/contracts/Erc20Quest.sol
66: function _transferRewards(uint256 amount_) internal override {
File: 2023-01-rabbithole/quest-protocol/contracts/Quest.sol
129: function _transferRewards(uint256 amount_) internal virtual {
File: 2023-01-rabbithole/quest-protocol/contracts/QuestFactory.sol
152: function grantDefaultAdminAndCreateQuestRole(address account_) internal {
>=
costs less gas than >
.The compiler uses opcodes GT
and ISZERO
for solidity code that uses >
, but only requires LT
for >=
, which saves 3 gas
Use >=
instead if appropriate.
File: 2023-01-rabbithole/quest-protocol/contracts/QuestFactory.sol
187: if (questFee_ > 10_000) revert QuestFeeTooHigh(); 220: if (quests[questId_].numberMinted + 1 > quests[questId_].totalParticipants) revert OverMaxAllowedToMint();
File: 2023-01-rabbithole/quest-protocol/contracts/RabbitHoleReceipt.sol
129: if (tokenIdsForQuest[i] > 0) {
<x> += <y>
costs more gas than <x> = <x> + <y>
for state variablesUsing the addition operator instead of plus-equals saves 113 gas
Use <x> = <x> + <y>
instead.
File: 2023-01-rabbithole/quest-protocol/contracts/Quest.sol
115: redeemedTokens += redeemableTokenCount;
#0 - c4-judge
2023-02-16T07:36:00Z
kirk-baird marked the issue as grade-b