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: 48/108
Findings: 2
Award: $62.56
๐ 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.2563 USDC - $41.26
โ
it is best practice to lock your pragma instead of using floating pragma. the use of floating pragma has a risk of accidentally get deployed using latest complier which may have higher risk of undiscovered bugs. Reference: https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/locking-pragmas/
./FoundationTreasuryNode.sol:3:pragma solidity ^0.8.12; ./NFTDropMarket.sol:42:pragma solidity ^0.8.12; ./NFTDropMarketFixedPriceSale.sol:3:pragma solidity ^0.8.12; ./SequentialMintCollection.sol:3:pragma solidity ^0.8.12; ./NFTDropCollection.sol:3:pragma solidity ^0.8.12; ./MarketSharedCore.sol:3:pragma solidity ^0.8.12; ./NFTCollectionFactory.sol:42:pragma solidity ^0.8.12; ./AdminRole.sol:3:pragma solidity ^0.8.12; ./MarketFees.sol:3:pragma solidity ^0.8.12; ./ContractFactory.sol:3:pragma solidity ^0.8.12; ./Gap10000.sol:3:pragma solidity ^0.8.12; ./Constants.sol:3:pragma solidity ^0.8.12; ./CollectionRoyalties.sol:3:pragma solidity ^0.8.12; ./FETHNode.sol:3:pragma solidity ^0.8.12; ./MinterRole.sol:3:pragma solidity ^0.8.12; ./NFTDropMarketCore.sol:3:pragma solidity ^0.8.12; ./NFTCollection.sol:3:pragma solidity ^0.8.12;
I suggest to lock your pragma and aviod using floating pragma.
// bad pragma solidity ^0.8.10; // good pragma solidity 0.8.10;
โ
Each event should have 3 indexed fields if there are 3 or more fields.
./NFTDropMarketFixedPriceSale.sol:79: event CreateFixedPriceSale(address indexed nftContract, address indexed seller, uint256 price, uint256 limitPerAccount); ./NFTDropCollection.sol:85: event URIUpdated(string baseURI, bytes32 postRevealBaseURIHash); ./MarketFees.sol:71: event BuyReferralPaid(address indexed nftContract, uint256 indexed tokenId, address buyReferrer, uint256 buyReferrerFee, uint256 buyReferrerSellerFee); ./NFTCollection.sol:70: event BaseURIUpdated(string baseURI);
Add up to 3 indexed fields when possible.
โ
Several function adds return statement even thought named returns variable are used. Remove unnecessary named returns variable to improve code readability. Also keeping the use of named returns or return statement consistent through out the whole project if possible is recommended.
Total of 4 issues found.
tokenURI() of NFTDropCollection.sol Remove returns variable "uri" https://github.com/code-423n4/2022-08-foundation/blob/main/contracts/NFTDropCollection.sol#L300-L304
createNFTDropCollection() of NFTCollectionFactory.sol Remove returns variable "collection" https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/NFTCollectionFactory.sol#L294-L295
createNFTDropCollectionWithPaymentAddress() of NFTCollectionFactory.sol Remove returns variable "collection" https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/NFTCollectionFactory.sol#L333-L334
createNFTDropCollectionWithPaymentFactory() of NFTCollectionFactory.sol Remove returns variable "collection" https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/NFTCollectionFactory.sol#L372-L373
Remove unused named returns variable and keep the use of named returns or return statement consistent through out the whole project if possible.
โ
There is mistake in following NatSpec.
60: * @dev This creates and initializes an ERC-1165 minimal proxy pointing to a NFT collection contract implementation.
โ
#0 - HardlyDifficult
2022-08-18T18:19:29Z
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.
BuyReferralPaid event should index buyReferrer
Agree but won't fix at this time. We have already started emitting this event on mainnet, to simplify integrations such as subgraph we are going to continue with this event as is. However it's a good catch and I agree that buyReferrer ideally would have been indexed here.
For the other examples I believe this is invalid. index should be reserved for params that are likely to be requested as a filter. In these examples those params are data not really filter candidates. And for the string specifically, indexed would prevent the full information from being communicated, requiring a second unindexed version which is a bit redundant and increases gas costs.
Use named returns consistently
Agree, we have opted to use the named returns instead of return ..
. This is more consistent with other code in our repo and saves a bit of on the contract size. We also like named returns as a way of improving natspec, and typechain (particularly when a tuple is returned).
ERC-1165 should be ERC-1167
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
21.2979 USDC - $21.30
โ
Using function parameter value instead of SLOAD state variable saves gas.
Total of 2 issues found.
Use parameter _baseURI of updatePreRevealContent() of NFTDropCollection.sol Change line 242 from state variable of baseURI to parameter of _baseURI. https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/NFTDropCollection.sol#L242
Use parameter _postRevealBaseURIHash of updatePreRevealContent() of NFTDropCollection.sol Change line 242 from state variable of postRevealBaseURIHash to parameter of _postRevealBaseURIHash. https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/NFTDropCollection.sol#L242
Change state variable to parameter variable as listed in above PoC.
โ
Since below require checks are used more than once, I recommend making these to a modifier or a function.
Total of 1 issue found.
./NFTCollectionFactory.sol:203: require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); ./NFTCollectionFactory.sol:227: require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract");
I recommend making duplicate require statement into modifier or a function.
โ
Certain function is defined even though it is called only once. Inline it instead to where it is called to avoid usage of extra gas.
Total of 3 issues found.
_initializeAdminRole() of AdminRole.sol Function called only once at line 148 of NFTDropCollection.sol https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/roles/AdminRole.sol#L13 https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/NFTDropCollection.sol#L148
_tryUseFETHBalance() of FETHNode.sol Function called only once at line 204 of NFTDropMarketFixedPriceSale.sol https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/shared/FETHNode.sol#L46 https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/nftDropMarket/NFTDropMarketFixedPriceSale.sol#L204
_initializeMinterRole() of MinterRole.sol Function called only once at line 150 of NFTDropCollection.sol https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/mixins/roles/MinterRole.sol#L26 https://github.com/code-423n4/2022-08-foundation/blob/792e00df429b0df9ee5d909a0a5a6e72bd07cf79/contracts/NFTDropCollection.sol#L150
I recommend to not define above functions and instead inline it at place it is called.
โ
When using constant it is expected that the value should be converted into a constant value at compile time. However when using a call to keccak256(), the expression is re-calculated each time the constant is referenced. Resulting in costing about 100 gas more on each access to this constant. Link for more details: https://github.com/ethereum/solidity/issues/9232
Total of 2 issues found.
./NFTDropMarketFixedPriceSale.sol:70: bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); ./MinterRole.sol:19: bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
Change the variable from constant to immutable.
โ
Since EVM operates on 32 bytes at a time, if the element is smaller than that, the EVM must use more operations in order to reduce the elements from 32 bytes to specified size.
Reference: https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html
Total of 13 issues found.
./NFTDropMarketFixedPriceSale.sol:120: uint80 price, ./NFTDropMarketFixedPriceSale.sol:121: uint16 limitPerAccount ./NFTDropMarketFixedPriceSale.sol:172: uint16 count, ./SequentialMintCollection.sol:62: function _initializeSequentialMintCollection(address payable _creator, uint32 _maxTokenId) internal onlyInitializing { ./SequentialMintCollection.sol:86: function _updateMaxTokenId(uint32 _maxTokenId) internal { ./NFTDropCollection.sol:126: uint32 _maxTokenId, ./NFTDropCollection.sol:220: function updateMaxTokenId(uint32 _maxTokenId) external onlyAdmin { ./NFTCollectionFactory.sol:192: function initialize(uint32 _versionNFTCollection) external initializer { ./NFTCollectionFactory.sol:291: uint32 maxTokenId, ./NFTCollectionFactory.sol:329: uint32 maxTokenId, ./NFTCollectionFactory.sol:368: uint32 maxTokenId, ./NFTCollectionFactory.sol:391: uint32 maxTokenId, ./NFTCollection.sol:251: function updateMaxTokenId(uint32 _maxTokenId) external onlyCreator {
I suggest using uint256 instead of anything smaller or downcast where needed.
โ
When variable is not initialized, it will have its default values. For example, 0 for uint, false for bool and address(0) for address. Reference: https://docs.soliditylang.org/en/v0.8.15/control-structures.html#scoping-and-declarations
Total of 5 issues found.
./MarketFees.sol:126: for (uint256 i = 0; i < creatorRecipients.length; ++i) { ./MarketFees.sol:198: for (uint256 i = 0; i < creatorShares.length; ++i) { ./MarketFees.sol:484: for (uint256 i = 0; i < creatorRecipients.length; ++i) { ./BytesLibrary.sol:25: for (uint256 i = 0; i < 20; ++i) { ./BytesLibrary.sol:44: for (uint256 i = 0; i < 4; ++i) {
I suggest removing default value initialization. For example,
โ
By storing an array's length as a variable before the for-loop, can save 3 gas per iteration.
Total of 4 issues found.
./MarketFees.sol:126: for (uint256 i = 0; i < creatorRecipients.length; ++i) { ./MarketFees.sol:198: for (uint256 i = 0; i < creatorShares.length; ++i) { ./MarketFees.sol:484: for (uint256 i = 0; i < creatorRecipients.length; ++i) { ./MarketFees.sol:503: for (uint256 i = 1; i < creatorRecipients.length; ) {
Store array's length as a variable before looping it.
โ
!= 0 costs less gas when optimizer is enabled and is used for unsigned integers in require statement.
Total of 3 issues found.
./NFTDropCollection.sol:88: require(bytes(_baseURI).length > 0, "NFTDropCollection: `_baseURI` must be set"); ./NFTDropCollection.sol:130: require(bytes(_symbol).length > 0, "NFTDropCollection: `_symbol` must be set"); ./NFTDropCollection.sol:131: require(_maxTokenId > 0, "NFTDropCollection: `_maxTokenId` must be set");
I suggest changing > 0 to != 0
โ
Since each storage slot is size of 32 bytes, error messages that is longer than this will need extra storage slot leading to more gas cost.
Total of 28 issues found.
./SequentialMintCollection.sol:58: require(msg.sender == owner, "SequentialMintCollection: Caller is not creator"); ./SequentialMintCollection.sol:63: require(_creator != address(0), "SequentialMintCollection: Creator cannot be the zero address"); ./SequentialMintCollection.sol:74: require(totalSupply() == 0, "SequentialMintCollection: Any NFTs minted must be burned first"); ./SequentialMintCollection.sol:87: require(_maxTokenId != 0, "SequentialMintCollection: Max token ID may not be cleared"); ./SequentialMintCollection.sol:88: require(maxTokenId == 0 || _maxTokenId < maxTokenId, "SequentialMintCollection: Max token ID may not increase"); ./SequentialMintCollection.sol:89: require(latestTokenId <= _maxTokenId, "SequentialMintCollection: Max token ID must be >= last mint"); ./NFTDropCollection.sol:88: require(bytes(_baseURI).length > 0, "NFTDropCollection: `_baseURI` must be set"); ./NFTDropCollection.sol:93: require(postRevealBaseURIHash != bytes32(0), "NFTDropCollection: Already revealed"); ./NFTDropCollection.sol:130: require(bytes(_symbol).length > 0, "NFTDropCollection: `_symbol` must be set"); ./NFTDropCollection.sol:131: require(_maxTokenId > 0, "NFTDropCollection: `_maxTokenId` must be set"); ./NFTDropCollection.sol:172: require(count != 0, "NFTDropCollection: `count` must be greater than 0"); ./NFTDropCollection.sol:179: require(latestTokenId <= maxTokenId, "NFTDropCollection: Exceeds max tokenId"); ./NFTDropCollection.sol:238: require(_postRevealBaseURIHash != bytes32(0), "NFTDropCollection: use `reveal` instead"); ./NFTCollectionFactory.sol:173: require(rolesContract.isAdmin(msg.sender), "NFTCollectionFactory: Caller does not have the Admin role"); ./NFTCollectionFactory.sol:182: require(_rolesContract.isContract(), "NFTCollectionFactory: RolesContract is not a contract"); ./NFTCollectionFactory.sol:203: require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); ./NFTCollectionFactory.sol:227: require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); ./NFTCollectionFactory.sol:262: require(bytes(symbol).length != 0, "NFTCollectionFactory: Symbol is required"); ./AdminRole.sol:19: require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "AdminRole: caller does not have the Admin role"); ./ContractFactory.sol:22: require(msg.sender == contractFactory, "ContractFactory: Caller is not the factory"); ./ContractFactory.sol:31: require(_contractFactory.isContract(), "ContractFactory: Factory is not a contract"); ./AddressLibrary.sol:31: require(contractAddress.isContract(), "InternalProxyCall: did not return a contract"); ./MinterRole.sol:22: require(isMinter(msg.sender) || isAdmin(msg.sender), "MinterRole: Must have the minter or admin role"); ./NFTCollection.sol:158: require(tokenCreatorPaymentAddress != address(0), "NFTCollection: tokenCreatorPaymentAddress is required"); ./NFTCollection.sol:263: require(bytes(tokenCID).length != 0, "NFTCollection: tokenCID is required"); ./NFTCollection.sol:264: require(!cidToMinted[tokenCID], "NFTCollection: NFT was already minted"); ./NFTCollection.sol:268: require(maxTokenId == 0 || tokenId <= maxTokenId, "NFTCollection: Max token count has already been minted"); ./NFTCollection.sol:327: require(_exists(tokenId), "NFTCollection: URI query for nonexistent token");
Simply keep the error messages within 32 bytes to avoid extra storage slot cost.
โ
Custom errors from Solidity 0.8.4 are cheaper than revert strings. Details are explained here: https://blog.soliditylang.org/2021/04/21/custom-errors/
Total of 28 issues found.
./SequentialMintCollection.sol:58: require(msg.sender == owner, "SequentialMintCollection: Caller is not creator"); ./SequentialMintCollection.sol:63: require(_creator != address(0), "SequentialMintCollection: Creator cannot be the zero address"); ./SequentialMintCollection.sol:74: require(totalSupply() == 0, "SequentialMintCollection: Any NFTs minted must be burned first"); ./SequentialMintCollection.sol:87: require(_maxTokenId != 0, "SequentialMintCollection: Max token ID may not be cleared"); ./SequentialMintCollection.sol:88: require(maxTokenId == 0 || _maxTokenId < maxTokenId, "SequentialMintCollection: Max token ID may not increase"); ./SequentialMintCollection.sol:89: require(latestTokenId <= _maxTokenId, "SequentialMintCollection: Max token ID must be >= last mint"); ./NFTDropCollection.sol:88: require(bytes(_baseURI).length > 0, "NFTDropCollection: `_baseURI` must be set"); ./NFTDropCollection.sol:93: require(postRevealBaseURIHash != bytes32(0), "NFTDropCollection: Already revealed"); ./NFTDropCollection.sol:130: require(bytes(_symbol).length > 0, "NFTDropCollection: `_symbol` must be set"); ./NFTDropCollection.sol:131: require(_maxTokenId > 0, "NFTDropCollection: `_maxTokenId` must be set"); ./NFTDropCollection.sol:172: require(count != 0, "NFTDropCollection: `count` must be greater than 0"); ./NFTDropCollection.sol:179: require(latestTokenId <= maxTokenId, "NFTDropCollection: Exceeds max tokenId"); ./NFTDropCollection.sol:238: require(_postRevealBaseURIHash != bytes32(0), "NFTDropCollection: use `reveal` instead"); ./NFTCollectionFactory.sol:173: require(rolesContract.isAdmin(msg.sender), "NFTCollectionFactory: Caller does not have the Admin role"); ./NFTCollectionFactory.sol:182: require(_rolesContract.isContract(), "NFTCollectionFactory: RolesContract is not a contract"); ./NFTCollectionFactory.sol:203: require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); ./NFTCollectionFactory.sol:227: require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); ./NFTCollectionFactory.sol:262: require(bytes(symbol).length != 0, "NFTCollectionFactory: Symbol is required"); ./AdminRole.sol:19: require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "AdminRole: caller does not have the Admin role"); ./ContractFactory.sol:22: require(msg.sender == contractFactory, "ContractFactory: Caller is not the factory"); ./ContractFactory.sol:31: require(_contractFactory.isContract(), "ContractFactory: Factory is not a contract"); ./AddressLibrary.sol:31: require(contractAddress.isContract(), "InternalProxyCall: did not return a contract"); ./MinterRole.sol:22: require(isMinter(msg.sender) || isAdmin(msg.sender), "MinterRole: Must have the minter or admin role"); ./NFTCollection.sol:158: require(tokenCreatorPaymentAddress != address(0), "NFTCollection: tokenCreatorPaymentAddress is required"); ./NFTCollection.sol:263: require(bytes(tokenCID).length != 0, "NFTCollection: tokenCID is required"); ./NFTCollection.sol:264: require(!cidToMinted[tokenCID], "NFTCollection: NFT was already minted"); ./NFTCollection.sol:268: require(maxTokenId == 0 || tokenId <= maxTokenId, "NFTCollection: Max token count has already been minted"); ./NFTCollection.sol:327: require(_exists(tokenId), "NFTCollection: URI query for nonexistent token");
I suggest implementing custom errors to save gas.
โ
#0 - batu-inal
2022-08-19T08:45:51Z
Use Function Parameter Instead of State Variable
Agree, will make some improvements here.
Duplicate require() Checks Should be a Modifier or a Function
Agreed. This is not a gas optimization; however, increases readability and maintainability.
Internal Function Called Only Once can be Inlined
Potentially valid but won't fix. While there may be trivial gas savings here we like the readability and maintainability of this pattern.
Constant Value of a Call to keccak256() should Use Immutable
Valid but won't fix. While there may be gas savings here we like the readability and maintainability of this pattern. Adding hard-coded hashes makes the code fragile and harder to read.
Using Elements Smaller than 32 bytes (256 bits) Might Use More Gas
Potentially valid but won't fix. This is uint32 to allow packing storage for a significant gas savings. Accepting the input as uint256 would incur additional overhead in checking for overflow before saving these values. Other inputs are logically limited to the size exposed, huge values cannot be used. Accepting the input as uint256 would incur additional overhead in checking for overflow before using these values.
Unnecessary Default Value Initialization
Invalid. This optimization technique is no longer applicable with the current version of Solidity.
Store Array's Length as a Variable
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.
!= 0 costs less gass 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
Keep The Revert Strings of Error Messages within 32 Bytes
Invalid. The baseURI cannot be stored in bytes32 because it may be longer than 32 bytes. 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.
Use Custom Errors to Save 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.