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: 60/108
Findings: 2
Award: $61.80
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Saw-mon_and_Natalie
Also found by: 0x1f8b, 0x52, 0xDjango, 0xNazgul, 0xSmartContract, 0xSolus, 0xackermann, 0xmatt, 0xsolstars, Aymen0909, Bnke0x0, Chom, Deivitto, DevABDee, Dravee, ElKu, IllIllI, JC, Kumpa, Lambda, LeoS, MiloTruck, PwnedNoMore, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, TomJ, Treasure-Seeker, Vexjon, Waze, Yiko, __141345__, apostle0x01, auditor0517, berndartmueller, bin2chen, bobirichman, brgltd, bulej93, c3phas, carlitox477, cccz, cryptphi, csanuragjain, d3e4, danb, delfin454000, durianSausage, erictee, fatherOfBlocks, gogo, iamwhitelights, joestakey, jonatascm, ladboy233, mics, oyc_109, rbserver, ret2basic, robee, rokinot, rvierdiiev, shenwilly, sikorico, simon135, thank_you, wagmi, yash90, zeesaw, zkhorse
41.1993 USDC - $41.20
Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.
https://swcregistry.io/docs/SWC-103
All the files under scope use a floating pragma as shown below:
pragma solidity ^0.8.12;
Mitigation would be to fix it to a particular version. For example:
pragma solidity 0.8.12;
Open TODO should be fixed. One instance in MarketFees.sol.
// TODO add referral info
#0 - HardlyDifficult
2022-08-18T18:22:10Z
Use fixed pragma
Disagree. We intentionally use a floating pragma in order to make integrating with contracts easier. Other contract developers are looking to interact with our contracts and they may be on a different version than we use. The pragma selected for our contracts is the minimum required in order to correctly compile and function. This way integration is easier if they lag a few versions behind, or if they use the latest but we don't bump our packages frequently enough, and when we do upgrade versions unless there was a breaking solidity change -- it should just swap in by incrementing our npm package version.
Unresolved TODO comments
Agree, will fix.
🌟 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
 | Contract | Method | Original Avg Gas Cost | Optimized Avg Gas Cost | Avg Gas Saved |
