Platform: Code4rena
Start Date: 13/12/2022
Pot Size: $36,500 USDC
Total HM: 5
Participants: 77
Period: 3 days
Judge: gzeon
Total Solo HM: 1
Id: 191
League: ETH
Rank: 4/77
Findings: 2
Award: $813.60
š Selected for report: 0
š Solo Findings: 0
787.6515 USDC - $787.65
Issue | Instances | |
---|---|---|
[Lā01] | Use Ownable2Step rather than Ownable | 2 |
Total: 2 instances over 1 issues
Issue | Instances | |
---|---|---|
[Nā01] | Upgradeable contract is missing a __gap[50] storage variable to allow for new storage variables in later versions | 2 |
[Nā02] | override function arguments that are unused should have the variable name removed or commented out to avoid compiler warnings | 1 |
[Nā03] | constant s should be defined rather than using magic numbers | 1 |
[Nā04] | Numeric values having to do with time should use time units for readability | 4 |
[Nā05] | Lines are too long | 1 |
[Nā06] | Non-library/interface files should use fixed compiler versions, not floating ones | 1 |
[Nā07] | NatSpec is incomplete | 4 |
[Nā08] | Contracts should have full test coverage | 1 |
[Nā09] | Large or complicated code bases should implement fuzzing tests | 1 |
Total: 16 instances over 9 issues
Ownable2Step
rather than Ownable
Ownable2Step
and Ownable2StepUpgradeable
prevent the contract ownership from mistakenly being transferred to an address that cannot handle it (e.g. due to a typo in the address), by requiring that the recipient of the owner permissions actively accept via a contract call of its own.
There are 2 instances of this issue:
File: src/VRFNFTRandomDrawFactory.sol 14 contract VRFNFTRandomDrawFactory is 15 IVRFNFTRandomDrawFactory, 16 OwnableUpgradeable, 17 UUPSUpgradeable, 18: Version(1)
File: src/VRFNFTRandomDraw.sol 15 contract VRFNFTRandomDraw is 16 IVRFNFTRandomDraw, 17 VRFConsumerBaseV2, 18 OwnableUpgradeable, 19: Version(1)
__gap[50]
storage variable to allow for new storage variables in later versionsSee this link for a description of this storage variable. While some contracts may not currently be sub-classed, adding the variable now protects against forgetting to add it in the future.
There are 2 instances of this issue:
File: src/VRFNFTRandomDrawFactory.sol 14 contract VRFNFTRandomDrawFactory is 15 IVRFNFTRandomDrawFactory, 16 OwnableUpgradeable, 17 UUPSUpgradeable, 18: Version(1)
File: src/VRFNFTRandomDraw.sol 15 contract VRFNFTRandomDraw is 16 IVRFNFTRandomDraw, 17 VRFConsumerBaseV2, 18 OwnableUpgradeable, 19: Version(1)
override
function arguments that are unused should have the variable name removed or commented out to avoid compiler warningsThere is 1 instance of this issue:
File: src/VRFNFTRandomDrawFactory.sol 55: function _authorizeUpgrade(address newImplementation)
constant
s should be defined rather than using magic numbersEven assembly can benefit from using readable constants instead of hex/numeric literals
There is 1 instance of this issue:
File: src/VRFNFTRandomDraw.sol /// @audit 12 95: block.timestamp + (MONTH_IN_SECONDS * 12)
There are units for seconds, minutes, hours, days, and weeks, and since they're defined, they should be used
There are 4 instances of this issue:
File: src/VRFNFTRandomDraw.sol /// @audit 60 /// @audit 60 29: uint256 immutable HOUR_IN_SECONDS = 60 * 60; /// @audit 3600 31: uint256 immutable WEEK_IN_SECONDS = (3600 * 24 * 7); /// @audit 3600 33: uint256 immutable MONTH_IN_SECONDS = (3600 * 24 * 7) * 30;
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
There is 1 instance of this issue:
File: src/interfaces/IVRFNFTRandomDraw.sol 64: /// @notice has chosen a random number (in case random number = 0(in case random number = 0)(in case random number = 0)(in case random number = 0)(in case random number = 0)(in case random number = 0)(in case random number = 0)(in case random number = 0)(in case random number = 0))
There is 1 instance of this issue:
File: src/utils/Version.sol 2: pragma solidity ^0.8.10;
There are 4 instances of this issue:
File: src/interfaces/IVRFNFTRandomDrawFactory.sol /// @audit Missing: '@return' 20 /// @notice Function to make a new drawing 21 /// @param settings settings for the new drawing 22 function makeNewDraw(IVRFNFTRandomDraw.Settings memory settings) 23 external 24: returns (address);
File: src/interfaces/IVRFNFTRandomDraw.sol /// @audit Missing: '@return' 106 /// @notice Function to determine if the user has won in the current drawing 107 /// @param user address for the user to check if they have won in the current drawing 108: function hasUserWon(address user) external view returns (bool);
File: src/VRFNFTRandomDrawFactory.sol /// @audit Missing: '@return' 36 /// @notice Function to make a new drawing 37 /// @param settings settings for the new drawing 38 function makeNewDraw(IVRFNFTRandomDraw.Settings memory settings) 39 external 40: returns (address)
File: src/VRFNFTRandomDraw.sol /// @audit Missing: '@return' 262 /// @notice Function to determine if the user has won in the current drawing 263 /// @param user address for the user to check if they have won in the current drawing 264: function hasUserWon(address user) public view returns (bool) {
While 100% code coverage does not guarantee that there are no bugs, it often will catch easy-to-find bugs, and will ensure that there are fewer regressions when the code invariably has to be modified. Furthermore, in order to get full coverage, code authors will often have to re-organize their code so that it is more modular, so that each component can be tested separately, which reduces interdependencies between modules and layers, and makes for code that is easier to reason about and audit.
There is 1 instance of this issue:
File: src/interfaces/IVRFNFTRandomDrawFactory.sol
Large code bases, or code with lots of inline-assembly, complicated math, or complicated interactions between multiple contracts, should implement fuzzing tests. Fuzzers such as Echidna require the test writer to come up with invariants which should not be violated under any circumstances, and the fuzzer tests various inputs and function calls to ensure that the invariants always hold. Even code with 100% code coverage can still have bugs due to the order of the operations a user performs, and fuzzers, with properly and extensively-written invariants, can close this testing gap significantly.
There is 1 instance of this issue:
File: src/interfaces/IVRFNFTRandomDrawFactory.sol
These findings are excluded from awards calculations because there are publicly-available automated tools that find them. The valid ones appear here for completeness
Issue | Instances | |
---|---|---|
[Lā01] | Missing checks for address(0x0) when assigning values to address state variables | 3 |
Total: 3 instances over 1 issues
Issue | Instances | |
---|---|---|
[Nā01] | public functions not called by the contract should be declared external instead | 1 |
[Nā02] | Event is missing indexed fields | 4 |
Total: 5 instances over 2 issues
address(0x0)
when assigning values to address
state variablesThere are 3 instances of this issue:
File: src/ownable/OwnableUpgradeable.sol /// @audit (valid but excluded finding) 62: _owner = _initialOwner; /// @audit (valid but excluded finding) 93: _owner = _newOwner; /// @audit (valid but excluded finding) 107: _pendingOwner = _newOwner;
public
functions not called by the contract should be declared external
insteadContracts are allowed to override their parents' functions and change the visibility from external
to public
.
There is 1 instance of this issue:
File: src/VRFNFTRandomDraw.sol /// @audit (valid but excluded finding) 75 function initialize(address admin, Settings memory _settings) 76 public 77: initializer
indexed
fieldsIndex event fields make the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Each event
should use three indexed
fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed.
There are 4 instances of this issue:
File: src/interfaces/IVRFNFTRandomDrawFactory.sol /// @audit (valid but excluded finding) 11: event SetupNewDrawing(address user, address drawing);
File: src/interfaces/IVRFNFTRandomDraw.sol /// @audit (valid but excluded finding) 43: event InitializedDraw(address indexed sender, Settings settings); /// @audit (valid but excluded finding) 45: event SetupDraw(address indexed sender, Settings settings); /// @audit (valid but excluded finding) 49: event DiceRollComplete(address indexed sender, CurrentRequest request);
#0 - c4-judge
2022-12-17T17:05:01Z
gzeon-c4 marked the issue as grade-a
š Selected for report: c3phas
Also found by: 0x1f8b, Aymen0909, Bnke0x0, Bobface, IllIllI, PaludoX0, Rahoz, RaymondFam, ReyAdmirado, Rolezn, Sathish9098, adriro, chaduke, codeislight, ctrlc03, indijanc, izhelyazkov, kuldeep, nadin, neko_nyaa, nicobevi, rvierdiiev, shark
25.9485 USDC - $25.95
Issue | Instances | Total Gas Saved | |
---|---|---|---|
[Gā01] | Structs can be packed into fewer storage slots | 1 | - |
[Gā02] | State variables should be cached in stack variables rather than re-reading them from storage | 3 | 291 |
[Gā03] | Optimize names to save gas | 7 | 154 |
[Gā04] | Functions guaranteed to revert when called by normal users can be marked payable | 10 | 210 |
Total: 21 instances over 4 issues with 655 gas saved
Gas totals use lower bounds of ranges and count two iterations of each for
-loop. All values above are runtime, not deployment, values; deployment values are listed in the individual issue descriptions. The table above as well as its gas numbers do not include any of the excluded findings.
Each slot saved can avoid an extra Gsset (20000 gas) for the first setting of the struct. Subsequent reads as well as writes have smaller gas savings
There is 1 instance of this issue:
File: src/interfaces/IVRFNFTRandomDraw.sol /// @audit Variable ordering with 8 slots instead of the current 9: /// uint256(32):tokenId, uint256(32):drawingTokenStartId, uint256(32):drawingTokenEndId, uint256(32):drawBufferTime, uint256(32):recoverTimelock, bytes32(32):keyHash, address(20):token, uint64(8):subscriptionId, address(20):drawingToken 71 struct Settings { 72 /// @notice Token Contract to put up for raffle 73 address token; 74 /// @notice Token ID to put up for raffle 75 uint256 tokenId; 76 /// @notice Token that each (sequential) ID has a entry in the raffle. 77 address drawingToken; 78 /// @notice Start token ID for the drawing (if totalSupply = 20 but the first token is 5 (5-25), setting this to 5 would fix the ordering) 79 uint256 drawingTokenStartId; 80 /// @notice End token ID for the drawing (exclusive) (token ids 0 - 9 would be 10 in this field) 81 uint256 drawingTokenEndId; 82 /// @notice Draw buffer time Ć¢ā¬ā time until a re-drawing can occur if the selected user cannot or does not claim the NFT. 83 uint256 drawBufferTime; 84 /// @notice block.timestamp that the admin can recover the NFT (as a safety fallback) 85 uint256 recoverTimelock; 86 /// @notice Chainlink gas keyhash 87 bytes32 keyHash; 88 /// @notice Chainlink subscription id 89 uint64 subscriptionId; 90: }
The instances below point to the second+ access of a state variable within a function. Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read. Other less obvious fixes/optimizations include having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.
There are 3 instances of this issue:
File: src/ownable/OwnableUpgradeable.sol /// @audit _pendingOwner on line 95 96: delete _pendingOwner; /// @audit _pendingOwner on line 122 124: delete _pendingOwner; /// @audit _pendingOwner on line 129 131: delete _pendingOwner;
public
/external
function names and public
member variable names can be optimized to save gas. See this link for an example of how it works. Below are the interfaces/abstract contracts that can be optimized so that the most frequently-called functions use the least amount of gas possible during method lookup. Method IDs that have two leading zero bytes can save 128 gas each during deployment, and renaming functions to have lower method IDs will save 22 gas per call, per sorted position shifted
There are 7 instances of this issue:
File: src/interfaces/IVRFNFTRandomDrawFactory.sol /// @audit initialize(), makeNewDraw() 6: interface IVRFNFTRandomDrawFactory {
File: src/interfaces/IVRFNFTRandomDraw.sol /// @audit initialize(), startDraw(), redraw(), hasUserWon(), winnerClaimNFT(), lastResortTimelockOwnerClaimNFT(), getRequestDetails() 4: interface IVRFNFTRandomDraw {
File: src/ownable/IOwnableUpgradeable.sol /// @audit pendingOwner(), safeTransferOwnership(), cancelOwnershipTransfer() 7: interface IOwnableUpgradeable {
File: src/ownable/OwnableUpgradeable.sol /// @audit pendingOwner(), safeTransferOwnership(), resignOwnership(), cancelOwnershipTransfer() 12: abstract contract OwnableUpgradeable is IOwnableUpgradeable, Initializable {
File: src/utils/Version.sol /// @audit contractVersion() 4: contract Version {
File: src/VRFNFTRandomDrawFactory.sol /// @audit initialize(), makeNewDraw() 14: contract VRFNFTRandomDrawFactory is
File: src/VRFNFTRandomDraw.sol /// @audit getRequestDetails(), initialize(), startDraw(), redraw(), hasUserWon(), winnerClaimNFT(), lastResortTimelockOwnerClaimNFT() 15: contract VRFNFTRandomDraw is
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. The extra opcodes avoided are
CALLVALUE
(2),DUP1
(3),ISZERO
(3),PUSH2
(3),JUMPI
(10),PUSH1
(3),DUP1
(3),REVERT
(0),JUMPDEST
(1),POP
(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost
There are 10 instances of this issue:
File: src/ownable/OwnableUpgradeable.sol 57 function __Ownable_init(address _initialOwner) 58 internal 59 notZeroAddress(_initialOwner) 60: onlyInitializing 79 function transferOwnership(address _newOwner) 80 public 81 notZeroAddress(_newOwner) 82: onlyOwner 102 function safeTransferOwnership(address _newOwner) 103 public 104 notZeroAddress(_newOwner) 105: onlyOwner 114: function resignOwnership() public onlyOwner { 119: function acceptOwnership() public onlyPendingOwner { 128: function cancelOwnershipTransfer() public onlyOwner {
File: src/VRFNFTRandomDrawFactory.sol 55 function _authorizeUpgrade(address newImplementation) 56 internal 57 override 58: onlyOwner
File: src/VRFNFTRandomDraw.sol 173: function startDraw() external onlyOwner returns (uint256) { 203: function redraw() external onlyOwner returns (uint256) { 304: function lastResortTimelockOwnerClaimNFT() external onlyOwner {
These findings are excluded from awards calculations because there are publicly-available automated tools that find them. The valid ones appear here for completeness
Issue | Instances | Total Gas Saved | |
---|---|---|---|
[Gā01] | Using calldata instead of memory for read-only arguments in external functions saves gas | 2 | 240 |
Total: 2 instances over 1 issues with 240 gas saved
Gas totals use lower bounds of ranges and count two iterations of each for
-loop. All values above are runtime, not deployment, values; deployment values are listed in the individual issue descriptions. The table above as well as its gas numbers do not include any of the excluded findings.
calldata
instead of memory
for read-only arguments in external
functions saves gasWhen a function with a memory
array is called externally, the abi.decode()
step has to use a for-loop to copy each index of the calldata
to the memory
index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length
). Using calldata
directly, obliviates the need for such a loop in the contract code and runtime execution. Note that even if an interface defines a function as having memory
arguments, it's still valid for implementation contracs to use calldata
arguments instead.
If the array is passed to an internal
function which passes the array to another internal function where the array is modified and therefore memory
is used in the external
call, it's still more gass-efficient to use calldata
when the external
function uses modifiers, since the modifiers may prevent the internal functions from being called. Structs have the same overhead as an array of length one
Note that I've also flagged instances where the function is public
but can be marked as external
since it's not called by the contract, and cases where a constructor is involved
There are 2 instances of this issue:
File: src/VRFNFTRandomDrawFactory.sol /// @audit settings - (valid but excluded finding) 38 function makeNewDraw(IVRFNFTRandomDraw.Settings memory settings) 39 external 40: returns (address)
File: src/VRFNFTRandomDraw.sol /// @audit _settings - (valid but excluded finding) 75 function initialize(address admin, Settings memory _settings) 76 public 77: initializer
#0 - c4-judge
2022-12-17T17:38:44Z
gzeon-c4 marked the issue as grade-b