From 7b3d048a5b352919f2b93d6700e6e87c92df0f7b Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 21 Aug 2024 01:50:29 +0000 Subject: [PATCH] Tidy, optimize --- contracts/ERC721A.sol | 310 +++++++++--------- .../extensions/ERC721ABatchTransferable.sol | 6 +- contracts/mocks/ERC721AGasReporterMock.sol | 26 ++ package-lock.json | 2 +- test/GasUsage.test.js | 18 + 5 files changed, 194 insertions(+), 168 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index f4341b9e7..8e001b714 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -378,7 +378,7 @@ contract ERC721A is IERC721A { * @dev Initializes the ownership slot minted at `index` for efficiency purposes. */ function _initializeOwnershipAt(uint256 index) internal virtual { - if (_packedOwnerships[index] == 0) { + if (_packedOwnerships[index] == uint256(0)) { _packedOwnerships[index] = _packedOwnershipOf(index); } } @@ -396,7 +396,7 @@ contract ERC721A is IERC721A { } // If the data at the starting slot does not exist, start the scan. - if (packed == 0) { + if (packed == uint256(0)) { if (tokenId >= _currentIndex) _revert(OwnerQueryForNonexistentToken.selector); // Invariant: // There will always be an initialized ownership slot @@ -411,8 +411,8 @@ contract ERC721A is IERC721A { unchecked { packed = _packedOwnerships[--tokenId]; } - if (packed == 0) continue; - if (packed & _BITMASK_BURNED == 0) return packed; + if (packed == uint256(0)) continue; + if (packed & _BITMASK_BURNED == uint256(0)) return packed; // Otherwise, the token is burned, and we must revert. // This handles the case of batch burned tokens, where only the burned bit // of the starting slot is set, and remaining slots are left uninitialized. @@ -423,7 +423,7 @@ contract ERC721A is IERC721A { // This is possible because we have already achieved the target condition. // This saves 2143 gas on transfers of initialized tokens. // If the token is not burned, return `packed`. Otherwise, revert. - if (packed & _BITMASK_BURNED == 0) return packed; + if (packed & _BITMASK_BURNED == uint256(0)) return packed; } _revert(OwnerQueryForNonexistentToken.selector); } @@ -527,8 +527,8 @@ contract ERC721A is IERC721A { if (tokenId < _currentIndex) { uint256 packed; - while ((packed = _packedOwnerships[tokenId]) == 0) --tokenId; - result = packed & _BITMASK_BURNED == 0; + while ((packed = _packedOwnerships[tokenId]) == uint256(0)) --tokenId; + result = packed & _BITMASK_BURNED == uint256(0); } } } @@ -548,33 +548,28 @@ contract ERC721A is IERC721A { * @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`. */ function _isSenderApprovedOrOwner( - address approvedAddress, - address owner, - address msgSender + uint256 approvedAddressValue, + uint256 ownerMasked, + uint256 msgSenderMasked ) private pure returns (bool result) { assembly { - // Mask `owner` to the lower 160 bits, in case the upper bits somehow aren't clean. - owner := and(owner, _BITMASK_ADDRESS) - // Mask `msgSender` to the lower 160 bits, in case the upper bits somehow aren't clean. - msgSender := and(msgSender, _BITMASK_ADDRESS) - // `msgSender == owner || msgSender == approvedAddress`. - result := or(eq(msgSender, owner), eq(msgSender, approvedAddress)) + result := or(eq(msgSenderMasked, ownerMasked), eq(msgSenderMasked, approvedAddressValue)) } } /** - * @dev Returns the storage slot and value for the approved address of `tokenId`. + * @dev Returns the storage slot and value for the approved address of `tokenId` casted to a uint256. */ - function _getApprovedSlotAndAddress(uint256 tokenId) + function _getApprovedSlotAndValue(uint256 tokenId) private view - returns (uint256 approvedAddressSlot, address approvedAddress) + returns (uint256 approvedAddressSlot, uint256 approvedAddressValue) { TokenApprovalRef storage tokenApproval = _tokenApprovals[tokenId]; - // The following is equivalent to `approvedAddress = _tokenApprovals[tokenId].value`. + // The following is equivalent to `approvedAddressValue = uint160(_tokenApprovals[tokenId].value)`. assembly { approvedAddressSlot := tokenApproval.slot - approvedAddress := sload(approvedAddressSlot) + approvedAddressValue := sload(approvedAddressSlot) } } @@ -601,25 +596,21 @@ contract ERC721A is IERC721A { uint256 tokenId ) public payable virtual override { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); + uint256 fromMasked = uint160(from); - // Mask `from` to the lower 160 bits, in case the upper bits somehow aren't clean. - from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS)); + if (uint160(prevOwnershipPacked) != fromMasked) _revert(TransferFromIncorrectOwner.selector); - if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); - - (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); + (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); // The nested ifs save around 20+ gas over a compound boolean condition. - if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) + if (!_isSenderApprovedOrOwner(approvedAddressValue, fromMasked, uint160(_msgSenderERC721A()))) if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); _beforeTokenTransfers(from, to, tokenId, 1); - // Clear approvals from the previous owner. assembly { - if approvedAddress { - // This is equivalent to `delete _tokenApprovals[tokenId]`. - sstore(approvedAddressSlot, 0) + if approvedAddressValue { + sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. } } @@ -642,10 +633,10 @@ contract ERC721A is IERC721A { ); // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . - if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { + if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == uint256(0)) { uint256 nextTokenId = tokenId + 1; // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (_packedOwnerships[nextTokenId] == 0) { + if (_packedOwnerships[nextTokenId] == uint256(0)) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. @@ -655,20 +646,20 @@ contract ERC721A is IERC721A { } } - // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. - uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; + // Mask to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint160(to); assembly { // Emit the `Transfer` event. log4( 0, // Start of data (0, since no data). 0, // End of data (0, since no data). _TRANSFER_EVENT_SIGNATURE, // Signature. - from, // `from`. + fromMasked, // `from`. toMasked, // `to`. tokenId // `tokenId`. ) } - if (toMasked == 0) _revert(TransferToZeroAddress.selector); + if (toMasked == uint256(0)) _revert(TransferToZeroAddress.selector); _afterTokenTransfers(from, to, tokenId, 1); } @@ -713,14 +704,14 @@ contract ERC721A is IERC721A { } /** - * @dev Equivalent to `_batchTransferFrom(from, to, tokenIds, false)`. + * @dev Equivalent to `_batchTransferFrom(from, to, tokenIds)`. */ function _batchTransferFrom( address from, address to, uint256[] memory tokenIds ) internal virtual { - _batchTransferFrom(from, to, tokenIds, false); + _batchTransferFrom(address(0), from, to, tokenIds); } /** @@ -732,141 +723,122 @@ contract ERC721A is IERC721A { * - `to` cannot be the zero address. * - `tokenIds` tokens must be owned by `from`. * - `tokenIds` must be strictly ascending. - * - If the caller is not `from`, it must be approved to move these tokens + * - If `by` is not `from`, it must be approved to move these tokens * by either {approve} or {setApprovalForAll}. * + * `by` is the address that to check token approval for. + * If token approval check is not needed, pass in `address(0)` for `by`. + * * Emits a {Transfer} event for each transfer. */ function _batchTransferFrom( + address by, address from, address to, - uint256[] memory tokenIds, - bool approvalCheck + uint256[] memory tokenIds ) internal virtual { - // We can use unchecked as the length of `tokenIds` is bounded - // to a small number by the max block gas limit. + uint256 fromMasked = uint160(from); + uint256 toMasked = uint160(to); + uint256 byMasked = uint160(by); + // Disallow transfer to zero address. + if (toMasked == uint256(0)) _revert(TransferToZeroAddress.selector); + // Early return if `tokenIds` is empty. + if (tokenIds.length == uint256(0)) return; + // Whether we need to check the individual token approvals. + bool approvalCheck = byMasked != uint256(0) && byMasked != fromMasked && !isApprovedForAll(from, by); + // The next `tokenId` to be minted (i.e. `_nextTokenId()`). + uint256 end = _currentIndex; + // Pointer to start and end (exclusive) of `tokenIds`. + (uint256 i, uint256 e) = _mdata(tokenIds); + // Ensure that `tokenIds` is strictly ascending, and perform the before hooks before any state changes. unchecked { - // Mask `from` and `to` to the lower 160 bits, in case the upper bits somehow aren't clean. - from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS)); - if (uint256(uint160(to)) & _BITMASK_ADDRESS == 0) revert TransferToZeroAddress(); - - // Disable `approvalCheck` if sender is either the owner or an approved operator for all tokens - approvalCheck = from != _msgSenderERC721A() && !isApprovedForAll(from, _msgSenderERC721A()); - + uint256 tokenId = _mload(i); // For checking if the `tokenIds` are strictly ascending. + // Revert if the minimum of the `tokenIds` is out of bounds. + // This is equivalent to `tokenId < _startTokenId() || end <= tokenId`. + if (end - _startTokenId() <= tokenId - _startTokenId()) _revert(OwnerQueryForNonexistentToken.selector); + _beforeTokenTransfers(from, to, tokenId, 1); // Perform the before hook. uint256 n = tokenIds.length; - + if (n >= 2) { + uint256 j = i + 0x20; + do { + uint256 next = _mload(j); + if (next <= tokenId) _revert(TokenIdsNotStrictlyAscending.selector); + _beforeTokenTransfers(from, to, tokenId = next, 1); // Perform the before hook. + } while ((j += 0x20) != e); + // Revert if the maximum of the `tokenIds` is out of bounds. + if (end <= tokenId) _revert(OwnerQueryForNonexistentToken.selector); + } // Increment and decrement the balances. _packedAddressData[from] -= n; _packedAddressData[to] += n; - - // The next `tokenId` to be minted (i.e. `_nextTokenId()`). - uint256 stop = _currentIndex; - - // For checking if the `tokenIds` are strictly ascending. - uint256 prevTokenId; - - uint256 tokenId; - uint256 currTokenId; - uint256 prevOwnershipPacked; - uint256 lastOwnershipPacked; - for (uint256 i; i != n; ) { - tokenId = tokenIds[i]; - - // Revert `tokenId` is out of bounds. - if (_or(tokenId < _startTokenId(), stop <= tokenId)) revert OwnerQueryForNonexistentToken(); - - // Revert if `tokenIds` is not strictly ascending. - if (i != 0) - if (tokenId <= prevTokenId) revert TokenIdsNotStrictlyAscending(); - + } + uint256 prevOwnershipPacked; + do { + uint256 tokenId = _mload(i); + unchecked { // Scan backwards for an initialized packed ownership slot. // ERC721A's invariant guarantees that there will always be an initialized slot as long as // the start of the backwards scan falls within `[_startTokenId() .. _nextTokenId())`. - for (uint256 j = tokenId; (prevOwnershipPacked = _packedOwnerships[j]) == 0; ) --j; - + for (uint256 j = tokenId; (prevOwnershipPacked = _packedOwnerships[j]) == uint256(0); ) --j; // If the initialized slot is burned, revert. - if (prevOwnershipPacked & _BITMASK_BURNED != 0) revert OwnerQueryForNonexistentToken(); - - // Check ownership of `tokenId`. - if (address(uint160(prevOwnershipPacked)) != from) revert TransferFromIncorrectOwner(); - - currTokenId = tokenId; - uint256 offset; - do { - address approvedAddress = _tokenApprovals[currTokenId].value; - - // Revert if the sender is not authorized to transfer the token. - if (approvalCheck) { - if (_msgSenderERC721A() != approvedAddress) revert TransferCallerNotOwnerNorApproved(); - } - - // Call the hook. - _beforeTokenTransfers(from, to, currTokenId, 1); - - if (approvedAddress != address(0)) delete _tokenApprovals[currTokenId]; - - // Emit the `Transfer` event. - emit Transfer(from, to, currTokenId); - // Call the hook. - _afterTokenTransfers(from, to, currTokenId, 1); - // Increment `offset` and update `currTokenId`. - currTokenId = tokenId + (++offset); - } while ( - // Neither out of bounds, nor at the end of `tokenIds`. - !_or(currTokenId == stop, i + offset == n) && - // Token ID is sequential. - tokenIds[i + offset] == currTokenId && - // The packed ownership slot is not initialized. - (lastOwnershipPacked = _packedOwnerships[currTokenId]) == 0 - ); - + if (prevOwnershipPacked & _BITMASK_BURNED != 0) _revert(OwnerQueryForNonexistentToken.selector); + // Check that `tokenId` is owned by `from`. + if (uint160(prevOwnershipPacked) != fromMasked) _revert(TransferFromIncorrectOwner.selector); // Updates tokenId: // - `address` to the next owner. - // - `startTimestamp` to the timestamp of transfering. + // - `startTimestamp` to the timestamp of transferring. // - `burned` to `false`. // - `nextInitialized` to `false`. _packedOwnerships[tokenId] = _packOwnershipData(to, _nextExtraData(from, to, prevOwnershipPacked)); - - // If the slot after the mini batch is neither out of bounds, nor initialized. - // If `lastOwnershipPacked == 0` we didn't break the loop due to an initialized slot. - if (currTokenId != stop) - if (lastOwnershipPacked == 0) - if (_packedOwnerships[currTokenId] == 0) _packedOwnerships[currTokenId] = prevOwnershipPacked; - - // Advance `i` by `offset`, the number of tokens transferred in the mini batch. - i += offset; - - // Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending. - prevTokenId = currTokenId - 1; + do { + (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); + // Revert if the sender is not authorized to transfer the token. + if (approvalCheck) + if (byMasked != approvedAddressValue) _revert(TransferCallerNotOwnerNorApproved.selector); + assembly { + if approvedAddressValue { + sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. + } + // Emit the `Transfer` event. + log4(0, 0, _TRANSFER_EVENT_SIGNATURE, fromMasked, toMasked, tokenId) + } + _afterTokenTransfers(from, to, tokenId, 1); // Perform the after hook. + if (_mload(i += 0x20) != ++tokenId) break; // Break if `tokenId` is not sequential. + if (i == e) break; // Break if at the end of `tokenIds`. + // Break if the packed ownership slot is initialized. + } while (_packedOwnerships[tokenId] == uint256(0)); } - } + // If the slot after the mini batch is neither out of bounds, nor initialized. + if (tokenId != end) + if (_packedOwnerships[tokenId] == uint256(0)) _packedOwnerships[tokenId] = prevOwnershipPacked; + } while (i != e); } /** - * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, false)`. + * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds)`. */ function _safeBatchTransferFrom( address from, address to, uint256[] memory tokenIds ) internal virtual { - _safeBatchTransferFrom(from, to, tokenIds, false); + _safeBatchTransferFrom(address(0), from, to, tokenIds); } /** - * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, '', approvalCheck)`. + * @dev Equivalent to `_safeBatchTransferFrom(by, from, to, tokenIds, '')`. */ function _safeBatchTransferFrom( + address by, address from, address to, - uint256[] memory tokenIds, - bool approvalCheck + uint256[] memory tokenIds ) internal virtual { - _safeBatchTransferFrom(from, to, tokenIds, '', approvalCheck); + _safeBatchTransferFrom(by, from, to, tokenIds, ''); } /** - * @dev Equivalent to `_safeBatchTransferFrom(from, to, tokenIds, _data, false)`. + * @dev Equivalent to `_safeBatchTransferFrom(address(0), from, to, tokenIds, _data)`. */ function _safeBatchTransferFrom( address from, @@ -874,7 +846,7 @@ contract ERC721A is IERC721A { uint256[] memory tokenIds, bytes memory _data ) internal virtual { - _safeBatchTransferFrom(from, to, tokenIds, _data, false); + _safeBatchTransferFrom(address(0), from, to, tokenIds, _data); } /** @@ -885,31 +857,32 @@ contract ERC721A is IERC721A { * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenIds` tokens must be owned by `from`. - * - If the caller is not `from`, it must be approved to move these tokens + * - If `by` is not `from`, it must be approved to move these tokens * by either {approve} or {setApprovalForAll}. * - If `to` refers to a smart contract, it must implement * {IERC721Receiver-onERC721Received}, which is called for each transferred token. * + * `by` is the address that to check token approval for. + * If token approval check is not needed, pass in `address(0)` for `by`. + * * Emits a {Transfer} event for each transfer. */ function _safeBatchTransferFrom( + address by, address from, address to, uint256[] memory tokenIds, - bytes memory _data, - bool approvalCheck + bytes memory _data ) internal virtual { - _batchTransferFrom(from, to, tokenIds, approvalCheck); + _batchTransferFrom(by, from, to, tokenIds); - uint256 tokenId; - uint256 n = tokenIds.length; unchecked { - for (uint256 i; i < n; ++i) { - tokenId = tokenIds[i]; - if (to.code.length != 0) - if (!_checkContractOnERC721Received(from, to, tokenId, _data)) { + if (to.code.length != 0) { + for ((uint256 i, uint256 e) = _mdata(tokenIds); i != e; i += 0x20) { + if (!_checkContractOnERC721Received(from, to, _mload(i), _data)) { _revert(TransferToNonERC721ReceiverImplementer.selector); } + } } } } @@ -981,7 +954,7 @@ contract ERC721A is IERC721A { ) { return retval == ERC721A__IERC721Receiver(to).onERC721Received.selector; } catch (bytes memory reason) { - if (reason.length == 0) { + if (reason.length == uint256(0)) { _revert(TransferToNonERC721ReceiverImplementer.selector); } assembly { @@ -1006,7 +979,7 @@ contract ERC721A is IERC721A { */ function _mint(address to, uint256 quantity) internal virtual { uint256 startTokenId = _currentIndex; - if (quantity == 0) _revert(MintZeroQuantity.selector); + if (quantity == uint256(0)) _revert(MintZeroQuantity.selector); _beforeTokenTransfers(address(0), to, startTokenId, quantity); @@ -1031,10 +1004,10 @@ contract ERC721A is IERC721A { // We can directly add to the `balance` and `numberMinted`. _packedAddressData[to] += quantity * ((1 << _BITPOS_NUMBER_MINTED) | 1); - // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. - uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; + // Mask to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint160(to); - if (toMasked == 0) _revert(MintToZeroAddress.selector); + if (toMasked == uint256(0)) _revert(MintToZeroAddress.selector); uint256 end = startTokenId + quantity; uint256 tokenId = startTokenId; @@ -1086,7 +1059,7 @@ contract ERC721A is IERC721A { function _mintERC2309(address to, uint256 quantity) internal virtual { uint256 startTokenId = _currentIndex; if (to == address(0)) _revert(MintToZeroAddress.selector); - if (quantity == 0) _revert(MintZeroQuantity.selector); + if (quantity == uint256(0)) _revert(MintZeroQuantity.selector); if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) _revert(MintERC2309QuantityExceedsLimit.selector); _beforeTokenTransfers(address(0), to, startTokenId, quantity); @@ -1203,10 +1176,10 @@ contract ERC721A is IERC721A { // We can directly add to the `balance` and `numberMinted`. _packedAddressData[to] += (1 << _BITPOS_NUMBER_MINTED) | 1; - // Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean. - uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; + // Mask to the lower 160 bits, in case the upper bits somehow aren't clean. + uint256 toMasked = uint160(to); - if (toMasked == 0) _revert(MintToZeroAddress.selector); + if (toMasked == uint256(0)) _revert(MintToZeroAddress.selector); assembly { // Emit the `Transfer` event. @@ -1332,23 +1305,22 @@ contract ERC721A is IERC721A { function _burn(uint256 tokenId, bool approvalCheck) internal virtual { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); - address from = address(uint160(prevOwnershipPacked)); + uint256 fromMasked = uint160(prevOwnershipPacked); + address from = address(uint160(fromMasked)); - (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); + (uint256 approvedAddressSlot, uint256 approvedAddressValue) = _getApprovedSlotAndValue(tokenId); if (approvalCheck) { // The nested ifs save around 20+ gas over a compound boolean condition. - if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) + if (!_isSenderApprovedOrOwner(approvedAddressValue, fromMasked, uint160(_msgSenderERC721A()))) if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); } _beforeTokenTransfers(from, address(0), tokenId, 1); - // Clear approvals from the previous owner. assembly { - if approvedAddress { - // This is equivalent to `delete _tokenApprovals[tokenId]`. - sstore(approvedAddressSlot, 0) + if approvedAddressValue { + sstore(approvedAddressSlot, 0) // Equivalent to `delete _tokenApprovals[tokenId]`. } } @@ -1375,10 +1347,10 @@ contract ERC721A is IERC721A { ); // If the next slot may not have been initialized (i.e. `nextInitialized == false`) . - if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { + if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == uint256(0)) { uint256 nextTokenId = tokenId + 1; // If the next slot's address is zero and not burned (i.e. packed value is zero). - if (_packedOwnerships[nextTokenId] == 0) { + if (_packedOwnerships[nextTokenId] == uint256(0)) { // If the next slot is within bounds. if (nextTokenId != _currentIndex) { // Initialize the next slot to maintain correctness for `ownerOf(tokenId + 1)`. @@ -1406,7 +1378,7 @@ contract ERC721A is IERC721A { */ function _setExtraDataAt(uint256 index, uint24 extraData) internal virtual { uint256 packed = _packedOwnerships[index]; - if (packed == 0) _revert(OwnershipNotInitializedForExtraData.selector); + if (packed == uint256(0)) _revert(OwnershipNotInitializedForExtraData.selector); uint256 extraDataCasted; // Cast `extraData` with assembly to avoid redundant masking. assembly { @@ -1515,11 +1487,21 @@ contract ERC721A is IERC721A { } /** - * @dev Branchless or. + * @dev Returns a memory pointer to the start of `a`'s data. + */ + function _mdata(uint256[] memory a) private pure returns (uint256 start, uint256 end) { + assembly { + start := add(a, 0x20) + end := add(start, shl(5, mload(a))) + } + } + + /** + * @dev Returns the uint256 at `p` in memory. */ - function _or(bool a, bool b) private pure returns (bool c) { + function _mload(uint256 p) private pure returns (uint256 result) { assembly { - c := or(a, b) + result := mload(p) } } } diff --git a/contracts/extensions/ERC721ABatchTransferable.sol b/contracts/extensions/ERC721ABatchTransferable.sol index d4caa4a0a..b1d66227b 100644 --- a/contracts/extensions/ERC721ABatchTransferable.sol +++ b/contracts/extensions/ERC721ABatchTransferable.sol @@ -18,7 +18,7 @@ abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable address to, uint256[] memory tokenIds ) public payable virtual override { - _batchTransferFrom(from, to, tokenIds, true); + _batchTransferFrom(_msgSenderERC721A(), from, to, tokenIds); } function safeBatchTransferFrom( @@ -26,7 +26,7 @@ abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable address to, uint256[] memory tokenIds ) public payable virtual override { - _safeBatchTransferFrom(from, to, tokenIds, true); + _safeBatchTransferFrom(_msgSenderERC721A(), from, to, tokenIds, ''); } function safeBatchTransferFrom( @@ -35,6 +35,6 @@ abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable uint256[] memory tokenIds, bytes memory _data ) public payable virtual override { - _safeBatchTransferFrom(from, to, tokenIds, _data, true); + _safeBatchTransferFrom(_msgSenderERC721A(), from, to, tokenIds, _data); } } diff --git a/contracts/mocks/ERC721AGasReporterMock.sol b/contracts/mocks/ERC721AGasReporterMock.sol index 847a7773d..a5eecac52 100644 --- a/contracts/mocks/ERC721AGasReporterMock.sol +++ b/contracts/mocks/ERC721AGasReporterMock.sol @@ -25,6 +25,14 @@ contract ERC721AGasReporterMock is ERC721A { _mint(to, 10); } + function safeMintHundred(address to) public { + _safeMint(to, 100); + } + + function mintHundred(address to) public { + _mint(to, 100); + } + function transferTenAsc(address to) public { unchecked { transferFrom(msg.sender, to, 0); @@ -69,4 +77,22 @@ contract ERC721AGasReporterMock is ERC721A { transferFrom(msg.sender, to, 9); } } + + function batchTransferHundredUnoptimized(address to) public { + unchecked { + for (uint256 i; i != 100; ++i) { + transferFrom(msg.sender, to, i); + } + } + } + + function batchTransferHundredOptimized(address to) public { + unchecked { + uint256[] memory tokenIds = new uint256[](100); + for (uint256 i; i != 100; ++i) { + tokenIds[i] = i; + } + _batchTransferFrom(msg.sender, msg.sender, to, tokenIds); + } + } } diff --git a/package-lock.json b/package-lock.json index 9ef9f6f98..bde6c1457 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48430,4 +48430,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/GasUsage.test.js b/test/GasUsage.test.js index 380ff9583..7b6e435c2 100644 --- a/test/GasUsage.test.js +++ b/test/GasUsage.test.js @@ -67,6 +67,24 @@ describe('ERC721A Gas Usage', function () { it('transferTen average order', async function () { await this.erc721a.connect(this.owner).transferTenAvg(this.addr1.address); }); + + it('transferTen average order', async function () { + await this.erc721a.connect(this.owner).transferTenAvg(this.addr1.address); + }); + }); + + context('batchTransferFromHundred', function () { + beforeEach(async function () { + await this.erc721a.mintHundred(this.owner.address); + }); + + it('batchTransferFromHundred unoptimized', async function () { + await this.erc721a.connect(this.owner).batchTransferHundredUnoptimized(this.addr1.address); + }); + + it('batchTransferFromHundred optimized', async function () { + await this.erc721a.connect(this.owner).batchTransferHundredOptimized(this.addr1.address); + }); }); it('mintOneERC2309', async function () {