diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index c0230ef45..f86e0b00f 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -1030,6 +1030,107 @@ contract ERC721A is IERC721A { } } + /** + * @dev Destroys `tokenIds`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenIds` must exist. + * - `tokenIds` must be strictly ascending. + * - `burner` must be the owner or approved to burn each of the token. + * + * Emits a {Transfer} event for each token burned. + */ + function _batchBurn(address burner, uint256[] memory tokenIds, bool approvalCheck) internal virtual { + // We can use unchecked as the length of `tokenIds` is bounded + // to a small number by the max block gas limit. + unchecked { + // The next `tokenId` to be minted (i.e. `_nextTokenId()`). + uint256 stop = _currentIndex; + + uint256 n = tokenIds.length; + + // For checking if the `tokenIds` are strictly ascending. + uint256 prevTokenId; + + for (uint256 i; i != n; ) { + uint256 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(); + + // The initialized packed ownership slot's value. + uint256 prevOwnershipPacked; + // 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; + + // If the initialized slot is burned, revert. + if (prevOwnershipPacked & _BITMASK_BURNED != 0) revert OwnerQueryForNonexistentToken(); + + // Unpack the `tokenOwner` from bits [0..159] of `prevOwnershipPacked`. + address tokenOwner = address(uint160(prevOwnershipPacked)); + + // Check if the burner is either the owner or an approved operator for all the + bool mayBurn = !approvalCheck || tokenOwner == burner || isApprovedForAll(tokenOwner, burner); + + uint256 offset; + uint256 currTokenId = tokenId; + do { + // Revert if the burner is not authorized to burn the token. + if (!mayBurn) + if (getApproved(currTokenId) != burner) revert TransferCallerNotOwnerNorApproved(); + // Call the hook. + _beforeTokenTransfers(tokenOwner, address(0), currTokenId, 1); + // Emit the `Transfer` event for burn. + emit Transfer(tokenOwner, address(0), currTokenId); + // Call the hook. + _afterTokenTransfers(tokenOwner, address(0), 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. + _packedOwnerships[currTokenId] == 0 + ); + + // Update the packed ownership for `tokenId` in ERC721A's storage. + _packedOwnerships[tokenId] = + _BITMASK_BURNED | + (block.timestamp << _BITPOS_START_TIMESTAMP) | + uint256(uint160(tokenOwner)); + + // If the slot after the mini batch is neither out of bounds, nor initialized. + if (currTokenId != stop) + if (_packedOwnerships[currTokenId] == 0) + _packedOwnerships[currTokenId] = prevOwnershipPacked; + + // Update the address data in ERC721A's storage. + // + // Note that this update has to be in the loop as tokens + // can be burned by an operator that is not the token owner. + _packedAddressData[tokenOwner] += (offset << _BITPOS_NUMBER_BURNED) - offset; + + // Advance `i` by `offset`, the number of tokens burned in the mini batch. + i += offset; + + // Set the `prevTokenId` for checking that the `tokenIds` is strictly ascending. + prevTokenId = currTokenId - 1; + } + // Increase the `_burnCounter` in ERC721A's storage. + _burnCounter += n; + } + } + // ============================================================= // EXTRA DATA OPERATIONS // ============================================================= @@ -1146,4 +1247,13 @@ contract ERC721A is IERC721A { revert(0x00, 0x04) } } + + /** + * @dev Branchless or. + */ + function _or(bool a, bool b) private pure returns (bool c) { + assembly { + c := or(a, b) + } + } } diff --git a/contracts/IERC721A.sol b/contracts/IERC721A.sol index 4bd87db45..a1aef23b3 100644 --- a/contracts/IERC721A.sol +++ b/contracts/IERC721A.sol @@ -74,6 +74,11 @@ interface IERC721A { */ error OwnershipNotInitializedForExtraData(); + /** + * The `tokenIds` must be strictly ascending. + */ + error TokenIdsNotStrictlyAscending(); + // ============================================================= // STRUCTS // ============================================================= diff --git a/contracts/mocks/ERC721ABatchBurnableMock.sol b/contracts/mocks/ERC721ABatchBurnableMock.sol new file mode 100644 index 000000000..d7d26061a --- /dev/null +++ b/contracts/mocks/ERC721ABatchBurnableMock.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.2.3 +// Creators: Chiru Labs + +pragma solidity ^0.8.4; + +import '../ERC721A.sol'; +import './DirectBurnBitSetterHelper.sol'; + +contract ERC721ABatchBurnableMock is ERC721A, DirectBurnBitSetterHelper { + constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} + + function exists(uint256 tokenId) public view returns (bool) { + return _exists(tokenId); + } + + function safeMint(address to, uint256 quantity) public { + _safeMint(to, quantity); + } + + function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) { + return _ownershipAt(index); + } + + function totalMinted() public view returns (uint256) { + return _totalMinted(); + } + + function totalBurned() public view returns (uint256) { + return _totalBurned(); + } + + function numberBurned(address owner) public view returns (uint256) { + return _numberBurned(owner); + } + + function bulkBurn(address burner, uint256[] memory tokenIds, bool approvalCheck) public { + _batchBurn(burner, tokenIds, approvalCheck); + } +}