Skip to content

Commit

Permalink
Add ERC721ABatchTransferable and ERC721ABatchBurnable (#486)
Browse files Browse the repository at this point in the history
* Add `batchTransferFrom` and `ERC721ABatchTransferable` extension (#458)

* 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

* refactor _batchTransfer logic

* optimized and stacked not too deep

* optimize: removed unneeded exists() via getApproved

* removed unneeded functions and batchBurn

* Tidy, optimize

* Tidy

* Add test

* Tidy

* Tidy

* Tidy, optimize

* Combine batch burnable and transferable

* Edit comment

* Tidy tests

* Tidy

* Edit comment

* Remove unnecessary internal functions, max out test coverage

---------

Co-authored-by: jacopo <39241410+jjranalli@users.noreply.github.com>
  • Loading branch information
Vectorized and jjranalli authored Sep 9, 2024
1 parent c5bd8e1 commit 338e327
Show file tree
Hide file tree
Showing 16 changed files with 1,530 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
}
}
]
}
}
381 changes: 325 additions & 56 deletions contracts/ERC721A.sol

Large diffs are not rendered by default.

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(_msgSenderERC721A(), tokenIds);
}
}
40 changes: 40 additions & 0 deletions contracts/extensions/ERC721ABatchTransferable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.3
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import '../ERC721A.sol';
import './IERC721ABatchTransferable.sol';

/**
* @title ERC721ABatchTransferable.
*
* @dev ERC721A token optimized for batch transfers.
*/
abstract contract ERC721ABatchTransferable is ERC721A, IERC721ABatchTransferable {
function batchTransferFrom(
address from,
address to,
uint256[] memory tokenIds
) public payable virtual override {
_batchTransferFrom(_msgSenderERC721A(), from, to, tokenIds);
}

function safeBatchTransferFrom(
address from,
address to,
uint256[] memory tokenIds
) public payable virtual override {
_safeBatchTransferFrom(_msgSenderERC721A(), from, to, tokenIds, '');
}

function safeBatchTransferFrom(
address from,
address to,
uint256[] memory tokenIds,
bytes memory _data
) public payable virtual override {
_safeBatchTransferFrom(_msgSenderERC721A(), from, to, tokenIds, _data);
}
}
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;
}
62 changes: 62 additions & 0 deletions contracts/extensions/IERC721ABatchTransferable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.3
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import '../IERC721A.sol';

/**
* @dev Interface of ERC721ABatchTransferable.
*/
interface IERC721ABatchTransferable is IERC721A {
/**
* @dev Transfers `tokenIds` in batch from `from` to `to`. See {ERC721A-_batchTransferFrom}.
*
* Requirements:
*
* - `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
* by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event for each transfer.
*/
function batchTransferFrom(
address from,
address to,
uint256[] memory tokenIds
) external payable;

/**
* @dev Equivalent to `safeBatchTransferFrom(from, to, tokenIds, '')`.
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory tokenIds
) external payable;

/**
* @dev Safely transfers `tokenIds` in batch from `from` to `to`. See {ERC721A-_safeBatchTransferFrom}.
*
* Requirements:
*
* - `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
* by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement
* {IERC721Receiver-onERC721Received}, which is called for each transferred token.
*
* Emits a {Transfer} event for each transfer.
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory tokenIds,
bytes memory _data
) external payable;
}
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';
7 changes: 7 additions & 0 deletions contracts/interfaces/IERC721ABatchTransferable.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/IERC721ABatchTransferable.sol';
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 '../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);
}
}
64 changes: 64 additions & 0 deletions contracts/mocks/ERC721ABatchTransferableMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.3
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import '../extensions/ERC721ABatchTransferable.sol';

contract ERC721ABatchTransferableMock is ERC721ABatchTransferable {
constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {}

function safeMint(address to, uint256 quantity) public {
_safeMint(to, quantity);
}

function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownershipAt(index);
}

function initializeOwnershipAt(uint256 index) public {
_initializeOwnershipAt(index);
}

function _extraData(
address,
address,
uint24 previousExtraData
) internal view virtual override returns (uint24) {
return previousExtraData;
}

function setExtraDataAt(uint256 index, uint24 extraData) public {
_setExtraDataAt(index, extraData);
}

function batchTransferFromUnoptimized(
address from,
address to,
uint256[] memory tokenIds
) public {
unchecked {
for (uint256 i; i != tokenIds.length; ++i) {
transferFrom(from, to, tokenIds[i]);
}
}
}

function directBatchTransferFrom(
address by,
address from,
address to,
uint256[] memory tokenIds
) public {
_batchTransferFrom(by, from, to, tokenIds);
}

function directBatchTransferFrom(
address from,
address to,
uint256[] memory tokenIds
) public {
_batchTransferFrom(from, to, tokenIds);
}
}
26 changes: 26 additions & 0 deletions contracts/mocks/ERC721AGasReporterMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions test/GasUsage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ describe('ERC721A Gas Usage', function () {
});
});

context('mintHundred', function () {
it('runs mintHundred 2 times', async function () {
for (let i = 0; i < 2; i++) {
await this.erc721a.mintHundred(this.addr1.address);
}
});
});

context('safeMintHundred', function () {
it('runs safeMintHundred 2 times', async function () {
for (let i = 0; i < 2; i++) {
await this.erc721a.safeMintHundred(this.addr1.address);
}
});
});

context('transferFrom', function () {
beforeEach(async function () {
await this.erc721a.mintTen(this.owner.address);
Expand Down Expand Up @@ -67,6 +83,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 () {
Expand Down
Loading

0 comments on commit 338e327

Please sign in to comment.