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: 42/108
Findings: 2
Award: $67.43
🌟 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.7141 USDC - $41.71
pragma solidity ^0.8.0;
its in most of the main contracts https://github.com/code-423n4/2022-08-foundation/blob/7d6392498e8f3b8cdc22beb582188ffb3ed25790/contracts/mixins/nftDropMarket/NFTDropMarketFixedPriceSale.sol#L3
when burnCounter overflow to zero then we dont know that what is burned which then only has tokendIds
and not minus the burnedTokens
supply = latestTokenId - burnCounter; }
maxTokenId
and make it more rarecreator says maxTokens is 100 but then one day the creators is like lets make it more rare so he makes Maxtokens to 50 causing users destrust of how many are going to be out there. There can even have a dos after like 1 users mints because then if max tokens is 2 and admin buy one and a user makes a tx to buy one then the admin can frontrun to make it the users tx revert and cause gas funds to be lost . make maxtokens a immutable to give users more trust in buying nfts from collectors. https://github.com/code-423n4/2022-08-foundation/blob/7d6392498e8f3b8cdc22beb582188ffb3ed25790/contracts/mixins/collections/SequentialMintCollection.sol#L86
make it some how better then having a gas off. myabe tell users to use private tx,flashbots to avoid this. https://github.com/code-423n4/2022-08-foundation/blob/7d6392498e8f3b8cdc22beb582188ffb3ed25790/contracts/mixins/nftDropMarket/NFTDropMarketFixedPriceSale.sol#L170
10-20-1= -11
which since uint and not in unchecked block it will revert so on certin times when users are buying nfts it will revert.
// 10 -20 - 1 = -11 sellerRev = price - totalFees - creatorRev; }
https://github.com/code-423n4/2022-08-foundation/blob/7d6392498e8f3b8cdc22beb582188ffb3ed25790/contracts/mixins/shared/MarketFees.sol#L473 make it into a unchecked block
/// @dev This value was replaced with an immutable version. address payable private __gap_was_treasury;
https://github.com/code-423n4/2022-08-foundation/blob/254d384116d22266b12916f1aeb46b9ef0143f1f/contracts/NFTCollectionFactory.sol#L133 https://github.com/code-423n4/2022-08-foundation/blob/254d384116d22266b12916f1aeb46b9ef0143f1f/contracts/NFTCollectionFactory.sol#L158
#0 - HardlyDifficult
2022-08-18T17:49:05Z
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.
Remove commented out code
Disagree - this comment is detailing how that awkwardly placed gap was previously used so future me can understand why it's important for upgrade compatibility that we don't leverage that space again (since those slots are dirty with data).
you can remove that gap because they have alot of them in the drevitve contracts
Disagree. As the comment there is trying to convey, the data at those locations is dirty and should not be leveraged again in the future.
accounting of burning can happen only when there are enough nfts
Invalid, burnCounter cannot be made to overflow.
creator can decrease maxTokenId and make it more rare
Disagree / by design. See also https://github.com/code-423n4/2022-08-foundation-findings/issues/254
#1 - HickupHH3
2022-09-01T09:15:23Z
if there is one nft left it wouldnt be fair to a user who makes tx then a frontruning bot makes the same tx bu with more gas and the frontrun bot gets the nft.
similar to botting issue #93, though unclear why it applies only to the "1 NFT" and not the entire sale.
there an be a underflow but since its not in a unchecked will revert
disagree, underflow cannot happen because totalFees
+ creatorRev
cannot exceed price
.
other issues that were not commented by the sponsor have been mentioned by other wardens, will score accordingly.
🌟 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
25.7188 USDC - $25.72
NFTCollectionFactory.sol:207: versionNFTCollection++; NFTCollectionFactory.sol:231: versionNFTDropCollection++;
mixins/shared/MarketFees.sol:126: for (uint256 i = 0; i < creatorRecipients.length; ++i) { mixins/shared/MarketFees.sol:198: for (uint256 i = 0; i < creatorShares.length; ++i) { mixins/shared/MarketFees.sol:484: for (uint256 i = 0; i < creatorRecipients.length; ++i) {
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) Source Custom Errors in Solidity: Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them. Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).
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"); NFTCollectionFactory.sol:173: require(rolesContract.isAdmin(msg.sender), "NFTCollectionFactory: Caller does not have the Admin role"); NFTCollectionFactory.sol:178: * @notice Defines requirements for the collection drop factory at deployment time. 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"); 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"); NFTDropCollection.sol:301: _requireMinted(tokenId);
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met. Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc. 1 byte for character
NFTCollection.sol:158: require(tokenCreatorPaymentAddress != address(0), "NFTCollection: tokenCreatorPaymentAddress is required"); 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"); NFTCollectionFactory.sol:173: require(rolesContract.isAdmin(msg.sender), "NFTCollectionFactory: Caller does not have the Admin role"); NFTCollectionFactory.sol:178: * @notice Defines requirements for the collection drop factory at deployment time. 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");
Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn’t possible (as an example, when a comparison is made before the arithmetic operation), some gas can be saved by using an unchecked block: Checked or Unchecked Arithmetic. I suggest wrapping with an unchecked block: Same thing with second unchecked because total can't overflow amount cant overflow
Unchecked { uint256 total = amount + hasMinted[msg.sender]; }
mixins/shared/MarketFees.sol:126: for (uint256 i = 0; i < creatorRecipients.length; ++i) { mixins/shared/MarketFees.sol:198: for (uint256 i = 0; i < creatorShares.length; ++i) { mixins/shared/MarketFees.sol:484: for (uint256 i = 0; i < creatorRecipients.length; ++i) {
// Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
mixins/shared/MarketFees.sol:40: * mapping(address => mapping(uint256 => bool)) private _nftContractToTokenIdToFirstSaleCompleted; mixins/shared/MarketFees.sol:61: bool private immutable assumePrimarySale;
require (uint amount >0);
intorequire (!= 0);
because a uint is ether 0 or greater so it saves gas .
NFTDropCollection.sol:131: require(_maxTokenId > 0, "NFTDropCollection: `_maxTokenId` must be set");
ex: 0x00000000000000
mixins/shared/MarketFees.sol:358: if (creator != address(0)) { mixins/shared/MarketFees.sol:369: if (owner != address(0)) { mixins/shared/MarketFees.sol:522: if (buyReferrer != address(0) && buyReferrer != msg.sender && buyReferrer != seller && buyReferrer != creator) NFTDropMarket.sol:118: return payable(address(0)); NFTDropMarket.sol:140: if (owner != address(0)) { NFTDropMarket.sol:116: if (owner != address(0)) { NFTDropCollection.sol:138: if (_paymentAddress != address(0)) { NFTDropCollection.sol:149: if (_approvedMinter != address(0)) { NFTDropCollection.sol:256: if (creatorPaymentAddress == address(0)) { NFTCollection.sol:305: if (creatorPaymentAddress == address(0)) { NFTCollection.sol:158: require(tokenCreatorPaymentAddress != address(0), "NFTCollection: tokenCreatorPaymentAddress is required");
ex: iszero
mixins/shared/MarketFees.sol:358: if (creator != address(0)) { mixins/shared/MarketFees.sol:369: if (owner != address(0)) { mixins/shared/MarketFees.sol:522: if (buyReferrer != address(0) && buyReferrer != msg.sender && buyReferrer != seller && buyReferrer != creator) NFTDropMarket.sol:118: return payable(address(0)); NFTDropMarket.sol:140: if (owner != address(0)) { NFTDropMarket.sol:116: if (owner != address(0)) { NFTDropCollection.sol:138: if (_paymentAddress != address(0)) { NFTDropCollection.sol:149: if (_approvedMinter != address(0)) { NFTDropCollection.sol:256: if (creatorPaymentAddress == address(0)) { NFTCollection.sol:305: if (creatorPaymentAddress == address(0)) { NFTCollection.sol:158: require(tokenCreatorPaymentAddress != address(0), "NFTCollection: tokenCreatorPaymentAddress is required");
If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2),DUP1(3),ISZERO(3),PUSH2(3),JUMPI(10),PUSH1(3),DUP1(3),REVERT(0),JUMPDEST(1),POP(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
NFTCollection.sol:119: function burn(uint256 tokenId) public override onlyCreator { NFTCollection.sol:230: function selfDestruct() external onlyCreator { NFTCollection.sol:238: function updateBaseURI(string calldata baseURIOverride) external onlyCreator { NFTCollection.sol:251: function updateMaxTokenId(uint32 _maxTokenId) external onlyCreator { NFTCollection.sol:262: function _mint(string calldata tokenCID) private onlyCreator returns (uint256 tokenId) { NFTDropCollection.sol:159: function burn(uint256 tokenId) public override onlyAdmin { NFTDropCollection.sol:195: function reveal(string calldata _baseURI) external onlyAdmin validBaseURI(_baseURI) onlyWhileUnrevealed { NFTDropCollection.sol:209: function selfDestruct() external onlyAdmin { NFTDropCollection.sol:220: function updateMaxTokenId(uint32 _maxTokenId) external onlyAdmin { NFTCollectionFactory.sol:202: function adminUpdateNFTCollectionImplementation(address _implementation) external onlyAdmin { NFTCollectionFactory.sol:226: function adminUpdateNFTDropCollectionImplementation(address _implementation) external onlyAdmin {
function getFethAddress() external view returns (address fethAddress) { fethAddress = address(feth); }
change it to return feth;
saves 3 gas
https://github.com/code-423n4/2022-08-foundation/blob/b4e1dc6a9a434d576cb8f74676e7dbb65dacc98d/contracts/mixins/shared/FETHNode.sol#L69
2. you can save gas by not using this memory variable
remove delta
and just do the math when doing the functoin to save 3 gas
uint256 delta = totalAmount - msg.value; feth.marketWithdrawFrom(msg.sender, delta); }
change to
feth.marketWithdrawFrom(msg.sender,totalAmount-msg.value);
#0 - batu-inal
2022-08-19T13:51:43Z
make i++ into ++i to save 3 gas on copy
Valid. We will make this change.
make array.length into a cahced variable to sav gas
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.
Use Custom Errors instead of Revert Strings 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.
Reduce the size of error messages (Long revert Strings)
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.
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.
Unchecking arithmetics operations that can’t underflow/overflow
valid we will make the changes for the last 3 for loops; however, the first recommendation is not from our codebase / this competition
save gas by making require (uint amount >0); into
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
make address(0) into long form to save gas & USE ASSEMBLY TO CHECK FOR ADDRESS(0)
valid but won't fix. we prefer readability and code maintainability over these types of small gas improvements.
FUNCTIONS GUARANTEED TO REVERT WHEN CALLED BY NORMAL USERS CAN BE MARKED PAYABLE
while this may save on gas, it's an optimization that does not follow the definition of payable: Functions and addresses declared payable can receive ether into the contract.
hence makes the code confusing
you can save gas by just reading the immutable variable
valid, esp for #2 we will make this change.