diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index ef2ec2235..b2dc780ad 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -504,6 +504,59 @@ contract ERC721A is IERC721A { _afterTokenTransfers(address(0), to, startTokenId, quantity); } + /** + * @dev Mints `quantity` tokens and transfers them to `to`. + * + * This function is intended for efficient minting during contract creation. + * + * It emits only one {ConsecutiveTransfer} as defined in + * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309), + * instead of one or more {Transfer} events. + * + * Calling this function outside of contract creation WILL break the ERC721 standard. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `quantity` must be greater than 0. + * + * Emits a {ConsecutiveTransfer} event. + */ + function _mintERC2309(address to, uint256 quantity) internal { + uint256 startTokenId = _currentIndex; + if (_addressToUint256(to) == 0) revert MintToZeroAddress(); + if (quantity == 0) revert MintZeroQuantity(); + + _beforeTokenTransfers(address(0), to, startTokenId, quantity); + + // Overflows are incredibly unrealistic. + // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 + // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 + unchecked { + // Updates: + // - `balance += quantity`. + // - `numberMinted += quantity`. + // + // We can directly add to the balance and number minted. + _packedAddressData[to] += quantity * ((1 << BITPOS_NUMBER_MINTED) | 1); + + // Updates: + // - `address` to the owner. + // - `startTimestamp` to the timestamp of minting. + // - `burned` to `false`. + // - `nextInitialized` to `quantity == 1`. + _packedOwnerships[startTokenId] = + _addressToUint256(to) | + (block.timestamp << BITPOS_START_TIMESTAMP) | + (_boolToUint256(quantity == 1) << BITPOS_NEXT_INITIALIZED); + + emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to); + + _currentIndex = startTokenId + quantity; + } + _afterTokenTransfers(address(0), to, startTokenId, quantity); + } + /** * @dev Transfers `tokenId` from `from` to `to`. * diff --git a/contracts/IERC721A.sol b/contracts/IERC721A.sol index 6d0be7231..f60800709 100644 --- a/contracts/IERC721A.sol +++ b/contracts/IERC721A.sol @@ -252,4 +252,13 @@ interface IERC721A { * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token. */ function tokenURI(uint256 tokenId) external view returns (string memory); + + // ============================== + // IERC2309 + // ============================== + + /** + * @dev Emitted when tokens in `fromTokenId` to `toTokenId` (inclusive) is transferred from `from` to `to. + */ + event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to); } diff --git a/contracts/mocks/ERC721AWithERC2309Mock.sol b/contracts/mocks/ERC721AWithERC2309Mock.sol new file mode 100644 index 000000000..fde2df221 --- /dev/null +++ b/contracts/mocks/ERC721AWithERC2309Mock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// ERC721A Contracts v4.0.0 +// Creators: Chiru Labs + +pragma solidity ^0.8.4; + +import '../ERC721A.sol'; + +/** + * @dev Mock for testing and benchmarking purposes. + * Calling `_mintERC2309` outside of contract creation breaks the ERC721 standard. + */ +contract ERC721AWithERC2309Mock is ERC721A { + constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} + + function mintOneERC2309(address to) public { + _mintERC2309(to, 1); + } + + function mintTenERC2309(address to) public { + _mintERC2309(to, 10); + } +} \ No newline at end of file diff --git a/test/ERC721A.test.js b/test/ERC721A.test.js index 10a261792..aa9e82de7 100644 --- a/test/ERC721A.test.js +++ b/test/ERC721A.test.js @@ -680,3 +680,27 @@ describe( 'ERC721A override _startTokenId()', createTestSuite({ contract: 'ERC721AStartTokenIdMock', constructorArgs: ['Azuki', 'AZUKI', 1] }) ); + +describe('ERC721A with ERC2309', async function () { + beforeEach(async function () { + this.erc721a = await deployContract('ERC721AWithERC2309Mock', ['Azuki', 'AZUKI']); + const [owner, addr1] = await ethers.getSigners(); + this.owner = owner; + this.addr1 = addr1; + this.testEmit = async (methodName, fromTokenId, toTokenId) => { + await expect(await this.erc721a[methodName](this.addr1.address)) + .to.emit(this.erc721a, 'ConsecutiveTransfer') + .withArgs(fromTokenId, toTokenId, ZERO_ADDRESS, this.addr1.address); + }; + }); + + it('emits a ConsecutiveTransfer event for single mint', async function () { + await this.testEmit('mintOneERC2309', 0, 0); + await this.testEmit('mintOneERC2309', 1, 1); + }); + + it('emits a ConsecutiveTransfer event for batch mint', async function () { + await this.testEmit('mintTenERC2309', 0, 9); + await this.testEmit('mintTenERC2309', 10, 19); + }); +}); diff --git a/test/GasUsage.test.js b/test/GasUsage.test.js index 50eeda415..295a158d2 100644 --- a/test/GasUsage.test.js +++ b/test/GasUsage.test.js @@ -40,6 +40,20 @@ describe('ERC721A Gas Usage', function () { }); }); + it('mintOneERC2309', async function () { + let contract = await deployContract('ERC721AWithERC2309Mock', ['Azuki', 'AZUKI']); + await contract.mintOneERC2309(this.owner.address); + await contract.mintOneERC2309(this.owner.address); + await contract.mintOneERC2309(this.addr1.address); + }); + + it('mintTenERC2309', async function () { + let contract = await deployContract('ERC721AWithERC2309Mock', ['Azuki', 'AZUKI']); + await contract.mintTenERC2309(this.owner.address); + await contract.mintTenERC2309(this.owner.address); + await contract.mintTenERC2309(this.addr1.address); + }); + context('transferFrom', function () { beforeEach(async function () { await this.erc721a.mintTen(this.owner.address);