ERC721 extension for efficient batch minting (#3311)
Co-authored-by: Francisco <frangio.1@gmail.com>pull/3683/head
parent
005a35b02a
commit
171fa40bc8
@ -0,0 +1,20 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.0; |
||||
|
||||
/** |
||||
* @dev ERC-2309: ERC-721 Consecutive Transfer Extension. |
||||
* |
||||
* _Available since v4.8._ |
||||
*/ |
||||
interface IERC2309 { |
||||
/** |
||||
* @dev Emitted when the tokens from `fromTokenId` to `toTokenId` are transferred from `fromAddress` to `toAddress`. |
||||
*/ |
||||
event ConsecutiveTransfer( |
||||
uint256 indexed fromTokenId, |
||||
uint256 toTokenId, |
||||
address indexed fromAddress, |
||||
address indexed toAddress |
||||
); |
||||
} |
@ -0,0 +1,158 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.0; |
||||
|
||||
import "../token/ERC721/extensions/ERC721Burnable.sol"; |
||||
import "../token/ERC721/extensions/ERC721Consecutive.sol"; |
||||
import "../token/ERC721/extensions/ERC721Enumerable.sol"; |
||||
import "../token/ERC721/extensions/ERC721Pausable.sol"; |
||||
import "../token/ERC721/extensions/draft-ERC721Votes.sol"; |
||||
|
||||
/** |
||||
* @title ERC721ConsecutiveMock |
||||
*/ |
||||
contract ERC721ConsecutiveMock is ERC721Burnable, ERC721Consecutive, ERC721Pausable, ERC721Votes { |
||||
constructor( |
||||
string memory name, |
||||
string memory symbol, |
||||
address[] memory delegates, |
||||
address[] memory receivers, |
||||
uint96[] memory amounts |
||||
) ERC721(name, symbol) EIP712(name, "1") { |
||||
for (uint256 i = 0; i < delegates.length; ++i) { |
||||
_delegate(delegates[i], delegates[i]); |
||||
} |
||||
|
||||
for (uint256 i = 0; i < receivers.length; ++i) { |
||||
_mintConsecutive(receivers[i], amounts[i]); |
||||
} |
||||
} |
||||
|
||||
function pause() external { |
||||
_pause(); |
||||
} |
||||
|
||||
function unpause() external { |
||||
_unpause(); |
||||
} |
||||
|
||||
function exists(uint256 tokenId) public view returns (bool) { |
||||
return _exists(tokenId); |
||||
} |
||||
|
||||
function mint(address to, uint256 tokenId) public { |
||||
_mint(to, tokenId); |
||||
} |
||||
|
||||
function mintConsecutive(address to, uint96 amount) public { |
||||
_mintConsecutive(to, amount); |
||||
} |
||||
|
||||
function safeMint(address to, uint256 tokenId) public { |
||||
_safeMint(to, tokenId); |
||||
} |
||||
|
||||
function _ownerOf(uint256 tokenId) internal view virtual override(ERC721, ERC721Consecutive) returns (address) { |
||||
return super._ownerOf(tokenId); |
||||
} |
||||
|
||||
function _mint(address to, uint256 tokenId) internal virtual override(ERC721, ERC721Consecutive) { |
||||
super._mint(to, tokenId); |
||||
} |
||||
|
||||
function _beforeTokenTransfer( |
||||
address from, |
||||
address to, |
||||
uint256 tokenId |
||||
) internal virtual override(ERC721, ERC721Pausable) { |
||||
super._beforeTokenTransfer(from, to, tokenId); |
||||
} |
||||
|
||||
function _afterTokenTransfer( |
||||
address from, |
||||
address to, |
||||
uint256 tokenId |
||||
) internal virtual override(ERC721, ERC721Votes, ERC721Consecutive) { |
||||
super._afterTokenTransfer(from, to, tokenId); |
||||
} |
||||
|
||||
function _beforeConsecutiveTokenTransfer( |
||||
address from, |
||||
address to, |
||||
uint256 first, |
||||
uint96 size |
||||
) internal virtual override(ERC721, ERC721Pausable) { |
||||
super._beforeConsecutiveTokenTransfer(from, to, first, size); |
||||
} |
||||
|
||||
function _afterConsecutiveTokenTransfer( |
||||
address from, |
||||
address to, |
||||
uint256 first, |
||||
uint96 size |
||||
) internal virtual override(ERC721, ERC721Votes) { |
||||
super._afterConsecutiveTokenTransfer(from, to, first, size); |
||||
} |
||||
} |
||||
|
||||
contract ERC721ConsecutiveEnumerableMock is ERC721Consecutive, ERC721Enumerable { |
||||
constructor( |
||||
string memory name, |
||||
string memory symbol, |
||||
address[] memory receivers, |
||||
uint96[] memory amounts |
||||
) ERC721(name, symbol) { |
||||
for (uint256 i = 0; i < receivers.length; ++i) { |
||||
_mintConsecutive(receivers[i], amounts[i]); |
||||
} |
||||
} |
||||
|
||||
function supportsInterface(bytes4 interfaceId) |
||||
public |
||||
view |
||||
virtual |
||||
override(ERC721, ERC721Enumerable) |
||||
returns (bool) |
||||
{ |
||||
return super.supportsInterface(interfaceId); |
||||
} |
||||
|
||||
function _ownerOf(uint256 tokenId) internal view virtual override(ERC721, ERC721Consecutive) returns (address) { |
||||
return super._ownerOf(tokenId); |
||||
} |
||||
|
||||
function _mint(address to, uint256 tokenId) internal virtual override(ERC721, ERC721Consecutive) { |
||||
super._mint(to, tokenId); |
||||
} |
||||
|
||||
function _beforeTokenTransfer( |
||||
address from, |
||||
address to, |
||||
uint256 tokenId |
||||
) internal virtual override(ERC721, ERC721Enumerable) { |
||||
super._beforeTokenTransfer(from, to, tokenId); |
||||
} |
||||
|
||||
function _afterTokenTransfer( |
||||
address from, |
||||
address to, |
||||
uint256 tokenId |
||||
) internal virtual override(ERC721, ERC721Consecutive) { |
||||
super._afterTokenTransfer(from, to, tokenId); |
||||
} |
||||
|
||||
function _beforeConsecutiveTokenTransfer( |
||||
address from, |
||||
address to, |
||||
uint256 first, |
||||
uint96 size |
||||
) internal virtual override(ERC721, ERC721Enumerable) { |
||||
super._beforeConsecutiveTokenTransfer(from, to, first, size); |
||||
} |
||||
} |
||||
|
||||
contract ERC721ConsecutiveNoConstructorMintMock is ERC721Consecutive { |
||||
constructor(string memory name, string memory symbol) ERC721(name, symbol) { |
||||
_mint(msg.sender, 0); |
||||
} |
||||
} |
@ -0,0 +1,123 @@ |
||||
// SPDX-License-Identifier: MIT |
||||
|
||||
pragma solidity ^0.8.0; |
||||
|
||||
import "../ERC721.sol"; |
||||
import "../../../interfaces/IERC2309.sol"; |
||||
import "../../../utils/Checkpoints.sol"; |
||||
import "../../../utils/math/SafeCast.sol"; |
||||
import "../../../utils/structs/BitMaps.sol"; |
||||
|
||||
/** |
||||
* @dev Implementation of the ERC2309 "Consecutive Transfer Extension" as defined in |
||||
* https://eips.ethereum.org/EIPS/eip-2309[EIP-2309]. |
||||
* |
||||
* This extension allows the minting of large batches of tokens during the contract construction. These batches are |
||||
* limited to 5000 tokens at a time to accommodate off-chain indexers. |
||||
* |
||||
* Using this extension removes the ability to mint single tokens during the contract construction. This ability is |
||||
* regained after construction. During construction, only batch minting is allowed. |
||||
* |
||||
* IMPORTANT: This extension bypasses the hooks {_beforeTokenTransfer} and {_afterTokenTransfer} for tokens minted in |
||||
* batch. When using this extension, you should consider the {_beforeConsecutiveTokenTransfer} and |
||||
* {_afterConsecutiveTokenTransfer} hooks in addition to {_beforeTokenTransfer} and {_afterTokenTransfer}. |
||||
* |
||||
* IMPORTANT: When overriding {_afterTokenTransfer}, be careful about call ordering. {ownerOf} may return invalid |
||||
* values during the {_afterTokenTransfer} execution if the super call is not called first. To be safe, execute the |
||||
* super call before your custom logic. |
||||
* |
||||
* _Available since v4.8._ |
||||
*/ |
||||
abstract contract ERC721Consecutive is IERC2309, ERC721 { |
||||
using BitMaps for BitMaps.BitMap; |
||||
using Checkpoints for Checkpoints.Trace160; |
||||
|
||||
Checkpoints.Trace160 private _sequentialOwnership; |
||||
BitMaps.BitMap private _sequentialBurn; |
||||
|
||||
/** |
||||
* @dev See {ERC721-_ownerOf}. Override that checks the sequential ownership structure for tokens that have |
||||
* been minted as part of a batch, and not yet transferred. |
||||
*/ |
||||
function _ownerOf(uint256 tokenId) internal view virtual override returns (address) { |
||||
address owner = super._ownerOf(tokenId); |
||||
|
||||
// If token is owned by the core, or beyond consecutive range, return base value |
||||
if (owner != address(0) || tokenId > type(uint96).max) { |
||||
return owner; |
||||
} |
||||
|
||||
// Otherwise, check the token was not burned, and fetch ownership from the anchors |
||||
// Note: no need for safe cast, we know that tokenId <= type(uint96).max |
||||
return _sequentialBurn.get(tokenId) ? address(0) : address(_sequentialOwnership.lowerLookup(uint96(tokenId))); |
||||
} |
||||
|
||||
/** |
||||
* @dev Mint a batch of tokens of length `batchSize` for `to`. |
||||
* |
||||
* WARNING: Consecutive mint is only available during construction. ERC721 requires that any minting done after |
||||
* construction emits a `Transfer` event, which is not the case of mints performed using this function. |
||||
* |
||||
* WARNING: Consecutive mint is limited to batches of 5000 tokens. Further minting is possible from a contract's |
||||
* point of view but would cause indexing issues for off-chain services. |
||||
* |
||||
* Emits a {ConsecutiveTransfer} event. |
||||
*/ |
||||
function _mintConsecutive(address to, uint96 batchSize) internal virtual returns (uint96) { |
||||
uint96 first = _totalConsecutiveSupply(); |
||||
|
||||
// minting a batch of size 0 is a no-op |
||||
if (batchSize > 0) { |
||||
require(!Address.isContract(address(this)), "ERC721Consecutive: batch minting restricted to constructor"); |
||||
require(to != address(0), "ERC721Consecutive: mint to the zero address"); |
||||
require(batchSize <= 5000, "ERC721Consecutive: batch too large"); |
||||
|
||||
// hook before |
||||
_beforeConsecutiveTokenTransfer(address(0), to, first, batchSize); |
||||
|
||||
// push an ownership checkpoint & emit event |
||||
uint96 last = first + batchSize - 1; |
||||
_sequentialOwnership.push(last, uint160(to)); |
||||
emit ConsecutiveTransfer(first, last, address(0), to); |
||||
|
||||
// hook after |
||||
_afterConsecutiveTokenTransfer(address(0), to, first, batchSize); |
||||
} |
||||
|
||||
return first; |
||||
} |
||||
|
||||
/** |
||||
* @dev See {ERC721-_mint}. Override version that restricts normal minting to after construction. |
||||
* |
||||
* Warning: Using {ERC721Consecutive} prevents using {_mint} during construction in favor of {_mintConsecutive}. |
||||
* After construction, {_mintConsecutive} is no longer available and {_mint} becomes available. |
||||
*/ |
||||
function _mint(address to, uint256 tokenId) internal virtual override { |
||||
require(Address.isContract(address(this)), "ERC721Consecutive: can't mint during construction"); |
||||
super._mint(to, tokenId); |
||||
} |
||||
|
||||
/** |
||||
* @dev See {ERC721-_afterTokenTransfer}. Burning of token that have been sequentially minted must be explicit. |
||||
*/ |
||||
function _afterTokenTransfer( |
||||
address from, |
||||
address to, |
||||
uint256 tokenId |
||||
) internal virtual override { |
||||
if ( |
||||
to == address(0) && // if we burn |
||||
tokenId <= _totalConsecutiveSupply() && // and the tokenId was minted is a batch |
||||
!_sequentialBurn.get(tokenId) // and the token was never marked as burnt |
||||
) { |
||||
_sequentialBurn.set(tokenId); |
||||
} |
||||
super._afterTokenTransfer(from, to, tokenId); |
||||
} |
||||
|
||||
function _totalConsecutiveSupply() private view returns (uint96) { |
||||
(bool exists, uint96 latestId, ) = _sequentialOwnership.latestCheckpoint(); |
||||
return exists ? latestId + 1 : 0; |
||||
} |
||||
} |
@ -0,0 +1,191 @@ |
||||
const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); |
||||
const { expect } = require('chai'); |
||||
|
||||
const ERC721ConsecutiveMock = artifacts.require('ERC721ConsecutiveMock'); |
||||
const ERC721ConsecutiveEnumerableMock = artifacts.require('ERC721ConsecutiveEnumerableMock'); |
||||
const ERC721ConsecutiveNoConstructorMintMock = artifacts.require('ERC721ConsecutiveNoConstructorMintMock'); |
||||
|
||||
contract('ERC721Consecutive', function (accounts) { |
||||
const [ user1, user2, user3, receiver ] = accounts; |
||||
|
||||
const name = 'Non Fungible Token'; |
||||
const symbol = 'NFT'; |
||||
const batches = [ |
||||
{ receiver: user1, amount: 0 }, |
||||
{ receiver: user1, amount: 3 }, |
||||
{ receiver: user2, amount: 5 }, |
||||
{ receiver: user3, amount: 0 }, |
||||
{ receiver: user1, amount: 7 }, |
||||
]; |
||||
const delegates = [ user1, user3 ]; |
||||
|
||||
describe('with valid batches', function () { |
||||
beforeEach(async function () { |
||||
this.token = await ERC721ConsecutiveMock.new( |
||||
name, |
||||
symbol, |
||||
delegates, |
||||
batches.map(({ receiver }) => receiver), |
||||
batches.map(({ amount }) => amount), |
||||
); |
||||
}); |
||||
|
||||
describe('minting during construction', function () { |
||||
it('events are emitted at construction', async function () { |
||||
let first = 0; |
||||
|
||||
for (const batch of batches) { |
||||
if (batch.amount > 0) { |
||||
await expectEvent.inConstruction(this.token, 'ConsecutiveTransfer', { |
||||
fromTokenId: web3.utils.toBN(first), |
||||
toTokenId: web3.utils.toBN(first + batch.amount - 1), |
||||
fromAddress: constants.ZERO_ADDRESS, |
||||
toAddress: batch.receiver, |
||||
}); |
||||
} else { |
||||
// expectEvent.notEmitted.inConstruction only looks at event name, and doesn't check the parameters
|
||||
} |
||||
first += batch.amount; |
||||
} |
||||
}); |
||||
|
||||
it('ownership is set', async function () { |
||||
const owners = batches.flatMap(({ receiver, amount }) => Array(amount).fill(receiver)); |
||||
|
||||
for (const tokenId in owners) { |
||||
expect(await this.token.ownerOf(tokenId)) |
||||
.to.be.equal(owners[tokenId]); |
||||
} |
||||
}); |
||||
|
||||
it('balance & voting power are set', async function () { |
||||
for (const account of accounts) { |
||||
const balance = batches |
||||
.filter(({ receiver }) => receiver === account) |
||||
.map(({ amount }) => amount) |
||||
.reduce((a, b) => a + b, 0); |
||||
|
||||
expect(await this.token.balanceOf(account)) |
||||
.to.be.bignumber.equal(web3.utils.toBN(balance)); |
||||
|
||||
// If not delegated at construction, check before + do delegation
|
||||
if (!delegates.includes(account)) { |
||||
expect(await this.token.getVotes(account)) |
||||
.to.be.bignumber.equal(web3.utils.toBN(0)); |
||||
|
||||
await this.token.delegate(account, { from: account }); |
||||
} |
||||
|
||||
// At this point all accounts should have delegated
|
||||
expect(await this.token.getVotes(account)) |
||||
.to.be.bignumber.equal(web3.utils.toBN(balance)); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
describe('minting after construction', function () { |
||||
it('consecutive minting is not possible after construction', async function () { |
||||
await expectRevert( |
||||
this.token.mintConsecutive(user1, 10), |
||||
'ERC721Consecutive: batch minting restricted to constructor', |
||||
); |
||||
}); |
||||
|
||||
it('simple minting is possible after construction', async function () { |
||||
const tokenId = batches.reduce((acc, { amount }) => acc + amount, 0); |
||||
|
||||
expect(await this.token.exists(tokenId)).to.be.equal(false); |
||||
|
||||
expectEvent( |
||||
await this.token.mint(user1, tokenId), |
||||
'Transfer', |
||||
{ from: constants.ZERO_ADDRESS, to: user1, tokenId: tokenId.toString() }, |
||||
); |
||||
}); |
||||
|
||||
it('cannot mint a token that has been batched minted', async function () { |
||||
const tokenId = batches.reduce((acc, { amount }) => acc + amount, 0) - 1; |
||||
|
||||
expect(await this.token.exists(tokenId)).to.be.equal(true); |
||||
|
||||
await expectRevert( |
||||
this.token.mint(user1, tokenId), |
||||
'ERC721: token already minted', |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('ERC721 behavior', function () { |
||||
it('core takes over ownership on transfer', async function () { |
||||
await this.token.transferFrom(user1, receiver, 1, { from: user1 }); |
||||
|
||||
expect(await this.token.ownerOf(1)).to.be.equal(receiver); |
||||
}); |
||||
|
||||
it('tokens can be burned and re-minted', async function () { |
||||
expectEvent( |
||||
await this.token.burn(1, { from: user1 }), |
||||
'Transfer', |
||||
{ from: user1, to: constants.ZERO_ADDRESS, tokenId: '1' }, |
||||
); |
||||
|
||||
await expectRevert(this.token.ownerOf(1), 'ERC721: invalid token ID'); |
||||
|
||||
expectEvent( |
||||
await this.token.mint(user2, 1), |
||||
'Transfer', |
||||
{ from: constants.ZERO_ADDRESS, to: user2, tokenId: '1' }, |
||||
); |
||||
|
||||
expect(await this.token.ownerOf(1)).to.be.equal(user2); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('invalid use', function () { |
||||
it('cannot mint a batch larger than 5000', async function () { |
||||
await expectRevert( |
||||
ERC721ConsecutiveMock.new( |
||||
name, |
||||
symbol, |
||||
[], |
||||
[user1], |
||||
['5001'], |
||||
), |
||||
'ERC721Consecutive: batch too large', |
||||
); |
||||
}); |
||||
|
||||
it('cannot use single minting during construction', async function () { |
||||
await expectRevert( |
||||
ERC721ConsecutiveNoConstructorMintMock.new( |
||||
name, |
||||
symbol, |
||||
), |
||||
'ERC721Consecutive: can\'t mint during construction', |
||||
); |
||||
}); |
||||
|
||||
it('cannot use single minting during construction', async function () { |
||||
await expectRevert( |
||||
ERC721ConsecutiveNoConstructorMintMock.new( |
||||
name, |
||||
symbol, |
||||
), |
||||
'ERC721Consecutive: can\'t mint during construction', |
||||
); |
||||
}); |
||||
|
||||
it('consecutive mint not compatible with enumerability', async function () { |
||||
await expectRevert( |
||||
ERC721ConsecutiveEnumerableMock.new( |
||||
name, |
||||
symbol, |
||||
batches.map(({ receiver }) => receiver), |
||||
batches.map(({ amount }) => amount), |
||||
), |
||||
'ERC721Enumerable: consecutive transfers not supported', |
||||
); |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue