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: 102/108
Findings: 1
Award: $20.60
🌟 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
20.6049 USDC - $20.60
Gas savings are estimated using yarn test-gas-stories
and may differ depending on the implementation of the fix. I keep my version of the fix for each finding and can provide them if you need.
Gas Optimizations
Issue | Instances | Estimated gas(deployments) | Estimated gas(gas-stories) | |
---|---|---|---|---|
1 | Use Custom Errors instead of Revert/Require Strings to save Gas | 28 | 631685 | - |
Require()/revert() strings longer than 32 bytes cost extra gas | 28 | 631685 | - | |
2 | Storage variables that use more than once can be cached | 8 | 11257 | - |
3 | x = x + 1 is more efficient than increment | 11 | 2604 | - |
4 | Usage of variable less than 256 bit (Optional) | 6 | 266482 | -230800 |
Total: 53 instances over 4 issues
Use Custom Errors instead of Revert/Require Strings to save Gas (28 instances)
Solidity 0.8.4 introduced custom errors. They are more gas efficient than revert strings, when it comes to deploy cost as well as runtime cost when the revert condition is met. Use custom errors instead of revert strings for gas savings.
Custom errors save ~50 gas each time they're hitby avoiding having to allocate and store the revert string. Additional gas is saved due to the lack of defining string. https://blog.soliditylang.org/2021/04/21/custom-errors/#errors-in-depth
Require()/revert() strings longer than 32 bytes cost extra gas (28 instances)
The code and gas usage estimation are merged due to the same PoC code and simplicity for gas usage estimation.
Deployment Gas Saved: 631685
173 require(rolesContract.isAdmin(msg.sender), "NFTCollectionFactory: Caller does not have the Admin role"); ... 182 require(_rolesContract.isContract(), "NFTCollectionFactory: RolesContract is not a contract"); ... 203 require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); ... 227 require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); ... 262 require(bytes(symbol).length != 0, "NFTCollectionFactory: Symbol is required");
88 require(bytes(_baseURI).length > 0, "NFTDropCollection: `_baseURI` must be set"); ... 93 require(postRevealBaseURIHash != bytes32(0), "NFTDropCollection: Already revealed"); ... 130 require(bytes(_symbol).length > 0, "NFTDropCollection: `_symbol` must be set"); 131 require(_maxTokenId > 0, "NFTDropCollection: `_maxTokenId` must be set"); ... 172 require(count != 0, "NFTDropCollection: `count` must be greater than 0"); ... 179 require(latestTokenId <= maxTokenId, "NFTDropCollection: Exceeds max tokenId"); ... 238 require(_postRevealBaseURIHash != bytes32(0), "NFTDropCollection: use `reveal` instead");
158 require(tokenCreatorPaymentAddress != address(0), "NFTCollection: tokenCreatorPaymentAddress is required"); ... 263 require(bytes(tokenCID).length != 0, "NFTCollection: tokenCID is required"); 264 require(!cidToMinted[tokenCID], "NFTCollection: NFT was already minted"); ... 268 require(maxTokenId == 0 || tokenId <= maxTokenId, "NFTCollection: Max token count has already been minted"); ... 327 require(_exists(tokenId), "NFTCollection: URI query for nonexistent token");
31 require(contractAddress.isContract(), "InternalProxyCall: did not return a contract");
19 require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "AdminRole: caller does not have the Admin role");
22 require(msg.sender == contractFactory, "ContractFactory: Caller is not the factory"); ... 31 require(_contractFactory.isContract(), "ContractFactory: Factory is not a contract");
22 require(isMinter(msg.sender) || isAdmin(msg.sender), "MinterRole: Must have the minter or admin role");
58 require(msg.sender == owner, "SequentialMintCollection: Caller is not creator"); ... 63 require(_creator != address(0), "SequentialMintCollection: Creator cannot be the zero address"); ... 74 require(totalSupply() == 0, "SequentialMintCollection: Any NFTs minted must be burned first"); ... 87 require(_maxTokenId != 0, "SequentialMintCollection: Max token ID may not be cleared"); 88 require(maxTokenId == 0 || _maxTokenId < maxTokenId, "SequentialMintCollection: Max token ID may not increase"); 89 require(latestTokenId <= _maxTokenId, "SequentialMintCollection: Max token ID must be >= last mint");
Storage variables that use more than once can be cached (8 instances)
Deployment Gas Saved: 11257
207 versionNFTCollection++; ... 213 string.concat("NFT Collection Implementation v", versionNFTCollection.toString()), 214 string.concat("NFTv", versionNFTCollection.toString()) ... 231 versionNFTDropCollection++; ... 239 string.concat("NFT Drop Collection Implementation v", versionNFTDropCollection.toString()), 240 string.concat("NFTDropV", versionNFTDropCollection.toString()),
fix:
uint32 _version; unchecked { // Version cannot overflow 256 bits. _version = ++versionNFTCollection; } ... string.concat("NFT Collection Implementation v", _version.toString()), string.concat("NFTv", _version.toString()) ); emit ImplementationNFTCollectionUpdated(_implementation, _version);
x = x + 1 is more efficient than increment (11 instances)
Deployment Gas Saved: 2604
207 versionNFTCollection++; ... 231 versionNFTDropCollection++;
184 ++i;
267 tokenId = ++latestTokenId;
126 for (uint256 i = 0; i < creatorRecipients.length; ++i) { ... 198 for (uint256 i = 0; i < creatorShares.length; ++i) { ... 484 for (uint256 i = 0; i < creatorRecipients.length; ++i) { ... 508 ++i;
25 for (uint256 i = 0; i < 20; ++i) { ... 44 for (uint256 i = 0; i < 4; ++i) {
98 ++burnCounter;
Usage of variable less than 256 bit (6 instances)
Deployment gas saved: 266482 User story gas losted: -230800
80 uint32 public versionNFTCollection; ... 95 uint32 public versionNFTDropCollection;
38 uint96 constant ROYALTY_IN_BASIS_POINTS = 1000;
27 uint32 public latestTokenId; ... 34 uint32 public maxTokenId; ... 40 uint32 private burnCounter;
#0 - HardlyDifficult
2022-08-17T19:55:11Z
Custom errors
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.
Use short error messages
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.
Storage variables that use more than once can be cached
Since this is just a small deployment savings, I'd prefer to keep the code simple for this admin-only functions.
++i costs less than i++
Agree and will fix.
x = x + 1 is more efficient than increment
This may be valid but it's not a large impact and I prefer the increment for readability.
Usage of variable less than 256 bit
Won't fix. These suggestions impact our packing strategy, which can have a significant impact on the user gas costs.