From 6aa02db3fb1ebbc0f2f97ed3be1006912a489e6e Mon Sep 17 00:00:00 2001 From: Vectorized Date: Tue, 7 Jun 2022 20:52:27 +0000 Subject: [PATCH 01/13] Add ERC2309 mint function and tests --- contracts/ERC721A.sol | 53 ++++++++++++++++++++++ contracts/IERC721A.sol | 9 ++++ contracts/mocks/ERC721AWithERC2309Mock.sol | 23 ++++++++++ test/ERC721A.test.js | 24 ++++++++++ test/GasUsage.test.js | 14 ++++++ 5 files changed, 123 insertions(+) create mode 100644 contracts/mocks/ERC721AWithERC2309Mock.sol 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); From 1b0f7491f01c24497174edd0eb89072facfb7971 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Tue, 7 Jun 2022 22:02:00 +0000 Subject: [PATCH 02/13] Add tests for full coverage --- contracts/mocks/ERC721AWithERC2309Mock.sol | 4 ++++ test/ERC721A.test.js | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/contracts/mocks/ERC721AWithERC2309Mock.sol b/contracts/mocks/ERC721AWithERC2309Mock.sol index fde2df221..d130a8ea2 100644 --- a/contracts/mocks/ERC721AWithERC2309Mock.sol +++ b/contracts/mocks/ERC721AWithERC2309Mock.sol @@ -20,4 +20,8 @@ contract ERC721AWithERC2309Mock is ERC721A { function mintTenERC2309(address to) public { _mintERC2309(to, 10); } + + function mintERC2309(address to, uint256 quantity) public { + _mintERC2309(to, quantity); + } } \ No newline at end of file diff --git a/test/ERC721A.test.js b/test/ERC721A.test.js index aa9e82de7..928a7b0bf 100644 --- a/test/ERC721A.test.js +++ b/test/ERC721A.test.js @@ -703,4 +703,12 @@ describe('ERC721A with ERC2309', async function () { await this.testEmit('mintTenERC2309', 0, 9); await this.testEmit('mintTenERC2309', 10, 19); }); + + it('rejects mints to the zero address', async function () { + await expect(this.erc721a.mintERC2309(ZERO_ADDRESS, 1)).to.be.revertedWith('MintToZeroAddress'); + }); + + it('requires quantity to be greater than 0', async function () { + await expect(this.erc721a.mintERC2309(this.owner.address, 0)).to.be.revertedWith('MintZeroQuantity'); + }); }); From 0d98abb1fb0796d5ffb0dc65e09f797636a29dc9 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 8 Jun 2022 00:36:21 +0000 Subject: [PATCH 03/13] Lint --- contracts/ERC721A.sol | 2 +- contracts/mocks/ERC721AWithERC2309Mock.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index b2dc780ad..565bf1fcc 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -509,7 +509,7 @@ contract ERC721A is IERC721A { * * This function is intended for efficient minting during contract creation. * - * It emits only one {ConsecutiveTransfer} as defined in + * It emits only one {ConsecutiveTransfer} as defined in * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309), * instead of one or more {Transfer} events. * diff --git a/contracts/mocks/ERC721AWithERC2309Mock.sol b/contracts/mocks/ERC721AWithERC2309Mock.sol index d130a8ea2..3e3f8572a 100644 --- a/contracts/mocks/ERC721AWithERC2309Mock.sol +++ b/contracts/mocks/ERC721AWithERC2309Mock.sol @@ -7,7 +7,7 @@ pragma solidity ^0.8.4; import '../ERC721A.sol'; /** - * @dev Mock for testing and benchmarking purposes. + * @dev Mock for testing and benchmarking purposes. * Calling `_mintERC2309` outside of contract creation breaks the ERC721 standard. */ contract ERC721AWithERC2309Mock is ERC721A { @@ -24,4 +24,4 @@ contract ERC721AWithERC2309Mock is ERC721A { function mintERC2309(address to, uint256 quantity) public { _mintERC2309(to, quantity); } -} \ No newline at end of file +} From d3434d47f0356a46207797923f8ff64c41f24f3e Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 8 Jun 2022 01:07:00 +0000 Subject: [PATCH 04/13] Edit comments --- contracts/ERC721A.sol | 4 +++- contracts/mocks/ERC721AWithERC2309Mock.sol | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 565bf1fcc..fb70494bb 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -511,9 +511,11 @@ contract ERC721A is IERC721A { * * It emits only one {ConsecutiveTransfer} as defined in * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309), - * instead of one or more {Transfer} events. + * instead of a sequence of {Transfer} events. * * Calling this function outside of contract creation WILL break the ERC721 standard. + * For full ERC721 compliance, substituting ERC721 {Transfer} event(s) with the ERC2309 + * {ConsecutiveTransfer} event is only permissible during contract creation. * * Requirements: * diff --git a/contracts/mocks/ERC721AWithERC2309Mock.sol b/contracts/mocks/ERC721AWithERC2309Mock.sol index 3e3f8572a..5646ddfa8 100644 --- a/contracts/mocks/ERC721AWithERC2309Mock.sol +++ b/contracts/mocks/ERC721AWithERC2309Mock.sol @@ -9,6 +9,8 @@ import '../ERC721A.sol'; /** * @dev Mock for testing and benchmarking purposes. * Calling `_mintERC2309` outside of contract creation breaks the ERC721 standard. + * The mock exposes the function for simplicity of testing and gas comparisons with + * the other mint functions. */ contract ERC721AWithERC2309Mock is ERC721A { constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} From b9e6d078d87d93a593df60d2d2716d7690e8e652 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 8 Jun 2022 01:10:36 +0000 Subject: [PATCH 05/13] Edit comments --- contracts/ERC721A.sol | 2 +- contracts/IERC721A.sol | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index fb70494bb..ad13f1b1c 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -511,7 +511,7 @@ contract ERC721A is IERC721A { * * It emits only one {ConsecutiveTransfer} as defined in * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309), - * instead of a sequence of {Transfer} events. + * instead of a sequence of {Transfer} event(s). * * Calling this function outside of contract creation WILL break the ERC721 standard. * For full ERC721 compliance, substituting ERC721 {Transfer} event(s) with the ERC2309 diff --git a/contracts/IERC721A.sol b/contracts/IERC721A.sol index f60800709..d779b03fa 100644 --- a/contracts/IERC721A.sol +++ b/contracts/IERC721A.sol @@ -258,7 +258,8 @@ interface IERC721A { // ============================== /** - * @dev Emitted when tokens in `fromTokenId` to `toTokenId` (inclusive) is transferred from `from` to `to. + * @dev Emitted when tokens in `fromTokenId` to `toTokenId` (inclusive) is transferred from `from` to `to`, + * as defined in the ERC2309 standard. See `_mintERC2309` for more details. */ event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to); } From f1bf9070450c10ee812e87dd77a5c53b1b39f3a8 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 9 Jun 2022 23:21:09 +0000 Subject: [PATCH 06/13] Add more comments --- contracts/ERC721A.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index ad13f1b1c..9a3d73136 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -432,6 +432,8 @@ contract ERC721A is IERC721A { * {IERC721Receiver-onERC721Received}, which is called for each safe transfer. * - `quantity` must be greater than 0. * + * See {_mint}. + * * Emits a {Transfer} event for each mint. */ function _safeMint( @@ -464,6 +466,10 @@ contract ERC721A is IERC721A { * - `to` cannot be the zero address. * - `quantity` must be greater than 0. * + * Note: some marketplaces may have difficulties fully registering a very large `quantity`. + * We have verified full support on OpenSea's and LooksRare's Rinkeby testnet marketplaces + * for `quantity = 5000` on 5th May 2022. Support may vary, and may change over time. + * * Emits a {Transfer} event for each mint. */ function _mint(address to, uint256 quantity) internal { @@ -517,6 +523,10 @@ contract ERC721A is IERC721A { * For full ERC721 compliance, substituting ERC721 {Transfer} event(s) with the ERC2309 * {ConsecutiveTransfer} event is only permissible during contract creation. * + * Note: some marketplaces may have difficulties fully registering a very large `quantity`. + * We have verified full support on OpenSea's and LooksRare's Rinkeby testnet marketplaces + * for `quantity = 5000` on 5th May 2022. Support may vary, and may change over time. + * * Requirements: * * - `to` cannot be the zero address. From acb7cbcd5490810bb9bfaf3c57434b4ff83f3531 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 9 Jun 2022 23:43:31 +0000 Subject: [PATCH 07/13] Edit comments --- contracts/ERC721A.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 9a3d73136..9ff83261d 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -468,7 +468,7 @@ contract ERC721A is IERC721A { * * Note: some marketplaces may have difficulties fully registering a very large `quantity`. * We have verified full support on OpenSea's and LooksRare's Rinkeby testnet marketplaces - * for `quantity = 5000` on 5th May 2022. Support may vary, and may change over time. + * for `quantity = 5000` on 5th June 2022. Support may vary, and may change over time. * * Emits a {Transfer} event for each mint. */ @@ -525,7 +525,7 @@ contract ERC721A is IERC721A { * * Note: some marketplaces may have difficulties fully registering a very large `quantity`. * We have verified full support on OpenSea's and LooksRare's Rinkeby testnet marketplaces - * for `quantity = 5000` on 5th May 2022. Support may vary, and may change over time. + * for `quantity = 5000` on 5th June 2022. Support may vary, and may change over time. * * Requirements: * From cb1cc884e8639d896c26704a12b64f931ba73507 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 13 Jun 2022 02:09:00 +0000 Subject: [PATCH 08/13] Add requested changes --- contracts/ERC721A.sol | 24 ++++++------- contracts/IERC721A.sol | 5 +++ contracts/mocks/ERC721AWithERC2309Mock.sol | 16 ++++++--- test/ERC721A.test.js | 41 ++++++++++++++-------- test/GasUsage.test.js | 6 ++-- 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 9ff83261d..0abee10ff 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -56,6 +56,12 @@ contract ERC721A is IERC721A { // The bit mask of the `nextInitialized` bit in packed ownership. uint256 private constant BITMASK_NEXT_INITIALIZED = 1 << 225; + // The maximum `quantity` that can be minted with `_mintERC2309`. + // This limit is to prevent overflows on the address data entries. + // For a limit of 5000, a total of 3.689e15 calls to `_mintERC2309` + // is required to cause an overflow, which is unrealistic. + uint256 private constant MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000; + // The tokenId of the next token to be minted. uint256 private _currentIndex; @@ -466,10 +472,6 @@ contract ERC721A is IERC721A { * - `to` cannot be the zero address. * - `quantity` must be greater than 0. * - * Note: some marketplaces may have difficulties fully registering a very large `quantity`. - * We have verified full support on OpenSea's and LooksRare's Rinkeby testnet marketplaces - * for `quantity = 5000` on 5th June 2022. Support may vary, and may change over time. - * * Emits a {Transfer} event for each mint. */ function _mint(address to, uint256 quantity) internal { @@ -513,20 +515,17 @@ contract ERC721A is IERC721A { /** * @dev Mints `quantity` tokens and transfers them to `to`. * - * This function is intended for efficient minting during contract creation. + * This function is intended for efficient minting only during contract creation. * * It emits only one {ConsecutiveTransfer} as defined in * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309), * instead of a sequence of {Transfer} event(s). * - * Calling this function outside of contract creation WILL break the ERC721 standard. + * Calling this function outside of contract creation WILL make your contract + * non-compliant with the ERC721 standard. * For full ERC721 compliance, substituting ERC721 {Transfer} event(s) with the ERC2309 * {ConsecutiveTransfer} event is only permissible during contract creation. * - * Note: some marketplaces may have difficulties fully registering a very large `quantity`. - * We have verified full support on OpenSea's and LooksRare's Rinkeby testnet marketplaces - * for `quantity = 5000` on 5th June 2022. Support may vary, and may change over time. - * * Requirements: * * - `to` cannot be the zero address. @@ -538,12 +537,11 @@ contract ERC721A is IERC721A { uint256 startTokenId = _currentIndex; if (_addressToUint256(to) == 0) revert MintToZeroAddress(); if (quantity == 0) revert MintZeroQuantity(); + if (quantity > MAX_MINT_ERC2309_QUANTITY_LIMIT) revert MintERC2309QuantityExceedsLimit(); _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 + // Overflows are unrealistic due to the above check for `quantity` to be below the limit. unchecked { // Updates: // - `balance += quantity`. diff --git a/contracts/IERC721A.sol b/contracts/IERC721A.sol index d779b03fa..143132dcf 100644 --- a/contracts/IERC721A.sol +++ b/contracts/IERC721A.sol @@ -68,6 +68,11 @@ interface IERC721A { */ error URIQueryForNonexistentToken(); + /** + * The `quantity` minted with ERC2309 exceeds the safety limit. + */ + error MintERC2309QuantityExceedsLimit(); + struct TokenOwnership { // The address of the owner. address addr; diff --git a/contracts/mocks/ERC721AWithERC2309Mock.sol b/contracts/mocks/ERC721AWithERC2309Mock.sol index 5646ddfa8..9e5d65aa9 100644 --- a/contracts/mocks/ERC721AWithERC2309Mock.sol +++ b/contracts/mocks/ERC721AWithERC2309Mock.sol @@ -8,12 +8,20 @@ import '../ERC721A.sol'; /** * @dev Mock for testing and benchmarking purposes. - * Calling `_mintERC2309` outside of contract creation breaks the ERC721 standard. - * The mock exposes the function for simplicity of testing and gas comparisons with - * the other mint functions. + * The mock exposes the function for gas benchmarks with other mint functions. */ contract ERC721AWithERC2309Mock is ERC721A { - constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {} + constructor( + string memory name_, + string memory symbol_, + address to, + uint256 quantity, + bool mintInConstructor + ) ERC721A(name_, symbol_) { + if (mintInConstructor) { + _mintERC2309(to, quantity); + } + } function mintOneERC2309(address to) public { _mintERC2309(to, 1); diff --git a/test/ERC721A.test.js b/test/ERC721A.test.js index 928a7b0bf..29c5ddd99 100644 --- a/test/ERC721A.test.js +++ b/test/ERC721A.test.js @@ -683,32 +683,45 @@ describe( 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); - }; + + let args; + args = ['Azuki', 'AZUKI', this.owner.address, 1, true]; + this.erc721aMint1 = await deployContract('ERC721AWithERC2309Mock', args); + args = ['Azuki', 'AZUKI', this.owner.address, 10, true]; + this.erc721aMint10 = await deployContract('ERC721AWithERC2309Mock', args); }); - 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 single mint', async function () { + expect(this.erc721aMint1.deployTransaction) + .to.emit(this.erc721aMint1, 'ConsecutiveTransfer') + .withArgs(0, 0, ZERO_ADDRESS, this.owner.address); }); - it('emits a ConsecutiveTransfer event for batch mint', async function () { - await this.testEmit('mintTenERC2309', 0, 9); - await this.testEmit('mintTenERC2309', 10, 19); + it('emits a ConsecutiveTransfer event for a batch mint', async function () { + expect(this.erc721aMint10.deployTransaction) + .to.emit(this.erc721aMint10, 'ConsecutiveTransfer') + .withArgs(0, 9, ZERO_ADDRESS, this.owner.address); }); + it('requires quantity to be below mint limit', async function () { + let args; + const mintLimit = 5000; + args = ['Azuki', 'AZUKI', this.owner.address, mintLimit, true]; + await deployContract('ERC721AWithERC2309Mock', args); + args = ['Azuki', 'AZUKI', this.owner.address, mintLimit + 1, true]; + await expect(deployContract('ERC721AWithERC2309Mock', args)).to.be.revertedWith('MintERC2309QuantityExceedsLimit'); + }) + it('rejects mints to the zero address', async function () { - await expect(this.erc721a.mintERC2309(ZERO_ADDRESS, 1)).to.be.revertedWith('MintToZeroAddress'); + let args = ['Azuki', 'AZUKI', ZERO_ADDRESS, 1, true]; + await expect(deployContract('ERC721AWithERC2309Mock', args)).to.be.revertedWith('MintToZeroAddress'); }); it('requires quantity to be greater than 0', async function () { - await expect(this.erc721a.mintERC2309(this.owner.address, 0)).to.be.revertedWith('MintZeroQuantity'); + let args = ['Azuki', 'AZUKI', this.owner.address, 0, true]; + await expect(deployContract('ERC721AWithERC2309Mock', args)).to.be.revertedWith('MintZeroQuantity'); }); }); diff --git a/test/GasUsage.test.js b/test/GasUsage.test.js index 295a158d2..7790ffb29 100644 --- a/test/GasUsage.test.js +++ b/test/GasUsage.test.js @@ -41,14 +41,16 @@ describe('ERC721A Gas Usage', function () { }); it('mintOneERC2309', async function () { - let contract = await deployContract('ERC721AWithERC2309Mock', ['Azuki', 'AZUKI']); + let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; + let contract = await deployContract('ERC721AWithERC2309Mock', args); 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']); + let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; + let contract = await deployContract('ERC721AWithERC2309Mock', args); await contract.mintTenERC2309(this.owner.address); await contract.mintTenERC2309(this.owner.address); await contract.mintTenERC2309(this.addr1.address); From e3a7cbd068b0c0c17d56bc023674324c98cb0fa7 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 13 Jun 2022 02:23:31 +0000 Subject: [PATCH 09/13] Remove unused mock function --- contracts/mocks/ERC721AWithERC2309Mock.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/mocks/ERC721AWithERC2309Mock.sol b/contracts/mocks/ERC721AWithERC2309Mock.sol index 9e5d65aa9..46d523843 100644 --- a/contracts/mocks/ERC721AWithERC2309Mock.sol +++ b/contracts/mocks/ERC721AWithERC2309Mock.sol @@ -30,8 +30,4 @@ contract ERC721AWithERC2309Mock is ERC721A { function mintTenERC2309(address to) public { _mintERC2309(to, 10); } - - function mintERC2309(address to, uint256 quantity) public { - _mintERC2309(to, quantity); - } } From 285b7d3281419efb3dcfc79ba5278d6c22a107fa Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 13 Jun 2022 05:50:58 +0000 Subject: [PATCH 10/13] Add comments on non-compliant benchmarks --- contracts/mocks/ERC721AWithERC2309Mock.sol | 12 +++++--- test/GasUsage.test.js | 36 ++++++++++++---------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/contracts/mocks/ERC721AWithERC2309Mock.sol b/contracts/mocks/ERC721AWithERC2309Mock.sol index 46d523843..980118242 100644 --- a/contracts/mocks/ERC721AWithERC2309Mock.sol +++ b/contracts/mocks/ERC721AWithERC2309Mock.sol @@ -6,10 +6,6 @@ pragma solidity ^0.8.4; import '../ERC721A.sol'; -/** - * @dev Mock for testing and benchmarking purposes. - * The mock exposes the function for gas benchmarks with other mint functions. - */ contract ERC721AWithERC2309Mock is ERC721A { constructor( string memory name_, @@ -23,10 +19,18 @@ contract ERC721AWithERC2309Mock is ERC721A { } } + /** + * @dev This function is only for gas benchmarking purposes. + * Calling `_mintERC3201` outside of contract creation is non-compliant with ERC721. + */ function mintOneERC2309(address to) public { _mintERC2309(to, 1); } + /** + * @dev This function is only for gas benchmarking purposes. + * Calling `_mintERC3201` outside of contract creation is non-compliant with ERC721. + */ function mintTenERC2309(address to) public { _mintERC2309(to, 10); } diff --git a/test/GasUsage.test.js b/test/GasUsage.test.js index 7790ffb29..71da3f62e 100644 --- a/test/GasUsage.test.js +++ b/test/GasUsage.test.js @@ -40,22 +40,6 @@ describe('ERC721A Gas Usage', function () { }); }); - it('mintOneERC2309', async function () { - let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; - let contract = await deployContract('ERC721AWithERC2309Mock', args); - await contract.mintOneERC2309(this.owner.address); - await contract.mintOneERC2309(this.owner.address); - await contract.mintOneERC2309(this.addr1.address); - }); - - it('mintTenERC2309', async function () { - let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; - let contract = await deployContract('ERC721AWithERC2309Mock', args); - 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); @@ -84,4 +68,24 @@ describe('ERC721A Gas Usage', function () { await this.erc721a.connect(this.owner).transferTenAvg(this.addr1.address); }); }); + + it('mintOneERC2309', async function () { + // The following call `_mintERC3201` internally outside of contract creation. + // This is non-compliant with ERC721, and is only meant for gas benchmark purposes. + let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; + let contract = await deployContract('ERC721AWithERC2309Mock', args); + await contract.mintOneERC2309(this.owner.address); + await contract.mintOneERC2309(this.owner.address); + await contract.mintOneERC2309(this.addr1.address); + }); + + it('mintTenERC2309', async function () { + // The following call `_mintERC3201` internally outside of contract creation. + // This is non-compliant with ERC721, and is only meant for gas benchmark purposes. + let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; + let contract = await deployContract('ERC721AWithERC2309Mock', args); + await contract.mintTenERC2309(this.owner.address); + await contract.mintTenERC2309(this.owner.address); + await contract.mintTenERC2309(this.addr1.address); + }); }); From 897079968cdc93f98b87cb5ebd8d394f33fb97ab Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 13 Jun 2022 05:52:37 +0000 Subject: [PATCH 11/13] Edit comments --- test/GasUsage.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/GasUsage.test.js b/test/GasUsage.test.js index 71da3f62e..a6637fc47 100644 --- a/test/GasUsage.test.js +++ b/test/GasUsage.test.js @@ -71,7 +71,7 @@ describe('ERC721A Gas Usage', function () { it('mintOneERC2309', async function () { // The following call `_mintERC3201` internally outside of contract creation. - // This is non-compliant with ERC721, and is only meant for gas benchmark purposes. + // This is non-compliant with ERC721, and is only meant for gas comparisons. let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; let contract = await deployContract('ERC721AWithERC2309Mock', args); await contract.mintOneERC2309(this.owner.address); @@ -81,7 +81,7 @@ describe('ERC721A Gas Usage', function () { it('mintTenERC2309', async function () { // The following call `_mintERC3201` internally outside of contract creation. - // This is non-compliant with ERC721, and is only meant for gas benchmark purposes. + // This is non-compliant with ERC721, and is only meant for gas comparisons. let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; let contract = await deployContract('ERC721AWithERC2309Mock', args); await contract.mintTenERC2309(this.owner.address); From 801a482ca65c676242a550f2875bef11121c7ff7 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 13 Jun 2022 05:59:28 +0000 Subject: [PATCH 12/13] Edit comments --- contracts/mocks/ERC721AWithERC2309Mock.sol | 10 ++++++---- test/GasUsage.test.js | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/contracts/mocks/ERC721AWithERC2309Mock.sol b/contracts/mocks/ERC721AWithERC2309Mock.sol index 980118242..802fd9915 100644 --- a/contracts/mocks/ERC721AWithERC2309Mock.sol +++ b/contracts/mocks/ERC721AWithERC2309Mock.sol @@ -20,16 +20,18 @@ contract ERC721AWithERC2309Mock is ERC721A { } /** - * @dev This function is only for gas benchmarking purposes. - * Calling `_mintERC3201` outside of contract creation is non-compliant with ERC721. + * @dev This function is only for gas comparison purposes. + * Calling `_mintERC3201` outside of contract creation is non-compliant + * with the ERC721 standard. */ function mintOneERC2309(address to) public { _mintERC2309(to, 1); } /** - * @dev This function is only for gas benchmarking purposes. - * Calling `_mintERC3201` outside of contract creation is non-compliant with ERC721. + * @dev This function is only for gas comparison purposes. + * Calling `_mintERC3201` outside of contract creation is non-compliant + * with the ERC721 standard. */ function mintTenERC2309(address to) public { _mintERC2309(to, 10); diff --git a/test/GasUsage.test.js b/test/GasUsage.test.js index a6637fc47..380ff9583 100644 --- a/test/GasUsage.test.js +++ b/test/GasUsage.test.js @@ -70,8 +70,9 @@ describe('ERC721A Gas Usage', function () { }); it('mintOneERC2309', async function () { - // The following call `_mintERC3201` internally outside of contract creation. - // This is non-compliant with ERC721, and is only meant for gas comparisons. + // The following call `_mintERC3201` outside of contract creation. + // This is non-compliant with the ERC721 standard, + // and is only meant for gas comparisons. let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; let contract = await deployContract('ERC721AWithERC2309Mock', args); await contract.mintOneERC2309(this.owner.address); @@ -80,8 +81,9 @@ describe('ERC721A Gas Usage', function () { }); it('mintTenERC2309', async function () { - // The following call `_mintERC3201` internally outside of contract creation. - // This is non-compliant with ERC721, and is only meant for gas comparisons. + // The following call `_mintERC3201` outside of contract creation. + // This is non-compliant with the ERC721 standard, + // and is only meant for gas comparisons. let args = ['Azuki', 'AZUKI', this.owner.address, 0, false]; let contract = await deployContract('ERC721AWithERC2309Mock', args); await contract.mintTenERC2309(this.owner.address); From 39d9a82bea135f039f03fdc1a7294e4973b4e90b Mon Sep 17 00:00:00 2001 From: Vectorized Date: Tue, 14 Jun 2022 09:43:20 +0800 Subject: [PATCH 13/13] Update ERC721A.sol --- contracts/ERC721A.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/ERC721A.sol b/contracts/ERC721A.sol index 1f8183a79..1a2cd4556 100644 --- a/contracts/ERC721A.sol +++ b/contracts/ERC721A.sol @@ -547,7 +547,7 @@ contract ERC721A is IERC721A { */ function _mintERC2309(address to, uint256 quantity) internal { uint256 startTokenId = _currentIndex; - if (_addressToUint256(to) == 0) revert MintToZeroAddress(); + if (to == address(0)) revert MintToZeroAddress(); if (quantity == 0) revert MintZeroQuantity(); if (quantity > MAX_MINT_ERC2309_QUANTITY_LIMIT) revert MintERC2309QuantityExceedsLimit(); @@ -567,10 +567,10 @@ contract ERC721A is IERC721A { // - `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); + _packedOwnerships[startTokenId] = _packOwnershipData( + to, + (_boolToUint256(quantity == 1) << BITPOS_NEXT_INITIALIZED) | _nextExtraData(address(0), to, 0) + ); emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to);