mirror of openzeppelin-contracts
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Consecutive.sol

195 lines
7.6 KiB

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721Consecutive.sol)
pragma solidity ^0.8.19;
import "../ERC721.sol";
import "../../../interfaces/IERC2309.sol";
import "../../../utils/structs/BitMaps.sol";
import "../../../utils/structs/Checkpoints.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 contract construction only. For upgradeable
* contracts this implies that batch minting is only available during proxy deployment, and not in subsequent upgrades.
* These batches are limited to 5000 tokens at a time by default to accommodate off-chain indexers.
*
* Using this extension removes the ability to mint single tokens during 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. The hooks will be only called once per batch, so you should take `batchSize` parameter into consideration
* when relying on hooks.
*
* 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 Batch mint is restricted to the constructor.
* Any batch mint not emitting the {IERC721-Transfer} event outside of the constructor
* is non-ERC721 compliant.
*/
error ERC721ForbiddenBatchMint();
/**
* @dev Exceeds the max amount of mints per batch.
*/
error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch);
/**
* @dev Individual minting is not allowed.
*/
error ERC721ForbiddenMint();
/**
* @dev Batch burn is not supported.
*/
error ERC721ForbiddenBatchBurn();
/**
* @dev Maximum size of a batch of consecutive tokens. This is designed to limit stress on off-chain indexing
* services that have to record one entry per token, and have protections against "unreasonably large" batches of
* tokens.
*
* NOTE: Overriding the default value of 5000 will not cause on-chain issues, but may result in the asset not being
* correctly supported by off-chain indexing services (including marketplaces).
*/
function _maxBatchSize() internal view virtual returns (uint96) {
return 5000;
}
/**
* @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 || tokenId < _firstConsecutiveId()) {
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`. Returns the token id of the first token minted in the
* batch; if `batchSize` is 0, returns the number of consecutive ids minted so far.
*
* Requirements:
*
* - `batchSize` must not be greater than {_maxBatchSize}.
* - The function is called in the constructor of the contract (directly or indirectly).
*
* CAUTION: Does not emit a `Transfer` event. This is ERC721 compliant as long as it is done inside of the
* constructor, which is enforced by this function.
*
* CAUTION: Does not invoke `onERC721Received` on the receiver.
*
* Emits a {IERC2309-ConsecutiveTransfer} event.
*/
function _mintConsecutive(address to, uint96 batchSize) internal virtual returns (uint96) {
uint96 next = _nextConsecutiveId();
// minting a batch of size 0 is a no-op
if (batchSize > 0) {
if (address(this).code.length > 0) {
revert ERC721ForbiddenBatchMint();
}
if (to == address(0)) {
revert ERC721InvalidReceiver(address(0));
}
uint256 maxBatchSize = _maxBatchSize();
if (batchSize > maxBatchSize) {
revert ERC721ExceededMaxBatchMint(batchSize, maxBatchSize);
}
// hook before
_beforeTokenTransfer(address(0), to, next, batchSize);
// push an ownership checkpoint & emit event
uint96 last = next + batchSize - 1;
_sequentialOwnership.push(last, uint160(to));
// The invariant required by this function is preserved because the new sequentialOwnership checkpoint
// is attributing ownership of `batchSize` new tokens to account `to`.
__unsafe_increaseBalance(to, batchSize);
emit ConsecutiveTransfer(next, last, address(0), to);
// hook after
_afterTokenTransfer(address(0), to, next, batchSize);
}
return next;
}
/**
* @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 {
if (address(this).code.length == 0) {
revert ERC721ForbiddenMint();
}
super._mint(to, tokenId);
}
/**
* @dev See {ERC721-_afterTokenTransfer}. Burning of tokens that have been sequentially minted must be explicit.
*/
function _afterTokenTransfer(
address from,
address to,
uint256 firstTokenId,
uint256 batchSize
) internal virtual override {
if (
to == address(0) && // if we burn
firstTokenId >= _firstConsecutiveId() &&
firstTokenId < _nextConsecutiveId() &&
!_sequentialBurn.get(firstTokenId)
) // and the token was never marked as burnt
{
if (batchSize != 1) {
revert ERC721ForbiddenBatchBurn();
}
_sequentialBurn.set(firstTokenId);
}
super._afterTokenTransfer(from, to, firstTokenId, batchSize);
}
/**
* @dev Used to offset the first token id in {_nextConsecutiveId}
*/
function _firstConsecutiveId() internal view virtual returns (uint96) {
return 0;
}
/**
* @dev Returns the next tokenId to mint using {_mintConsecutive}. It will return {_firstConsecutiveId}
* if no consecutive tokenId has been minted before.
*/
function _nextConsecutiveId() private view returns (uint96) {
(bool exists, uint96 latestId, ) = _sequentialOwnership.latestCheckpoint();
return exists ? latestId + 1 : _firstConsecutiveId();
}
}