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
63 changes: 63 additions & 0 deletions contracts/ERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ contract ERC721A is IERC721A {

// The mask of the lower 160 bits for addresses.
uint256 private constant BITMASK_ADDRESS = (1 << 160) - 1;

// 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;
Expand Down Expand Up @@ -443,6 +449,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 @@ -516,6 +524,61 @@ 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 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 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.
*
* 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 (to == address(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 unrealistic due to the above check for `quantity` to be below the limit.
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] = _packOwnershipData(
to,
(_boolToUint256(quantity == 1) << BITPOS_NEXT_INITIALIZED) | _nextExtraData(address(0), to, 0)
);

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
15 changes: 15 additions & 0 deletions contracts/IERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -254,4 +259,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);
}
39 changes: 39 additions & 0 deletions contracts/mocks/ERC721AWithERC2309Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.0.0
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import '../ERC721A.sol';

contract ERC721AWithERC2309Mock is ERC721A {
constructor(
string memory name_,
string memory symbol_,
address to,
uint256 quantity,
bool mintInConstructor
) ERC721A(name_, symbol_) {
if (mintInConstructor) {
_mintERC2309(to, quantity);
}
}

/**
* @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 comparison purposes.
* Calling `_mintERC3201` outside of contract creation is non-compliant
* with the ERC721 standard.
*/
function mintTenERC2309(address to) public {
_mintERC2309(to, 10);
}
}
45 changes: 45 additions & 0 deletions test/ERC721A.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,51 @@ describe(
createTestSuite({ contract: 'ERC721AStartTokenIdMock', constructorArgs: ['Azuki', 'AZUKI', 1] })
);

describe('ERC721A with ERC2309', async function () {
beforeEach(async function () {
const [owner, addr1] = await ethers.getSigners();
this.owner = owner;
this.addr1 = addr1;

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 () {
expect(this.erc721aMint1.deployTransaction)
.to.emit(this.erc721aMint1, 'ConsecutiveTransfer')
.withArgs(0, 0, ZERO_ADDRESS, this.owner.address);
});

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 () {
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 () {
let args = ['Azuki', 'AZUKI', this.owner.address, 0, true];
await expect(deployContract('ERC721AWithERC2309Mock', args)).to.be.revertedWith('MintZeroQuantity');
});
});

describe(
'ERC721A override _extraData()',
createTestSuite({ contract: 'ERC721ATransferCounterMock', constructorArgs: ['Azuki', 'AZUKI'] })
Expand Down
22 changes: 22 additions & 0 deletions test/GasUsage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,26 @@ describe('ERC721A Gas Usage', function () {
await this.erc721a.connect(this.owner).transferTenAvg(this.addr1.address);
});
});

it('mintOneERC2309', async function () {
// 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);
await contract.mintOneERC2309(this.owner.address);
await contract.mintOneERC2309(this.addr1.address);
});

it('mintTenERC2309', async function () {
// 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);
await contract.mintTenERC2309(this.owner.address);
await contract.mintTenERC2309(this.addr1.address);
});
});