Platform: Code4rena
Start Date: 11/08/2022
Pot Size: $40,000 USDC
Total HM: 8
Participants: 108
Period: 4 days
Judge: hickuphh3
Total Solo HM: 2
Id: 152
League: ETH
Rank: 87/108
Findings: 1
Award: $22.31
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Dravee
Also found by: 0x040, 0x1f8b, 0xDjango, 0xHarry, 0xNazgul, 0xSmartContract, 0xbepresent, 0xkatana, Amithuddar, Aymen0909, Bnke0x0, Chom, CodingNameKiki, Deivitto, DevABDee, Diraco, ElKu, Fitraldys, Funen, IllIllI, JC, LeoS, Metatron, MiloTruck, Noah3o6, ReyAdmirado, Rohan16, Rolezn, Saw-mon_and_Natalie, Sm4rty, SpaceCake, TomJ, Tomio, Trabajo_de_mates, Waze, Yiko, __141345__, ajtra, apostle0x01, bobirichman, brgltd, bulej93, c3phas, cRat1st0s, carlitox477, d3e4, durianSausage, erictee, fatherOfBlocks, gerdusx, gogo, hakerbaya, ignacio, jag, joestakey, ladboy233, medikko, mics, newfork01, oyc_109, pfapostol, robee, rvierdiiev, sach1r0, saian, samruna, sikorico, simon135, wagmi, zeesaw, zkhorse, zuhaibmohd
22.3055 USDC - $22.31
++I (--I) cost less gas than I++ (I--)
NFTCollectionFactory.sol#L207 NFTCollectionFactory.sol#L231
Expressions for constant values such as a call to KECCAK256 should use IMMUTABLE rather than constant
MinterRole.sol#L19 NFTDropMarketFixedPriceSale.sol#L70
Storage array length checks incur an extra Gwarmaccess (100 gas) per loop. Store the array length in a variable and use it in the for loop helps to save gas
MarketFees.sol#L126 MarketFees.sol#L198 MarketFees.sol#L484 MarketFees.sol#L503
Change all <= / >= operators for < / > and remember to increse / decrese in consecuence to maintain the logic (example, a <= b for a < b + 1)
NFTCollection.sol#L268 NFTDropCollection.sol#L179 NFTDropCollection.sol#L181 NFTDropCollection.sol#L275 MarketFees.sol#L526 SequentialMintCollection.sol#L89 NFTDropMarketFixedPriceSale.sol#L240
Replace all > 0 for != 0
NFTDropCollection.sol#L88 NFTDropCollection.sol#L130 NFTDropCollection.sol#L131 MarketFees.sol#L244
MarketFees.sol#L134 MarketFees.sol#L150 MarketFees.sol#L199 MarketFees.sol#L490 MarketFees.sol#L505 MarketFees.sol#L527
Use bytes32 instead of string when it's possible to save some gas. In this project, it's probably that the symbol is not going to be bigger than 32 bytes so it's possible to change it from string to bytes32 to save some gas.
NFTCollection.sol#L108 NFTCollectionFactory.sol#L138 NFTCollectionFactory.sol#L163 NFTCollectionFactory.sol#L259 NFTCollectionFactory.sol#L288 NFTCollectionFactory.sol#L326 NFTCollectionFactory.sol#L365 NFTCollectionFactory.sol#L388 NFTDropCollection.sol#L123
Each extra memory word of bytes past the original 32 incurs an MSTORE which costs 3 gas
NFTCollection.sol#L158 NFTCollection.sol#L263 NFTCollection.sol#L264 NFTCollection.sol#L268 NFTCollection.sol#L327 NFTCollectionFactory.sol#L173 NFTCollectionFactory.sol#L182 NFTCollectionFactory.sol#L203 NFTCollectionFactory.sol#L227 NFTCollectionFactory.sol#L262 NFTDropCollection.sol#L88 NFTDropCollection.sol#L93 NFTDropCollection.sol#L130 NFTDropCollection.sol#L131 NFTDropCollection.sol#L172 NFTDropCollection.sol#L179 NFTDropCollection.sol#L238 AddressLibrary.sol#L31 ContractFactory.sol#L22 ContractFactory.sol#L31 MinterRole.sol#L22 SequentialMintCollection.sol#L58 SequentialMintCollection.sol#L63 SequentialMintCollection.sol#L74 SequentialMintCollection.sol#L87 SequentialMintCollection.sol#L88 SequentialMintCollection.sol#L89 AdminRole.sol#L19
Custom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they’re hitby avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas.
NFTCollection.sol#L158 NFTCollection.sol#L263 NFTCollection.sol#L264 NFTCollection.sol#L268 NFTCollection.sol#L327 NFTCollectionFactory.sol#L173 NFTCollectionFactory.sol#L182 NFTCollectionFactory.sol#L203 NFTCollectionFactory.sol#L227 NFTCollectionFactory.sol#L262 NFTDropCollection.sol#L88 NFTDropCollection.sol#L93 NFTDropCollection.sol#L130 NFTDropCollection.sol#L131 NFTDropCollection.sol#L172 NFTDropCollection.sol#L179 NFTDropCollection.sol#L238 AddressLibrary.sol#L31 ContractFactory.sol#L22 ContractFactory.sol#L31 MinterRole.sol#L22 SequentialMintCollection.sol#L58 SequentialMintCollection.sol#L63 SequentialMintCollection.sol#L74 SequentialMintCollection.sol#L87 SequentialMintCollection.sol#L88 SequentialMintCollection.sol#L89 AdminRole.sol#L19
If needed, the value can be read from the verified contract source code. Savings are due to the compiler not having to create non-payable getter functions for deployment calldata, and not adding another entry to the method ID table
MinterRole.sol#L19 NFTDropMarketFixedPriceSale.sol#L70
If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address,...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
MarketFees.sol#L126 MarketFees.sol#L198 MarketFees.sol#L484 BytesLibrary.sol#L25 BytesLibrary.sol#L44
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas), and to avoid Gsset (20000 gas) when changing from 'false' to 'true', after having been 'true' in the past.
NFTCollection.sol#L53 MarketFees.sol#L61 NFTDropMarketFixedPriceSale.sol#L232
The default “checked” behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected. if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an unchecked block will save gas.
For all for-loops in the code it is possible to change as the following example.
for (uint256 i;i < X;){ -- code -- unchecked { ++i; } }
In the case of _baseURI it's possible to refactor the code to save gas. 180 of gas is saved with the following change. (-) Remove the line (+) Add the line
function _baseURI() internal view override returns (string memory) { - if (bytes(baseURI_).length != 0) { - return baseURI_; - } - return "ipfs://"; + return (bytes(baseURI_).length != 0)?baseURI_:"ipfs://"; }
In the case of startsWith it's possible to refactor the code to save gas as the following way. 222 gas will be saved with this change. (-) Remove the line (+) Add the line
function startsWith(bytes memory callData, bytes4 functionSig) internal pure returns (bool) { // A signature is 4 bytes long - if (callData.length < 4) { - return false; - } - unchecked { - for (uint256 i = 0; i < 4; ++i) { - if (callData[i] != functionSig[i]) { - return false; - } - } - } - return true; + return !(callData.length < 4 || callData[0] != functionSig[0]|| callData[1] != functionSig[1]|| callData[2] != functionSig[2]|| callData[3] != functionSig[3]); }
#0 - HardlyDifficult
2022-08-17T18:47:08Z
Good report, thanks.
- Post-increment/decrement cost more gas then pre-increment/decrement
Agree and will fix.
- Call to KECCAK256 should use IMMUTABLE rather than constant
While it seems this should help, changing to immutable shows a regression in our test suite. Gas costs go up by ~50 gas.
- Array length should not be looked up in every loop of a for-loop
May be theoretically valid, but won't fix. I tested this: gas-reporter and our gas-stories suite is reporting a small regression using this technique. It also hurts readability a bit so we wouldn't want to include it unless it was a clear win.
- Operatos <= or >= cost more gas than operators < or >
This could potentially be valid but won't fix to improve code readability and avoid potential overflow/underflow concerns.
- != 0 is cheaper than > 0
Invalid. We tested the recommendation and got the following results:
createNFTDropCollection gas reporter results: using > 0 (current): - 319246 · 319578 · 319361 using != 0 (recommendation): - 319252 · 319584 · 319367 impact: +6 gas
- Variable1 = Variable1 + (-) Variable2 is cheaper in gas cost than variable1 += (-=) variable2.
No impact. We made the recommended change and ran gas-reporter for our entire test suite. 0 impact detected. Even if there were a small benefit, we feel this hurts readability and would not be worth trivial savings.
- Use bytes32 instead of string
It's possible this technique could work for name or symbol, however in order to stay flexible in case a creator would like to use a long value here -- we will keep these as strings.
- Require / Revert strings longer than 32 bytes cost extra gas
Agree but won't fix. We use up to 64 bytes, aiming to respect the incremental cost but 32 bytes is a bit too short to provide descriptive error messages for our users.
- Use custom errors rather than REVERT()/REQUIRE() strings to save deployment gas
Agree but won't fix at this time. We use these in the market but not in collections. Unfortunately custom errors are still not as good of an experience for users (e.g. on etherscan). We used them in the market originally because we were nearing the max contract size limit and this was a good way to reduce the bytecode. We'll consider this in the future as tooling continues to improve.
- Using private rather than public for constants, saves gas
Agree but won't fix. For ease of use and consistency we will continue to expose some constants publicly.
- Initialize variables with default values are not needed
Invalid. This optimization technique is no longer applicable with the current version of Solidity.
- Using bools for storage incurs overhead
Valid for cidToMinted
, saving ~200 gas. Not seeing any benefit for assumePrimarySale
, potentially because it's an immutable variable.
- Uncheked arithmetic when it is not possible for them to overflow
getFeesAndRecipients
is a read only function not intended to be used on-chain, but as a best practice we will add unchecked there as well.
- Refactoring the code of NFTCollection._baseURI to save gas
This information isn't usually read by another contract, but out of curiosity I tested .tokenURI
for the standard scenario (no override provided)..
Before:
36642
After:
36644
Impact: -2 gas
Invalid. The contract bytecode size also increased by 0.002 KB.
- Refactoring the code of BytesLibrary.startsWith to save gas
Given the results from 14 I'm a bit skeptical, however more important is this one hurts readability - I wouldn't want to make a change like that unless it was a clear win.