Platform: Code4rena
Start Date: 24/02/2022
Pot Size: $75,000 USDC
Total HM: 21
Participants: 28
Period: 7 days
Judge: alcueca
Total Solo HM: 15
Id: 94
League: ETH
Rank: 10/28
Findings: 2
Award: $1,528.74
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: leastwood
Also found by: 0x1f8b, 0xliumin, 0xwags, CertoraInc, Dravee, IllIllI, Ruhum, TerrierLover, WatchPug, cmichel, csanuragjain, defsec, gzeon, hubble, jayjonah8, kenta, kirk-baird, rfa, robee
454.886 USDC - $454.89
Table of Contents:
@audit
tagsThe code is annotated at multiple places with
//@audit
comments to pinpoint the issues. Please, pay attention to them for more details.
FoundationTreasury.sol:initialize(address admin)
is front-runnable. Details below.^0.8.0
. As custom errors were introduced in 0.8.4
, I suggest replacing the pragmas with at least 0.8.4
(and fixing a version before deployment).@param
and @return
comments are extensively documented on events and functions. However, at some places, @param
comments are missing. Details below.File: FoundationTreasury.sol 55: /** 56: * @title Manage revenue and roles for Foundation. 57: * @notice All fees generated by the market are forwarded to this contract. 58: * It also defines the Admin and Operator roles which are used in other contracts. 59: */ 60: contract FoundationTreasury is AdminRole, OperatorRole, CollateralManagement, WithdrawFromEscrow { 61: function initialize(address admin) external initializer { //@audit front-runnable & L57 "All fees generated by the market are forwarded to this contract." 62: AdminRole._initializeAdminRole(admin); 63: } 64: }
While this initialized
is protected against multiple calls, it doesn't implement any type of access-control. As it is mentioned L57 that "All fees generated by the market are forwarded to this contract.", I suggest adding some form of access-control (as an example: the deployer being the default/initial owner).
141: /** 142: * @notice Checks if an escrowed NFT is currently in active auction. 143: * @return Returns false if the auction has ended, even if it has not yet been settled. //@audit missing @param nftContract and @param tokenId 144: */ 145: function _isInActiveAuction(address nftContract, uint256 tokenId) internal view virtual returns (bool);
@param nftContract
40: /** 41: * @notice Emitted when an NFT is sold in a private sale. 42: * @dev The total amount of this sale is `f8nFee` + `creatorFee` + `ownerRev`. 43: * @param nftContract The address of the NFT contract. 44: * @param tokenId The ID of the NFT. 45: * @param seller The address of the seller. 46: * @param buyer The address of the buyer. 47: * @param f8nFee The amount of ETH that was sent to Foundation for this sale. 48: * @param creatorFee The amount of ETH that was sent to the creator for this sale. 49: * @param ownerRev The amount of ETH that was sent to the owner for this sale. //@audit missing @param deadline 50: */ 51: event PrivateSaleFinalized( 52: address indexed nftContract, 53: uint256 indexed tokenId, 54: address indexed seller, 55: address buyer, 56: uint256 f8nFee, 57: uint256 creatorFee, 58: uint256 ownerRev, 59: uint256 deadline 60: );
@param deadline
483: /** 484: * @notice Settle an auction that has already ended. 485: * This will send the NFT to the highest bidder and distribute revenue for this sale. 486: * @param keepInEscrow If true, the NFT will be kept in escrow to save gas by avoiding //@audit missing @param auctionId 487: * redundant transfers if the NFT should remain in escrow, such as when the new owner 488: * sets a buy price or lists it in a new auction. 489: */ 490: function _finalizeReserveAuction(uint256 auctionId, bool keepInEscrow) private {
@param auctionId
Several functions are declaring named returns but then are using return statements. I suggest choosing only one for readability reasons.
Places declaring a named returns but using a return statement:
contracts/FETH.sol: 212: function approve(address spender, uint256 amount) external returns (bool success) { 215: return true; 266: ) external payable onlyFoundationMarket returns (uint256 expiration) { 268: return _marketLockupFor(lockupFor, lockupAmount); 284: returns (uint256 expiration) 286: return _marketLockupFor(account, amount); 356: function transfer(address to, uint256 amount) external returns (bool success) { 357: return transferFrom(msg.sender, to, amount); 371: ) public returns (bool success) { 391: return true; 470: function _freeFromEscrow(address account) private returns (AccountInfo storage) { 477: return accountInfo; 510: return accountInfo; 661: function allowance(address account, address operator) external view returns (uint256 amount) { 663: return accountInfo.allowance[operator]; 693: function getFoundationMarket() external view returns (address market) { 694: return foundationMarket; 774: function totalSupply() external view returns (uint256 supply) { 777: return address(this).balance; contracts/FNDNFTMarket.sol: 164: returns (address payable seller) 166: return super._getSellerFor(nftContract, tokenId); contracts/libraries/AccountMigrationLibrary.sol: 68: function _char(bytes1 b) private pure returns (bytes1 c) { 70: if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); 71: else return bytes1(uint8(b) + 0x57); contracts/mixins/FoundationTreasuryNode.sol: 60: function getFoundationTreasury() public view returns (address payable treasuryAddress) { 61: return treasury; contracts/mixins/NFTMarketBuyPrice.sol: 337: function getBuyPrice(address nftContract, uint256 tokenId) external view returns (address seller, uint256 price) { 340: return (address(0), type(uint256).max); 342: return (buyPrice.seller, buyPrice.price); contracts/mixins/NFTMarketCore.sol: 112: function getFethAddress() external view returns (address fethAddress) { 113: return address(feth); contracts/mixins/NFTMarketCreators.sol: 257: function getRoyaltyRegistry() public view returns (address registry) { 258: return address(royaltyRegistry); contracts/mixins/NFTMarketOffer.sol: 358: function getMinOfferAmount(address nftContract, uint256 tokenId) external view returns (uint256 minimum) { 361: return _getMinIncrement(offer.amount); 364: return 1; 382: returns ( 383: address buyer, 384: uint256 expiration, 385: uint256 amount 391: return (address(0), 0, 0); 395: return (offer.buyer, offer.expiration, offer.amount); contracts/mixins/NFTMarketReserveAuction.sol: 617: function getMinBidAmount(uint256 auctionId) external view returns (uint256 minimum) { 620: return auction.amount; 622: return _getMinIncrement(auction.amount); 630: function getReserveAuction(uint256 auctionId) external view returns (ReserveAuction memory auction) { 631: return auctionIdToAuction[auctionId]; 641: function getReserveAuctionIdFor(address nftContract, uint256 tokenId) external view returns (uint256 auctionId) { 642: return nftContractToTokenIdToAuctionId[nftContract][tokenId]; contracts/mixins/SendValueWithFallbackWithdraw.sol: 84: function getPendingWithdrawal(address user) external view returns (uint256 balance) { 85: return pendingWithdrawals[user];
#0 - HardlyDifficult
2022-03-09T19:40:56Z
return
instead. We'll still keep the return param name there as it's good for documentation.#1 - alcueca
2022-03-17T09:22:06Z
Unadjusted score: 50 (Including 20 points for nice formatting)
🌟 Selected for report: Dravee
Also found by: 0x1f8b, CertoraInc, TerrierLover, csanuragjain, gzeon, iain, kenta, rfa, robee, thankthedark
Table of Contents:
The code can be optimized by minimising the number of SLOADs. SLOADs are expensive (100 gas) compared to MLOADs/MSTOREs (3 gas). In the paragraphs below, please see the
@audit-issue
tags in the pieces of code's comments for more information about SLOADs that could be saved by caching the mentioned storage variables in memory variables.
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, or the operation doesn't depend on user input), some gas can be saved by using an
unchecked
block: https://docs.soliditylang.org/en/v0.8.10/control-structures.html#checked-or-unchecked-arithmetic
@audit
tagsThe code is annotated at multiple places with
//@audit
comments to pinpoint the issues. Please, pay attention to them for more details.
File: FETH.sol 441: function _deductAllowanceFrom(AccountInfo storage accountInfo, uint256 amount) private { 442: if (accountInfo.allowance[msg.sender] != type(uint256).max) { //@audit accountInfo.allowance[msg.sender] SLOAD 1 443: if (accountInfo.allowance[msg.sender] < amount) { //@audit accountInfo.allowance[msg.sender] SLOAD 2 444: revert FETH_Insufficient_Allowance(accountInfo.allowance[msg.sender]); //@audit accountInfo.allowance[msg.sender] SLOAD 3 445: } 446: // The check above ensures allowance cannot underflow. 447: unchecked { 448: accountInfo.allowance[msg.sender] -= amount; //@audit accountInfo.allowance[msg.sender] SLOAD 3 449: } 450: } 451: }
accountInfo.allowance[msg.sender]
Caching this in memory can save around 2 SLOADs (around 200 gas). This is due to the fact that both conditions will get evaluated before L448, which is using -=
(therefore making a SLOAD + SSTORE)
I recommend caching this value and using it as such:
function _deductAllowanceFrom(AccountInfo storage accountInfo, uint256 amount) private { uint256 _allowance = accountInfo.allowance[msg.sender]; //@audit 1 MSTORE + 1 SLOAD if (_allowance != type(uint256).max) { //@audit 1 MLOAD spent if (_allowance < amount) { //@audit 1 SLOAD saved, 1 MLOAD spent revert FETH_Insufficient_Allowance(_allowance); //@audit 1 SLOAD saved, 1 MLOAD spent } // The check above ensures allowance cannot underflow. unchecked { accountInfo.allowance[msg.sender] = _allowance - amount; //@audit 1 SLOAD saved, 1 MLOAD spent } } }
File: FETH.sol 456: function _deductBalanceFrom(AccountInfo storage accountInfo, uint256 amount) private { 457: // Free from escrow in order to consider any expired escrow balance 458: if (accountInfo.freedBalance < amount) { //@audit accountInfo.freedBalance SLOAD 1 459: revert FETH_Insufficient_Available_Funds(accountInfo.freedBalance); //@audit accountInfo.freedBalance SLOAD 2 460: } 461: // The check above ensures balance cannot underflow. 462: unchecked { 463: accountInfo.freedBalance -= uint96(amount); //@audit accountInfo.freedBalance SLOAD 2 464: } 465: }
accountInfo.freedBalance
Caching this in memory can save around 1 SLOAD (around 100 gas). I recommend caching this value and using it as such:
function _deductBalanceFrom(AccountInfo storage accountInfo, uint256 amount) private { uint256 _freedBalance = accountInfo.freedBalance; //@audit 1 MSTORE + 1 SLOAD // Free from escrow in order to consider any expired escrow balance if (_freedBalance < amount) { ///@audit 1 MLOAD spent revert FETH_Insufficient_Available_Funds(_freedBalance); //@audit 1 SLOAD saved, 1 MLOAD spent } // The check above ensures balance cannot underflow. unchecked { accountInfo.freedBalance = _freedBalance - uint96(amount); //@audit 1 SLOAD saved, 1 MLOAD spent } }
accountInfo.freedBalance
or call _deductBalanceFrom
to save gas while saving some sizeImpacted code:
File: FETH.sol 535: // The check above prevents underflow with delta. 536: unchecked { 537: uint256 delta = amount - msg.value; //@audit just use / should call private _deductBalanceFrom(accountInfo, amount - msg.value); : 6.833 vs 6.894 538: if (accountInfo.freedBalance < delta) { //@audit accountInfo.freedBalance SLOAD 1 539: revert FETH_Insufficient_Available_Funds(accountInfo.freedBalance); //@audit accountInfo.freedBalance SLOAD 2 540: } 541: // The check above prevents underflow of freed balance. 542: accountInfo.freedBalance -= uint96(delta); //@audit accountInfo.freedBalance SLOAD 2 543: }
The optimization by caching accountInfo.freedBalance
would be exactly the same as above, which would save around 100 gas. However, here, it'd be better to actually call the optimized _deductBalanceFrom
to benefit from the previous gas-gains and reduce the contract's size (0.061KB saved).
The code should become:
File: FETH.sol 534: if (msg.value < amount) { 535: _deductBalanceFrom(accountInfo, amount - msg.value); 536: } else if (msg.value != amount) { 537: // There's no reason to send msg.value more than the amount being locked up 538: revert FETH_Too_Much_ETH_Provided(); 539: }
File: NFTMarketBuyPrice.sol 125: function cancelBuyPrice(address nftContract, uint256 tokenId) external nonReentrant { 126: BuyPrice storage buyPrice = nftContractToTokenIdToBuyPrice[nftContract][tokenId]; 127: if (buyPrice.seller == address(0)) { //@audit buyPrice.seller SLOAD 1 128: // This check is redundant with the next one, but done in order to provide a more clear error message. 129: revert NFTMarketBuyPrice_Cannot_Cancel_Unset_Price(); 130: } else if (buyPrice.seller != msg.sender) { //@audit buyPrice.seller SLOAD 2 (evaluated even if false) 131: revert NFTMarketBuyPrice_Only_Owner_Can_Cancel_Price(buyPrice.seller); //@audit buyPrice.seller SLOAD 3 132: } ... 141: }
buyPrice.seller
Caching this in memory can save around 2 SLOADs (around 200 gas).
File: NFTMarketBuyPrice.sol 150: function setBuyPrice( ... 165: BuyPrice storage buyPrice = nftContractToTokenIdToBuyPrice[nftContract][tokenId]; ... 170: if (buyPrice.seller == address(0)) { //@audit buyPrice.seller SLOAD 1 171: // Transfer the NFT into escrow, if it's already in escrow confirm the `msg.sender` is the owner. 172: _transferToEscrow(nftContract, tokenId); 173: 174: // The price was not previously set for this NFT, store the seller. 175: buyPrice.seller = payable(msg.sender); 176: } else if (buyPrice.seller != msg.sender) { //@audit buyPrice.seller SLOAD 2 (evaluated even if false) 177: // Buy price was previously set by a different user 178: revert NFTMarketBuyPrice_Only_Owner_Can_Set_Price(buyPrice.seller); //@audit buyPrice.seller SLOAD 3 179: } ... 182: }
buyPrice.seller
Caching this in memory can save around 2 SLOADs (around 200 gas).
File: NFTMarketBuyPrice.sol 276: function _transferFromEscrow( ... 282: BuyPrice storage buyPrice = nftContractToTokenIdToBuyPrice[nftContract][tokenId]; 283: if (buyPrice.seller != address(0)) { //@audit buyPrice.seller SLOAD 1 284: // A buy price was set for this NFT. 285: if (buyPrice.seller != seller) { //@audit buyPrice.seller SLOAD 2 286: // When there is a buy price set, the `buyPrice.seller` is the owner of the NFT. 287: revert NFTMarketBuyPrice_Seller_Mismatch(buyPrice.seller); //@audit buyPrice.seller SLOAD 3 288: } ... 294: }
buyPrice.seller
Caching this in memory can save around 2 SLOADs (around 200 gas).
File: NFTMarketBuyPrice.sol 316: function _transferToEscrow(address nftContract, uint256 tokenId) internal virtual override { 317: BuyPrice storage buyPrice = nftContractToTokenIdToBuyPrice[nftContract][tokenId]; 318: if (buyPrice.seller == address(0)) { //@audit buyPrice.seller SLOAD 1 319: // The NFT is not in escrow for buy now. 320: super._transferToEscrow(nftContract, tokenId); 321: } else if (buyPrice.seller != msg.sender) { //@audit buyPrice.seller SLOAD 2 322: // When there is a buy price set, the `buyPrice.seller` is the owner of the NFT. 323: revert NFTMarketBuyPrice_Seller_Mismatch(buyPrice.seller); //@audit buyPrice.seller SLOAD 3 324: } 325: }
buyPrice.seller
Caching this in memory can save around 2 SLOADs (around 200 gas).
File: NFTMarketBuyPrice.sol 337: function getBuyPrice(address nftContract, uint256 tokenId) external view returns (address seller, uint256 price) { 338: BuyPrice storage buyPrice = nftContractToTokenIdToBuyPrice[nftContract][tokenId]; 339: if (buyPrice.seller == address(0)) { //@audit buyPrice.seller SLOAD 1 340: return (address(0), type(uint256).max); 341: } 342: return (buyPrice.seller, buyPrice.price); //@audit buyPrice.seller SLOAD 2 343: }
buyPrice.seller
Caching this in memory can save around 1 SLOAD (around 100 gas).
File: NFTMarketFees.sol 48: function _distributeFunds( ... 74: if (creatorFee > 0) { 75: if (creatorRecipients.length > 1) { 76: uint256 maxCreatorIndex = creatorRecipients.length - 1; //@audit should be unchecked too (see L75) 77: if (maxCreatorIndex > MAX_ROYALTY_RECIPIENTS_INDEX) { 78: maxCreatorIndex = MAX_ROYALTY_RECIPIENTS_INDEX; 79: } 80: 81: // Determine the total shares defined so it can be leveraged to distribute below 82: uint256 totalShares; 83: unchecked { 84: // The array length cannot overflow 256 bits. 85: for (uint256 i; i <= maxCreatorIndex; ++i) { 86: if (creatorShares[i] > BASIS_POINTS) { 87: // If the numbers are >100% we ignore the fee recipients and pay just the first instead 88: maxCreatorIndex = 0; 89: break; 90: } 91: // The check above ensures totalShares wont overflow. 92: totalShares += creatorShares[i]; 93: } 94: }
This line can't underflow due to L75. Therefore, it should be wrapped in an unchecked
block.
I'd suggest starting the unchecked
block L83 at line 76.
File: NFTMarketOffer.sol 189: function makeOffer( ... 204: Offer storage offer = nftContractToIdToOffer[nftContract][tokenId]; 205: 206: if (offer.expiration < block.timestamp) { //@audit offer.expiration SLOAD 1 ... 211: } else { ... 214: if (amount < _getMinIncrement(offer.amount)) { //@audit offer.amount SLOAD 1 215: // A non-trivial increase in price is required to avoid sniping 216: revert NFTMarketOffer_Offer_Must_Be_At_Least_Min_Amount(_getMinIncrement(offer.amount)); //@audit offer.amount SLOAD 2 217: } ... 221: expiration = feth.marketChangeLockup{ value: msg.value }( 222: offer.buyer, 223: offer.expiration, //@audit offer.expiration SLOAD 2 224: offer.amount, //@audit offer.amount SLOAD 2 225: msg.sender, 226: amount 227: ); 228: }
offer.expiration
Caching this in memory can save around 1 SLOAD (around 100 gas).
offer.amount
Caching this in memory can save around 1 SLOAD (around 100 gas).
File: NFTMarketOffer.sol 379: function getOffer(address nftContract, uint256 tokenId) ... 388: Offer storage offer = nftContractToIdToOffer[nftContract][tokenId]; 389: if (offer.expiration < block.timestamp) { //@audit offer.expiration SLOAD 1 390: // Offer not found or has expired 391: return (address(0), 0, 0); 392: } 393: 394: // An offer was found and it has not yet expired. 395: return (offer.buyer, offer.expiration, offer.amount); //@audit offer.expiration SLOAD 2 396: }
offer.expiration
Caching this in memory can save around 1 SLOAD (around 100 gas).
File: NFTMarketReserveAuction.sol 263: function adminAccountMigration( 264: uint256[] calldata listedAuctionIds, 265: address originalAddress, 266: address payable newAddress, 267: bytes memory signature //@audit gas should be calldata 268: ) external onlyFoundationOperator { 269: // Validate the owner of the original account has approved this change. 270: originalAddress.requireAuthorizedAccountMigration(newAddress, signature); 271: 272: unchecked { 273: // The array length cannot overflow 256 bits. 274: for (uint256 i; i < listedAuctionIds.length; ++i) { 275: uint256 auctionId = listedAuctionIds[i]; 276: ReserveAuction storage auction = auctionIdToAuction[auctionId]; 277: if (auction.seller != address(0)) { //@audit auction.seller SLOAD 1 278: // Only if the auction was found and not finalized before this transaction. 279: 280: if (auction.seller != originalAddress) { //@audit auction.seller SLOAD 2 281: // Confirm that the signature approval was the correct owner of this auction. 282: revert NFTMarketReserveAuction_Cannot_Migrate_Non_Matching_Seller(auction.seller); //@audit auction.seller SLOAD 3 283: } 284: 285: // Update the auction's seller address. 286: auction.seller = newAddress; 287: 288: emit ReserveAuctionSellerMigrated(auctionId, originalAddress, newAddress); 289: } 290: } 291: } 292: }
calldata
instead of memory
for external functions where the function argument is read-onlyHere, bytes memory signature
should be bytes calldata signature
auction.seller
Caching this in memory can save around 2 SLOADs (around 200 gas).
File: NFTMarketReserveAuction.sol 386: function placeBidOf(uint256 auctionId, uint256 amount) public payable nonReentrant { ... 402: ReserveAuction storage auction = auctionIdToAuction[auctionId]; 403: 404: if (auction.amount == 0) { //@audit auction.amount SLOAD 1 405: // No auction found 406: revert NFTMarketReserveAuction_Cannot_Bid_On_Nonexistent_Auction(); 407: } 408: 409: if (auction.endTime == 0) { //@audit auction.endTime SLOAD 1 410: // This is the first bid, kicking off the auction. 411: 412: if (auction.amount > amount) { //@audit auction.amount SLOAD 2 413: // The bid must be >= the reserve price. 414: revert NFTMarketReserveAuction_Cannot_Bid_Lower_Than_Reserve_Price(auction.amount); //@audit auction.amount SLOAD 3 415: } ... 429: } else { 430: if (auction.endTime < block.timestamp) { //@audit auction.endTime SLOAD 2 431: // The auction has already ended. 432: revert NFTMarketReserveAuction_Cannot_Bid_On_Ended_Auction(auction.endTime); //@audit auction.endTime SLOAD 3 433: } else if (auction.bidder == msg.sender) { //@audit auction.bidder SLOAD 1 434: // We currently do not allow a bidder to increase their bid unless another user has outbid them first. 435: revert NFTMarketReserveAuction_Cannot_Rebid_Over_Outstanding_Bid(); 436: } else if (amount < _getMinIncrement(auction.amount)) { //@audit auction.amount SLOAD 2 437: // If this bid outbids another, it must be at least 10% greater than the last bid. 438: revert NFTMarketReserveAuction_Bid_Must_Be_At_Least_Min_Amount(_getMinIncrement(auction.amount)); //@audit auction.amount SLOAD 3 439: } 440: 441: // Cache and update bidder state 442: uint256 originalAmount = auction.amount; //@audit auction.amount SLOAD 3 443: address payable originalBidder = auction.bidder; //@audit auction.bidder SLOAD 2 444: auction.amount = amount; 445: auction.bidder = payable(msg.sender); 446: 447: unchecked { 448: // When a bid outbids another, check to see if a time extension should apply. 449: // We confirmed that the auction has not ended, so endTime is always >= the current timestamp. 450: if (auction.endTime - block.timestamp < auction.extensionDuration) { //@audit auction.endTime SLOAD 3 //@audit auction.extensionDuration SLOAD 1 451: // Current time plus extension duration (always 15 mins) cannot overflow. 452: auction.endTime = block.timestamp + auction.extensionDuration; //@audit auction.extensionDuration SLOAD 2 453: } 454: } 455: 456: // Refund the previous bidder 457: _sendValueWithFallbackWithdraw(originalBidder, originalAmount, SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT); 458: } 459: 460: emit ReserveAuctionBidPlaced(auctionId, msg.sender, amount, auction.endTime); //@audit auction.endTime SLOAD 4 461: }
auction.endTime
Caching this in memory can save around 3 SLOADs (around 300 gas).
auction.extensionDuration
Caching this in memory can save around 1 SLOAD (around 100 gas).
auction.bidder
Caching this in memory can save around 1 SLOAD (around 100 gas).
auction.amount
Caching this in memory can save around 2 SLOADs (around 200 gas).
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
Caching the array length in the stack saves around 3 gas per iteration.
Therefore, it's possible to save a significant amount of gas (>= 102 after 34 iterations).
Here, I suggest storing the array's length in a variable before the for-loop, and use it instead:
mixins/OZ/ERC165Checker.sol:64: for (uint256 i = 0; i < interfaceIds.length; ++i) { mixins/OZ/ERC165Checker.sol:90: for (uint256 i = 0; i < interfaceIds.length; ++i) { mixins/NFTMarketCreators.sol:94: for (uint256 i = 0; i < _recipients.length; ++i) { mixins/NFTMarketCreators.sol:155: for (uint256 i = 0; i < _recipients.length; ++i) { mixins/NFTMarketCreators.sol:193: for (uint256 i = 0; i < _recipients.length; ++i) { mixins/NFTMarketOffer.sol:161: for (uint256 i = 0; i < nftContracts.length; ++i) { mixins/NFTMarketReserveAuction.sol:274: for (uint256 i = 0; i < listedAuctionIds.length; ++i) {
In Solidity 0.8+, there's a default overflow check on unsigned integers. It's possible to uncheck this in for-loops and save a significant amount of gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.
Instances include:
mixins/OZ/ERC165Checker.sol:64: for (uint256 i = 0; i < interfaceIds.length; ++i) { mixins/OZ/ERC165Checker.sol:90: for (uint256 i = 0; i < interfaceIds.length; ++i) { mixins/NFTMarketCreators.sol:94: for (uint256 i = 0; i < _recipients.length; ++i) { mixins/NFTMarketCreators.sol:155: for (uint256 i = 0; i < _recipients.length; ++i) { mixins/NFTMarketCreators.sol:193: for (uint256 i = 0; i < _recipients.length; ++i) { mixins/NFTMarketOffer.sol:161: for (uint256 i = 0; i < nftContracts.length; ++i) { mixins/NFTMarketReserveAuction.sol:274: for (uint256 i = 0; i < listedAuctionIds.length; ++i) { FETH.sol:552: for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) { FETH.sol:679: for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) { FETH.sol:713: for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) { FETH.sol:733: for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) { FETH.sol:760: for (uint256 escrowIndex = accountInfo.lockupStartIndex; ; ++escrowIndex) {
The code would go from:
for (uint256 i; i < numIterations; ++i) { // ... }
to:
for (uint256 i; i < numIterations;) { // ... unchecked { ++i; } }
The risk of overflow is inexistant for a uint256
here.
#0 - HardlyDifficult
2022-03-10T13:08:29Z
This was a very detailed and helpful gas report! I love that you commented the code with the @audit tags and detailed exactly how much savings could be made and why. This was very useful when evaluating the recommendations.
We have implemented many of the suggestions here. Some we choose not to change in order to preserve readability. Generally the savings is inline with the expected values you have stated here.
#1 - alcueca
2022-03-17T15:34:12Z
Great report. Score: 4300 (including +20% from formatting)