Skip to content

Commit

Permalink
Add ERC721ABatchBurnable extension (#444)
Browse files Browse the repository at this point in the history
* added comments on transfer hooks

* added sort

* added clearApprovalsAndEmitTransferEvent

* added tokenBatchTransfer hooks

* added _batchTransferFrom and safe variants

* added ERC721ABatchTransferable extension and interface

* formatting

* added interface and ERC721ABatchTransferableMock

* added ERC721ABatchTransferable tests (wip)

* added approvalCheck

* fixed duplicate call

* comment

* fixed next initialized

* refactored lastInitPackedOwnership to use prevPackedOwnership

* comments

* ensured correctness of nextInitialized in slots of transferred token Ids

* renamed variables

* reverted to leave nextInitialized unchanged

* comment

* replace sort -> insertion sort

* bump: prettier-plugin-solidity

* prettier

* added prettier-ignore

* fixed nextTokenId in last array element

* tests wip

* refactor

* updated BatchTransferable mock and extension

* updated tests

* add approval tests

* lint

* lint fix

* restore original .prettierrc

* fix

* comments and refactor

* added _batchBurn

* added ERC721ABatchBurnable extension, interfaces and mock

* fixed _batchBurn

* fixed update of last tokenId + 1

* batchBurnable tests wip

* refactor

* fix

* add auto-clearing of consecutive ids and set `nextInitialized` to false

* batchTransfer tests refactor

* tests wip

* tests wip

* comments

* added extraData logic to batch mocks

* updated batch tests

* refactored ERC721A to use _updateTokenId

* wip

* comment

* Add ERC721ABatchBurnableMock (#450)

* change tokenIds in ascending order in test

* removal of unneeded internal functions

* prettier

* removed batch transfer logic

* changed _updateTokenId

* fixed mock

* fixed extension and mock

* fixed tests and cleaned unused functions in mock

* removed _updateTokenId

* minor gas optimizations

* comment

* optimize: avoid potential double read from storage

* removed bulkBurn from mock

* optimization: reset _packedOwnerships for initialized sequential IDs

* added tests for sequential ID clearing

* added test for tokenIds in strictly ascending order

* comment

* optimize: keep track of prevTokenOwner to bypass duplicated logic

* revert: resetting _packedOwnerships in initialized sequential IDs

* cleanup

* optimize: avoid potential double read from storage

* optimize: removed unneeded exists() via getApproved
  • Loading branch information
jjranalli authored Aug 21, 2024
1 parent c5bd8e1 commit b9f9485
Show file tree
Hide file tree
Showing 8 changed files with 599 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
}
}
]
}
}
133 changes: 133 additions & 0 deletions contracts/ERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,130 @@ contract ERC721A is IERC721A {
}
}

/**
* @dev Equivalent to `_batchBurn(msg.sender, tokenIds, false)`.
*/
function _batchBurn(uint256[] memory tokenIds) internal virtual {
_batchBurn(_msgSenderERC721A(), tokenIds, false);
}

/**
* @dev Destroys `tokenIds`.
* Approvals are not cleared when tokenIds are 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;

uint256 tokenId;
uint256 currTokenId;
uint256 prevOwnershipPacked;
address prevTokenOwner;
uint256 lastOwnershipPacked;
address tokenOwner;
bool mayBurn;
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();

// 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`.
tokenOwner = address(uint160(prevOwnershipPacked));

if (tokenOwner != prevTokenOwner) {
// Update `prevTokenOwner`.
prevTokenOwner = tokenOwner;

// Check if the burner is either the owner or an approved operator for all tokens
mayBurn = !approvalCheck || tokenOwner == burner || isApprovedForAll(tokenOwner, burner);
}

currTokenId = tokenId;
uint256 offset;
do {
// Revert if the burner is not authorized to burn the token.
if (!mayBurn)
if (_tokenApprovals[currTokenId].value != 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.
(lastOwnershipPacked = _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 (lastOwnershipPacked == 0)
if (_packedOwnerships[currTokenId] == 0)
// If `lastOwnershipPacked == 0` we didn't break the loop due to an initialized slot.
_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 @@ -1311,4 +1435,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();

/**
* `_sequentialUpTo()` must be greater than `_startTokenId()`.
*/
Expand Down
19 changes: 19 additions & 0 deletions contracts/extensions/ERC721ABatchBurnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.3
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import './ERC721ABurnable.sol';
import './IERC721ABatchBurnable.sol';

/**
* @title ERC721ABatchBurnable.
*
* @dev ERC721A token optimized for batch burns.
*/
abstract contract ERC721ABatchBurnable is ERC721ABurnable, IERC721ABatchBurnable {
function batchBurn(uint256[] memory tokenIds) public virtual override {
_batchBurn(msg.sender, tokenIds, true);
}
}
14 changes: 14 additions & 0 deletions contracts/extensions/IERC721ABatchBurnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.3
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import './IERC721ABurnable.sol';

/**
* @dev Interface of ERC721ABatchBurnable.
*/
interface IERC721ABatchBurnable is IERC721ABurnable {
function batchBurn(uint256[] memory tokenIds) external;
}
7 changes: 7 additions & 0 deletions contracts/interfaces/IERC721ABatchBurnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.3
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import '../extensions/IERC721ABatchBurnable.sol';
50 changes: 50 additions & 0 deletions contracts/mocks/ERC721ABatchBurnableMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.3
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import '../extensions/ERC721ABatchBurnable.sol';
import './DirectBurnBitSetterHelper.sol';

contract ERC721ABatchBurnableMock is ERC721ABatchBurnable, 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 initializeOwnershipAt(uint256 index) public {
_initializeOwnershipAt(index);
}

function batchBurnUnoptimized(uint256[] memory tokenIds) public {
unchecked {
uint256 tokenId;
for (uint256 i; i < tokenIds.length; ++i) {
tokenId = tokenIds[i];
_burn(tokenId);
}
}
}
}
Loading

0 comments on commit b9f9485

Please sign in to comment.