Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions[bot] committed Feb 24, 2024
1 parent 05bd2b9 commit cf0108d
Show file tree
Hide file tree
Showing 8 changed files with 710 additions and 41 deletions.
3 changes: 3 additions & 0 deletions contracts/ERC721AStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ library ERC721AStorage {
mapping(uint256 => ERC721AStorage.TokenApprovalRef) _tokenApprovals;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) _operatorApprovals;
// The amount of tokens minted above `_sequentialUpTo()`.
// We call these spot mints (i.e. non-sequential mints).
uint256 _spotMinted;
}

bytes32 internal constant STORAGE_SLOT = keccak256('ERC721A.contracts.storage.ERC721A');
Expand Down
186 changes: 174 additions & 12 deletions contracts/ERC721AUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ interface ERC721A__IERC721ReceiverUpgradeable {
* Token IDs are minted in sequential order (e.g. 0, 1, 2, 3, ...)
* starting from `_startTokenId()`.
*
* The `_sequentialUpTo()` function can be overriden to enable spot mints
* (i.e. non-consecutive mints) for `tokenId`s greater than `_sequentialUpTo()`.
*
* Assumptions:
*
* - An owner cannot have more than 2**64 - 1 (max value of uint64) of supply.
Expand Down Expand Up @@ -101,20 +104,37 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
ERC721AStorage.layout()._name = name_;
ERC721AStorage.layout()._symbol = symbol_;
ERC721AStorage.layout()._currentIndex = _startTokenId();

if (_sequentialUpTo() < _startTokenId()) _revert(SequentialUpToTooSmall.selector);
}

// =============================================================
// TOKEN COUNTING OPERATIONS
// =============================================================

/**
* @dev Returns the starting token ID.
* To change the starting token ID, please override this function.
* @dev Returns the starting token ID for sequential mints.
*
* Override this function to change the starting token ID for sequential mints.
*
* Note: The value returned must never change after any tokens have been minted.
*/
function _startTokenId() internal view virtual returns (uint256) {
return 0;
}

/**
* @dev Returns the maximum token ID (inclusive) for sequential mints.
*
* Override this function to return a value less than 2**256 - 1,
* but greater than `_startTokenId()`, to enable spot (non-sequential) mints.
*
* Note: The value returned must never change after any tokens have been minted.
*/
function _sequentialUpTo() internal view virtual returns (uint256) {
return type(uint256).max;
}

/**
* @dev Returns the next token ID to be minted.
*/
Expand All @@ -127,22 +147,26 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
* Burned tokens will reduce the count.
* To get the total number of tokens minted, please see {_totalMinted}.
*/
function totalSupply() public view virtual override returns (uint256) {
// Counter underflow is impossible as _burnCounter cannot be incremented
// more than `_currentIndex - _startTokenId()` times.
function totalSupply() public view virtual override returns (uint256 result) {
// Counter underflow is impossible as `_burnCounter` cannot be incremented
// more than `_currentIndex + _spotMinted - _startTokenId()` times.
unchecked {
return ERC721AStorage.layout()._currentIndex - ERC721AStorage.layout()._burnCounter - _startTokenId();
// With spot minting, the intermediate `result` can be temporarily negative,
// and the computation must be unchecked.
result = ERC721AStorage.layout()._currentIndex - ERC721AStorage.layout()._burnCounter - _startTokenId();
if (_sequentialUpTo() != type(uint256).max) result += ERC721AStorage.layout()._spotMinted;
}
}

/**
* @dev Returns the total amount of tokens minted in the contract.
*/
function _totalMinted() internal view virtual returns (uint256) {
function _totalMinted() internal view virtual returns (uint256 result) {
// Counter underflow is impossible as `_currentIndex` does not decrement,
// and it is initialized to `_startTokenId()`.
unchecked {
return ERC721AStorage.layout()._currentIndex - _startTokenId();
result = ERC721AStorage.layout()._currentIndex - _startTokenId();
if (_sequentialUpTo() != type(uint256).max) result += ERC721AStorage.layout()._spotMinted;
}
}

Expand All @@ -153,6 +177,13 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
return ERC721AStorage.layout()._burnCounter;
}

/**
* @dev Returns the total number of tokens that are spot-minted.
*/
function _totalSpotMinted() internal view virtual returns (uint256) {
return ERC721AStorage.layout()._spotMinted;
}

// =============================================================
// ADDRESS DATA OPERATIONS
// =============================================================
Expand Down Expand Up @@ -311,11 +342,17 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
}

