Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ERC721ABatchBurnableMock #450

Merged
merged 1 commit into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions contracts/ERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
// =============================================================
Expand Down Expand Up @@ -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)
}
}
}
5 changes: 5 additions & 0 deletions contracts/IERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ interface IERC721A {
*/
error OwnershipNotInitializedForExtraData();

/**
* The `tokenIds` must be strictly ascending.
*/
error TokenIdsNotStrictlyAscending();

// =============================================================
// STRUCTS
// =============================================================
Expand Down
40 changes: 40 additions & 0 deletions contracts/mocks/ERC721ABatchBurnableMock.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}