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 ERC-2309 support for mints during contract creation #311

Merged
merged 14 commits into from
Jun 14, 2022
65 changes: 65 additions & 0 deletions contracts/ERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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`.
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
* 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 {
Expand Down Expand Up @@ -504,6 +510,65 @@ 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.
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
*
* 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.
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
* 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.
* - `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.
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
// 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`.
*
Expand Down
10 changes: 10 additions & 0 deletions contracts/IERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,14 @@ 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`,
* as defined in the ERC2309 standard. See `_mintERC2309` for more details.
*/
event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to);
}
29 changes: 29 additions & 0 deletions contracts/mocks/ERC721AWithERC2309Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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.
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
* 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_) {}

function mintOneERC2309(address to) public {
_mintERC2309(to, 1);
}

function mintTenERC2309(address to) public {
_mintERC2309(to, 10);
}

function mintERC2309(address to, uint256 quantity) public {
_mintERC2309(to, quantity);
}
}
32 changes: 32 additions & 0 deletions test/ERC721A.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -680,3 +680,35 @@ 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);
});

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');
});
});
14 changes: 14 additions & 0 deletions test/GasUsage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
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);
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
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);
Expand Down