/**
* Returns the packed ownership data of `tokenId`.
* @dev Returns the packed ownership data of `tokenId`.
*/
function _packedOwnershipOf(uint256 tokenId) private view returns (uint256 packed) {
if (_startTokenId() <= tokenId) {
packed = ERC721AStorage.layout()._packedOwnerships[tokenId];

if (tokenId > _sequentialUpTo()) {
if (_packedOwnershipExists(packed)) return packed;
_revert(OwnerQueryForNonexistentToken.selector);
}

// If the data at the starting slot does not exist, start the scan.
if (packed == 0) {
if (tokenId >= ERC721AStorage.layout()._currentIndex) _revert(OwnerQueryForNonexistentToken.selector);
Expand Down Expand Up @@ -444,6 +481,9 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
*/
function _exists(uint256 tokenId) internal view virtual returns (bool result) {
if (_startTokenId() <= tokenId) {
if (tokenId > _sequentialUpTo())
return _packedOwnershipExists(ERC721AStorage.layout()._packedOwnerships[tokenId]);

if (tokenId < ERC721AStorage.layout()._currentIndex) {
uint256 packed;
while ((packed = ERC721AStorage.layout()._packedOwnerships[tokenId]) == 0) --tokenId;
Expand All @@ -452,6 +492,17 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
}
}

/**
* @dev Returns whether `packed` represents a token that exists.
*/
function _packedOwnershipExists(uint256 packed) private pure returns (bool result) {
assembly {
// The following is equivalent to `owner != address(0) && burned == false`.
// Symbolically tested.
result := gt(and(packed, _BITMASK_ADDRESS), and(packed, _BITMASK_BURNED))
}
}

/**
* @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`.
*/
Expand Down Expand Up @@ -745,6 +796,8 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
uint256 end = startTokenId + quantity;
uint256 tokenId = startTokenId;

if (end - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector);

do {
assembly {
// Emit the `Transfer` event.
Expand Down Expand Up @@ -814,6 +867,8 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
_nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)
);

if (startTokenId + quantity - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector);

emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to);

ERC721AStorage.layout()._currentIndex = startTokenId + quantity;
Expand Down Expand Up @@ -850,8 +905,9 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
_revert(TransferToNonERC721ReceiverImplementer.selector);
}
} while (index < end);
// Reentrancy protection.
if (ERC721AStorage.layout()._currentIndex != end) _revert(bytes4(0));
// This prevents reentrancy to `_safeMint`.
// It does not prevent reentrancy to `_safeMintSpot`.
if (ERC721AStorage.layout()._currentIndex != end) revert();
}
}
}
Expand All @@ -863,6 +919,112 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
_safeMint(to, quantity, '');
}

