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: 52/108
Findings: 2
Award: $62.00
🌟 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
Almost all the contracts have floating Pragmas, which is considered, a bad practice.
It is always recommended that pragma
should be fixed (not floating) to the version that you are intending to deploy your contracts with.
For more information Check this out
So Instead of this:
pragma solidity ^0.8.12;
Use this:
pragma solidity 0.8.12;
in all the contracts.
contracts/NFTDropCollection.sol:20: import "./mixins/shared/Constants.sol"; contracts/NFTDropMarket.sol:47: import "./mixins/shared/Constants.sol";
Indexed
, where they can be.The indexed keyword helps you to filter the logs to find the wanted data. thus you can search for specific items instead of getting all the logs. So in some contracts, the events can be described as indexed
but they are not. See:
contracts/NFTDropCollection.sol:85: event URIUpdated(string baseURI, bytes32 postRevealBaseURIHash); contracts/NFTCollection.sol:70: event BaseURIUpdated(string baseURI); contracts/mixins/shared/MarketFees.sol:71: event BuyReferralPaid( // 1 more can be marked as `indexed` here address indexed nftContract, uint256 indexed tokenId, address buyReferrer, uint256 buyReferrerFee, uint256 buyReferrerSellerFee ); contracts/mixins/nftDropMarket/NFTDropMarketFixedPriceSale.sol75: event CreateFixedPriceSale( // 1 more can be marked as `indexed` here address indexed nftContract, address indexed seller, uint256 price, uint256 limitPerAccount );
#0 - HardlyDifficult
2022-08-18T18:20:38Z
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.
UNUSED IMPORTS
Agree, will fix.
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.
🌟 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.7985 USDC - $20.80
Custom Errors
instead of require
statements.Some contracts use many require
statements. It makes deployment and function calling very gas expensive. As error messages stores full-length strings. So Replace those require
statements with the custom errors.
Check this Post for more info.
contracts/NFTCollectionFactory.sol:173: require(rolesContract.isAdmin(msg.sender), "NFTCollectionFactory: Caller does not have the Admin role"); contracts/NFTCollectionFactory.sol:182: require(_rolesContract.isContract(), "NFTCollectionFactory: RolesContract is not a contract"); contracts/NFTCollectionFactory.sol:203: require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); contracts/NFTCollectionFactory.sol:227: require(_implementation.isContract(), "NFTCollectionFactory: Implementation is not a contract"); contracts/NFTCollectionFactory.sol:262: require(bytes(symbol).length != 0, "NFTCollectionFactory: Symbol is required"); contracts/NFTDropCollection.sol:088: require(bytes(_baseURI).length > 0, "NFTDropCollection: `_baseURI` must be set"); contracts/NFTDropCollection.sol:093: require(postRevealBaseURIHash != bytes32(0), "NFTDropCollection: Already revealed"); contracts/NFTDropCollection.sol:130: require(bytes(_symbol).length > 0, "NFTDropCollection: `_symbol` must be set"); contracts/NFTDropCollection.sol:131: require(_maxTokenId > 0, "NFTDropCollection: `_maxTokenId` must be set"); contracts/NFTDropCollection.sol:172: require(count != 0, "NFTDropCollection: `count` must be greater than 0"); contracts/NFTDropCollection.sol:179: require(latestTokenId <= maxTokenId, "NFTDropCollection: Exceeds max tokenId"); contracts/NFTDropCollection.sol:238: require(_postRevealBaseURIHash != bytes32(0), "NFTDropCollection: use `reveal` instead"); contracts/NFTCollection.sol:158: require(tokenCreatorPaymentAddress != address(0), "NFTCollection: tokenCreatorPaymentAddress is required"); contracts/NFTCollection.sol:263: require(bytes(tokenCID).length != 0, "NFTCollection: tokenCID is required"); contracts/NFTCollection.sol:264: require(!cidToMinted[tokenCID], "NFTCollection: NFT was already minted"); contracts/NFTCollection.sol:268: require(maxTokenId == 0 || tokenId <= maxTokenId, "NFTCollection: Max token count has already been minted"); contracts/NFTCollection.sol:327: require(_exists(tokenId), "NFTCollection: URI query for nonexistent token");
uint256
variables togetherAs we know that in the EVM, a slot is of 32bytes (256bits). So when we define a uint256
it takes the full slot. But when we have a uints that are not uint256
like uint8 & unit32
then the EVM merges them in one slot and gas gets optimized. But for that we have to make sure that both the non-uint256s are defined together. So as in the `` we have two uint32
defined, but they are not defined together, sowe cannot take the advantage of EVM's feature.
So from this:
address public implementationNFTCollection; /** * @notice The implementation version of new NFTCollections. * @dev This is auto-incremented each time `implementationNFTCollection` is changed. * @return The current NFTCollection implementation version. */ uint32 public versionNFTCollection; /****** Slot 1 ******/ /** * @notice The address of the implementation all new NFTDropCollections will leverage. * @dev When this is changed, `versionNFTDropCollection` is incremented. * @return The implementation address for NFTDropCollection. */ address public implementationNFTDropCollection; /** * @notice The implementation version of new NFTDropCollections. * @dev This is auto-incremented each time `implementationNFTDropCollection` is changed. * @return The current NFTDropCollection implementation version. */ uint32 public versionNFTDropCollection; /****** End of storage ******/
Change it into this:
address public implementationNFTCollection; /****** Slot 1 ******/ /** * @notice The address of the implementation all new NFTDropCollections will leverage. * @dev When this is changed, `versionNFTDropCollection` is incremented. * @return The implementation address for NFTDropCollection. */ address public implementationNFTDropCollection; /** * @notice The implementation version of new NFTCollections. * @dev This is auto-incremented each time `implementationNFTCollection` is changed. * @return The current NFTCollection implementation version. */ uint32 public versionNFTCollection; /** * @notice The implementation version of new NFTDropCollections. * @dev This is auto-incremented each time `implementationNFTDropCollection` is changed. * @return The current NFTDropCollection implementation version. */ uint32 public versionNFTDropCollection; /****** End of storage ******/
uint256
instead of uint32
replace units lower than uint256 like uint32
with uint256
, which will cost less gas
contracts/NFTCollectionFactory.sol:80: uint32 public versionNFTCollection; contracts/NFTCollectionFactory.sol95: uint32 public versionNFTDropCollection; contracts/mixins/nftDropMarket/NFTDropMarketFixedPriceSale.sol:54: uint80 price; contracts/mixins/nftDropMarket/NFTDropMarketFixedPriceSale.sol:58: uint16 limitPerAccount;
For more info check this post Change these:
- contracts/NFTCollectionFactory.sol:207: versionNFTCollection++; + contracts/NFTCollectionFactory.sol:207: ++versionNFTCollection; - contracts/NFTCollectionFactory.sol:231: versionNFTDropCollection++; + contracts/NFTCollectionFactory.sol:231: ++versionNFTDropCollection;
latestTokenId
should be cached into memory. Then used inside a Loop.In contracts/NFTDropCollection.sol
there is a loop, which access latestTokenId
again and again from the storage.
line 181: for (uint256 i = firstTokenId; i <= latestTokenId; ) {
Inside the function, before the loop, define `_latestTokenId = latestTokenId; and then use that inside the loop. Something like this:
line 181: for (uint256 i = firstTokenId; i <= _latestTokenId; ) {
contracts/NFTDropCollection.sol:20: import "./mixins/shared/Constants.sol"; contracts/NFTDropMarket.sol:47: import "./mixins/shared/Constants.sol";
#0 - batu-inal
2022-08-19T08:56:39Z
Use Custom Errors instead of require statements.
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.
Pack non-uint256 variables together
Invalid. Both approaches take up two slots in storage; however, the current version is more efficient for read/access patterns since e.g. implementationNFTCollection/versionNFTCollection are always accessed at the same time from a single slot. Where as the recommendation would require reading 2 slots.
Use uint256 instead of uint32
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.
Pre-increments costs unnecessary gas
Agreed will fix.
latestTokenId should be cached into memory. Then used inside a Loop.
Valid, will fix. Tests show as much as 1,400 gas saved when minting 10.
Unused & Uneccassary Imports
Agreed will fix.