---|---|---|---|---|---|
1 | NFTDropCollection | mintCountTo | 75217 | 75048 | 169 |
2 | NFTDropMarketFixedPriceSale | createFixedPriceSale | 73230 | 73187 | 43 |
3 | NFTDropMarketFixedPriceSale | mintFromFixedPriceSale | 288858 | 288031 | 827 |
In the mintCountTo function state variable latestTokenId
was read 3 times and few times inside a for loop. By using a cached copy, we could save at least 200 gas.
// The function can be optimized as follows function mintCountTo(uint16 count, address to) external onlyMinterOrAdmin returns (uint256 firstTokenId) { require(count != 0, "NFTDropCollection: `count` must be greater than 0"); uint32 cachedlatestTokenId = latestTokenId; //cache state variable here unchecked { // If +1 overflows then +count would also overflow, unless count==0 in which case the loop would exceed gas limits firstTokenId = cachedlatestTokenId + 1; //read cached variable here } cachedlatestTokenId = cachedlatestTokenId + count; //read cached variable here require(cachedlatestTokenId <= maxTokenId, "NFTDropCollection: Exceeds max tokenId"); //read cached variable here for (uint256 i = firstTokenId; i <= cachedlatestTokenId; ) { //read cached variable here _mint(to, i); unchecked { ++i; } } latestTokenId = cachedlatestTokenId; //write back to state variable at the end }
Comparison of Hardhat gas reports, before and after the above changes, showed an average gas saving of 169 gas. while the max gas usage was reduced from 306257 to 305073(a saving of 1184).
In the createFixedPriceSale function state variable saleConfig
is read to emit an event. If possible, local variables should be used in the emit.
//Original Code: emit CreateFixedPriceSale(nftContract, saleConfig.seller, saleConfig.price, saleConfig.limitPerAccount); //Can be optimized into: emit CreateFixedPriceSale(nftContract, payable(msg.sender), price, limitPerAccount);
Comparison of Hardhat gas reports, before and after the above changes, showed an average gas saving of 43 gas.
Another major area in which we could save deployment cost would be in converting the revert
strings into custom errors. If the function does revert, you can also save on dynamic gas cost. See this example implementation to understand the dynamics of custom errors. It shows that each require
string converted into a custom error saves you around 11000 gas.
Many other files in scope have already implemented custom errors. The rest are listed below:
File: contracts/NFTCollectionFactory.sol --------------------------------------------- Line 173: require(rolesContract.isAdmin(msg.sender), "NFTCollectionFactory: Caller does not have the Admin role"); Line 182: require(_rolesContract.isContract(), "NFTCollectionFactory: RolesContract is not a contract"); Line 203: require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); Line 227: require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); Line 262: require(bytes(symbol).length != 0, "NFTCollectionFactory: Symbol is required"); File: contracts/libraries/AddressLibrary.sol ------------------------------------------------- Line 031: require(contractAddress.isContract(), "InternalProxyCall: did not return a contract"); File: contracts/NFTDropCollection.sol ------------------------------------------- Line 088: require(bytes(_baseURI).length > 0, "NFTDropCollection: `_baseURI` must be set"); Line 093: require(postRevealBaseURIHash != bytes32(0), "NFTDropCollection: Already revealed"); Line 130: require(bytes(_symbol).length > 0, "NFTDropCollection: `_symbol` must be set"); Line 131: require(_maxTokenId > 0, "NFTDropCollection: `_maxTokenId` must be set"); Line 172: require(count != 0, "NFTDropCollection: `count` must be greater than 0"); Line 179: require(latestTokenId <= maxTokenId, "NFTDropCollection: Exceeds max tokenId"); Line 238: require(_postRevealBaseURIHash != bytes32(0), "NFTDropCollection: use `reveal` instead"); File: contracts/mixins/shared/ContractFactory.sol -------------------------------------------------------- Line 022: require(msg.sender == contractFactory, "ContractFactory: Caller is not the factory"); Line 031: require(_contractFactory.isContract(), "ContractFactory: Factory is not a contract"); File: contracts/mixins/roles/AdminRole.sol ----------------------------------------------- Line 019: require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "AdminRole: caller does not have the Admin role"); File: contracts/mixins/roles/MinterRole.sol ------------------------------------------------ Line 022: require(isMinter(msg.sender) || isAdmin(msg.sender), "MinterRole: Must have the minter or admin role"); File: contracts/mixins/collections/SequentialMintCollection.sol -------------------------------------------------------------------- Line 058: require(msg.sender == owner, "SequentialMintCollection: Caller is not creator"); Line 063: require(_creator != address(0), "SequentialMintCollection: Creator cannot be the zero address"); Line 074: require(totalSupply() == 0, "SequentialMintCollection: Any NFTs minted must be burned first"); Line 087-089: require(_maxTokenId != 0, "SequentialMintCollection: Max token ID may not be cleared"); require(maxTokenId == 0 || _maxTokenId < maxTokenId, "SequentialMintCollection: Max token ID may not increase"); require(latestTokenId <= _maxTokenId, "SequentialMintCollection: Max token ID must be >= last mint"); File: contracts/NFTCollection.sol -------------------------------------- Line 158: require(tokenCreatorPaymentAddress != address(0), "NFTCollection: tokenCreatorPaymentAddress is required"); Line 263: require(bytes(tokenCID).length != 0, "NFTCollection: tokenCID is required"); Line 264: require(!cidToMinted[tokenCID], "NFTCollection: NFT was already minted"); Line 268: require(maxTokenId == 0 || tokenId <= maxTokenId, "NFTCollection: Max token count has already been minted"); Line 327: require(_exists(tokenId), "NFTCollection: URI query for nonexistent token");
There are a total of 5 + 1 + 7 + 2 + 1 + 1 + 6 + 5 = 28 require
strings spread out in 8 files.
Converting all of them into custom errors can save us around 28 * 11000 = 308,000 gas when deploying.
The above changes reduced the deployment cost by 308,000. And Dynamic cost was reduced by â€1039‬.
#0 - batu-inal
2022-08-19T08:28:13Z
using a cached copy, we could save at least 200 gas.
Valid, will fix. Tests show as much as 1,400 gas saved when minting 10.
local variables should be used in the emit.
Valid, will fix. Tests show 43 gas saved.
use 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.