/**
* @dev Mints a single token at `tokenId`.
*
* Note: A spot-minted `tokenId` that has been burned can be re-minted again.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - `tokenId` must be greater than `_sequentialUpTo()`.
* - `tokenId` must not exist.
*
* Emits a {Transfer} event for each mint.
*/
function _mintSpot(address to, uint256 tokenId) internal virtual {
if (tokenId <= _sequentialUpTo()) _revert(SpotMintTokenIdTooSmall.selector);
uint256 prevOwnershipPacked = ERC721AStorage.layout()._packedOwnerships[tokenId];
if (_packedOwnershipExists(prevOwnershipPacked)) _revert(TokenAlreadyExists.selector);

_beforeTokenTransfers(address(0), to, tokenId, 1);

// Overflows are incredibly unrealistic.
// The `numberMinted` for `to` is incremented by 1, and has a max limit of 2**64 - 1.
// `_spotMinted` is incremented by 1, and has a max limit of 2**256 - 1.
unchecked {
// Updates:
// - `address` to the owner.
// - `startTimestamp` to the timestamp of minting.
// - `burned` to `false`.
// - `nextInitialized` to `true` (as `quantity == 1`).
ERC721AStorage.layout()._packedOwnerships[tokenId] = _packOwnershipData(
to,
_nextInitializedFlag(1) | _nextExtraData(address(0), to, prevOwnershipPacked)
);

// Updates:
// - `balance += 1`.
// - `numberMinted += 1`.
//
// We can directly add to the `balance` and `numberMinted`.
ERC721AStorage.layout()._packedAddressData[to] += (1 << _BITPOS_NUMBER_MINTED) | 1;

// Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean.
uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS;

if (toMasked == 0) _revert(MintToZeroAddress.selector);

assembly {
// Emit the `Transfer` event.
log4(
0, // Start of data (0, since no data).
0, // End of data (0, since no data).
_TRANSFER_EVENT_SIGNATURE, // Signature.
0, // `address(0)`.
toMasked, // `to`.
tokenId // `tokenId`.
)
}

++ERC721AStorage.layout()._spotMinted;
}

_afterTokenTransfers(address(0), to, tokenId, 1);
}

/**
* @dev Safely mints a single token at `tokenId`.
*
* Note: A spot-minted `tokenId` that has been burned can be re-minted again.
*
* Requirements:
*
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}.
* - `tokenId` must be greater than `_sequentialUpTo()`.
* - `tokenId` must not exist.
*
* See {_mintSpot}.
*
* Emits a {Transfer} event.
*/
function _safeMintSpot(
address to,
uint256 tokenId,
bytes memory _data
) internal virtual {
_mintSpot(to, tokenId);

unchecked {
if (to.code.length != 0) {
uint256 currentSpotMinted = ERC721AStorage.layout()._spotMinted;
if (!_checkContractOnERC721Received(address(0), to, tokenId, _data)) {
_revert(TransferToNonERC721ReceiverImplementer.selector);
}
// This prevents reentrancy to `_safeMintSpot`.
// It does not prevent reentrancy to `_safeMint`.
if (ERC721AStorage.layout()._spotMinted != currentSpotMinted) revert();
}
}
}

/**
* @dev Equivalent to `_safeMintSpot(to, tokenId, '')`.
*/
function _safeMintSpot(address to, uint256 tokenId) internal virtual {
_safeMintSpot(to, tokenId, '');
}

// =============================================================
// APPROVAL OPERATIONS
// =============================================================
Expand Down Expand Up @@ -986,7 +1148,7 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
emit Transfer(from, address(0), tokenId);
_afterTokenTransfers(from, address(0), tokenId, 1);

// Overflow not possible, as _burnCounter cannot be exceed _currentIndex times.
// Overflow not possible, as `_burnCounter` cannot be exceed `_currentIndex + _spotMinted` times.
unchecked {
ERC721AStorage.layout()._burnCounter++;
}
Expand Down
25 changes: 25 additions & 0 deletions contracts/IERC721AUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ interface IERC721AUpgradeable {
*/
error OwnershipNotInitializedForExtraData();

/**
* `_sequentialUpTo()` must be greater than `_startTokenId()`.
*/
error SequentialUpToTooSmall();

/**
* The `tokenId` of a sequential mint exceeds `_sequentialUpTo()`.
*/
error SequentialMintExceedsLimit();

/**
* Spot minting requires a `tokenId` greater than `_sequentialUpTo()`.
*/
error SpotMintTokenIdTooSmall();

/**
* Cannot mint over a token that already exists.
*/
error TokenAlreadyExists();

/**
* The feature is not compatible with spot mints.
*/
error NotCompatibleWithSpotMints();

// =============================================================
// STRUCTS
// =============================================================
Expand Down
Loading

0 comments on commit cf0108d

Please sign in to comment.