Skip to content

Commit cf0108d

Browse files
1 parent 05bd2b9 commit cf0108d

8 files changed

+710
-41
lines changed

contracts/ERC721AStorage.sol

+3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ library ERC721AStorage {
4444
mapping(uint256 => ERC721AStorage.TokenApprovalRef) _tokenApprovals;
4545
// Mapping from owner to operator approvals
4646
mapping(address => mapping(address => bool)) _operatorApprovals;
47+
// The amount of tokens minted above `_sequentialUpTo()`.
48+
// We call these spot mints (i.e. non-sequential mints).
49+
uint256 _spotMinted;
4750
}
4851

4952
bytes32 internal constant STORAGE_SLOT = keccak256('ERC721A.contracts.storage.ERC721A');

contracts/ERC721AUpgradeable.sol

+174-12
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ interface ERC721A__IERC721ReceiverUpgradeable {
3030
* Token IDs are minted in sequential order (e.g. 0, 1, 2, 3, ...)
3131
* starting from `_startTokenId()`.
3232
*
33+
* The `_sequentialUpTo()` function can be overriden to enable spot mints
34+
* (i.e. non-consecutive mints) for `tokenId`s greater than `_sequentialUpTo()`.
35+
*
3336
* Assumptions:
3437
*
3538
* - An owner cannot have more than 2**64 - 1 (max value of uint64) of supply.
@@ -101,20 +104,37 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
101104
ERC721AStorage.layout()._name = name_;
102105
ERC721AStorage.layout()._symbol = symbol_;
103106
ERC721AStorage.layout()._currentIndex = _startTokenId();
107+
108+
if (_sequentialUpTo() < _startTokenId()) _revert(SequentialUpToTooSmall.selector);
104109
}
105110

106111
// =============================================================
107112
// TOKEN COUNTING OPERATIONS
108113
// =============================================================
109114

110115
/**
111-
* @dev Returns the starting token ID.
112-
* To change the starting token ID, please override this function.
116+
* @dev Returns the starting token ID for sequential mints.
117+
*
118+
* Override this function to change the starting token ID for sequential mints.
119+
*
120+
* Note: The value returned must never change after any tokens have been minted.
113121
*/
114122
function _startTokenId() internal view virtual returns (uint256) {
115123
return 0;
116124
}
117125

126+
/**
127+
* @dev Returns the maximum token ID (inclusive) for sequential mints.
128+
*
129+
* Override this function to return a value less than 2**256 - 1,
130+
* but greater than `_startTokenId()`, to enable spot (non-sequential) mints.
131+
*
132+
* Note: The value returned must never change after any tokens have been minted.
133+
*/
134+
function _sequentialUpTo() internal view virtual returns (uint256) {
135+
return type(uint256).max;
136+
}
137+
118138
/**
119139
* @dev Returns the next token ID to be minted.
120140
*/
@@ -127,22 +147,26 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
127147
* Burned tokens will reduce the count.
128148
* To get the total number of tokens minted, please see {_totalMinted}.
129149
*/
130-
function totalSupply() public view virtual override returns (uint256) {
131-
// Counter underflow is impossible as _burnCounter cannot be incremented
132-
// more than `_currentIndex - _startTokenId()` times.
150+
function totalSupply() public view virtual override returns (uint256 result) {
151+
// Counter underflow is impossible as `_burnCounter` cannot be incremented
152+
// more than `_currentIndex + _spotMinted - _startTokenId()` times.
133153
unchecked {
134-
return ERC721AStorage.layout()._currentIndex - ERC721AStorage.layout()._burnCounter - _startTokenId();
154+
// With spot minting, the intermediate `result` can be temporarily negative,
155+
// and the computation must be unchecked.
156+
result = ERC721AStorage.layout()._currentIndex - ERC721AStorage.layout()._burnCounter - _startTokenId();
157+
if (_sequentialUpTo() != type(uint256).max) result += ERC721AStorage.layout()._spotMinted;
135158
}
136159
}
137160

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

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

180+
/**
181+
* @dev Returns the total number of tokens that are spot-minted.
182+
*/
183+
function _totalSpotMinted() internal view virtual returns (uint256) {
184+
return ERC721AStorage.layout()._spotMinted;
185+
}
186+
156187
// =============================================================
157188
// ADDRESS DATA OPERATIONS
158189
// =============================================================
@@ -311,11 +342,17 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
311342
}
312343

313344
/**
314-
* Returns the packed ownership data of `tokenId`.
345+
* @dev Returns the packed ownership data of `tokenId`.
315346
*/
316347
function _packedOwnershipOf(uint256 tokenId) private view returns (uint256 packed) {
317348
if (_startTokenId() <= tokenId) {
318349
packed = ERC721AStorage.layout()._packedOwnerships[tokenId];
350+
351+
if (tokenId > _sequentialUpTo()) {
352+
if (_packedOwnershipExists(packed)) return packed;
353+
_revert(OwnerQueryForNonexistentToken.selector);
354+
}
355+
319356
// If the data at the starting slot does not exist, start the scan.
320357
if (packed == 0) {
321358
if (tokenId >= ERC721AStorage.layout()._currentIndex) _revert(OwnerQueryForNonexistentToken.selector);
@@ -444,6 +481,9 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
444481
*/
445482
function _exists(uint256 tokenId) internal view virtual returns (bool result) {
446483
if (_startTokenId() <= tokenId) {
484+
if (tokenId > _sequentialUpTo())
485+
return _packedOwnershipExists(ERC721AStorage.layout()._packedOwnerships[tokenId]);
486+
447487
if (tokenId < ERC721AStorage.layout()._currentIndex) {
448488
uint256 packed;
449489
while ((packed = ERC721AStorage.layout()._packedOwnerships[tokenId]) == 0) --tokenId;
@@ -452,6 +492,17 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
452492
}
453493
}
454494

495+
/**
496+
* @dev Returns whether `packed` represents a token that exists.
497+
*/
498+
function _packedOwnershipExists(uint256 packed) private pure returns (bool result) {
499+
assembly {
500+
// The following is equivalent to `owner != address(0) && burned == false`.
501+
// Symbolically tested.
502+
result := gt(and(packed, _BITMASK_ADDRESS), and(packed, _BITMASK_BURNED))
503+
}
504+
}
505+
455506
/**
456507
* @dev Returns whether `msgSender` is equal to `approvedAddress` or `owner`.
457508
*/
@@ -745,6 +796,8 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
745796
uint256 end = startTokenId + quantity;
746797
uint256 tokenId = startTokenId;
747798

799+
if (end - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector);
800+
748801
do {
749802
assembly {
750803
// Emit the `Transfer` event.
@@ -814,6 +867,8 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
814867
_nextInitializedFlag(quantity) | _nextExtraData(address(0), to, 0)
815868
);
816869

870+
if (startTokenId + quantity - 1 > _sequentialUpTo()) _revert(SequentialMintExceedsLimit.selector);
871+
817872
emit ConsecutiveTransfer(startTokenId, startTokenId + quantity - 1, address(0), to);
818873

819874
ERC721AStorage.layout()._currentIndex = startTokenId + quantity;
@@ -850,8 +905,9 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
850905
_revert(TransferToNonERC721ReceiverImplementer.selector);
851906
}
852907
} while (index < end);
853-
// Reentrancy protection.
854-
if (ERC721AStorage.layout()._currentIndex != end) _revert(bytes4(0));
908+
// This prevents reentrancy to `_safeMint`.
909+
// It does not prevent reentrancy to `_safeMintSpot`.
910+
if (ERC721AStorage.layout()._currentIndex != end) revert();
855911
}
856912
}
857913
}
@@ -863,6 +919,112 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
863919
_safeMint(to, quantity, '');
864920
}
865921

922+
/**
923+
* @dev Mints a single token at `tokenId`.
924+
*
925+
* Note: A spot-minted `tokenId` that has been burned can be re-minted again.
926+
*
927+
* Requirements:
928+
*
929+
* - `to` cannot be the zero address.
930+
* - `tokenId` must be greater than `_sequentialUpTo()`.
931+
* - `tokenId` must not exist.
932+
*
933+
* Emits a {Transfer} event for each mint.
934+
*/
935+
function _mintSpot(address to, uint256 tokenId) internal virtual {
936+
if (tokenId <= _sequentialUpTo()) _revert(SpotMintTokenIdTooSmall.selector);
937+
uint256 prevOwnershipPacked = ERC721AStorage.layout()._packedOwnerships[tokenId];
938+
if (_packedOwnershipExists(prevOwnershipPacked)) _revert(TokenAlreadyExists.selector);
939+
940+
_beforeTokenTransfers(address(0), to, tokenId, 1);
941+
942+
// Overflows are incredibly unrealistic.
943+
// The `numberMinted` for `to` is incremented by 1, and has a max limit of 2**64 - 1.
944+
// `_spotMinted` is incremented by 1, and has a max limit of 2**256 - 1.
945+
unchecked {
946+
// Updates:
947+
// - `address` to the owner.
948+
// - `startTimestamp` to the timestamp of minting.
949+
// - `burned` to `false`.
950+
// - `nextInitialized` to `true` (as `quantity == 1`).
951+
ERC721AStorage.layout()._packedOwnerships[tokenId] = _packOwnershipData(
952+
to,
953+
_nextInitializedFlag(1) | _nextExtraData(address(0), to, prevOwnershipPacked)
954+
);
955+
956+
// Updates:
957+
// - `balance += 1`.
958+
// - `numberMinted += 1`.
959+
//
960+
// We can directly add to the `balance` and `numberMinted`.
961+
ERC721AStorage.layout()._packedAddressData[to] += (1 << _BITPOS_NUMBER_MINTED) | 1;
962+
963+
// Mask `to` to the lower 160 bits, in case the upper bits somehow aren't clean.
964+
uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS;
965+
966+
if (toMasked == 0) _revert(MintToZeroAddress.selector);
967+
968+
assembly {
969+
// Emit the `Transfer` event.
970+
log4(
971+
0, // Start of data (0, since no data).
972+
0, // End of data (0, since no data).
973+
_TRANSFER_EVENT_SIGNATURE, // Signature.
974+
0, // `address(0)`.
975+
toMasked, // `to`.
976+
tokenId // `tokenId`.
977+
)
978+
}
979+
980+
++ERC721AStorage.layout()._spotMinted;
981+
}
982+
983+
_afterTokenTransfers(address(0), to, tokenId, 1);
984+
}
985+
986+
/**
987+
* @dev Safely mints a single token at `tokenId`.
988+
*
989+
* Note: A spot-minted `tokenId` that has been burned can be re-minted again.
990+
*
991+
* Requirements:
992+
*
993+
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}.
994+
* - `tokenId` must be greater than `_sequentialUpTo()`.
995+
* - `tokenId` must not exist.
996+
*
997+
* See {_mintSpot}.
998+
*
999+
* Emits a {Transfer} event.
1000+
*/
1001+
function _safeMintSpot(
1002+
address to,
1003+
uint256 tokenId,
1004+
bytes memory _data
1005+
) internal virtual {
1006+
_mintSpot(to, tokenId);
1007+
1008+
unchecked {
1009+
if (to.code.length != 0) {
1010+
uint256 currentSpotMinted = ERC721AStorage.layout()._spotMinted;
1011+
if (!_checkContractOnERC721Received(address(0), to, tokenId, _data)) {
1012+
_revert(TransferToNonERC721ReceiverImplementer.selector);
1013+
}
1014+
// This prevents reentrancy to `_safeMintSpot`.
1015+
// It does not prevent reentrancy to `_safeMint`.
1016+
if (ERC721AStorage.layout()._spotMinted != currentSpotMinted) revert();
1017+
}
1018+
}
1019+
}
1020+
1021+
/**
1022+
* @dev Equivalent to `_safeMintSpot(to, tokenId, '')`.
1023+
*/
1024+
function _safeMintSpot(address to, uint256 tokenId) internal virtual {
1025+
_safeMintSpot(to, tokenId, '');
1026+
}
1027+
8661028
// =============================================================
8671029
// APPROVAL OPERATIONS
8681030
// =============================================================
@@ -986,7 +1148,7 @@ contract ERC721AUpgradeable is ERC721A__Initializable, IERC721AUpgradeable {
9861148
emit Transfer(from, address(0), tokenId);
9871149
_afterTokenTransfers(from, address(0), tokenId, 1);
9881150

989-
// Overflow not possible, as _burnCounter cannot be exceed _currentIndex times.
1151+
// Overflow not possible, as `_burnCounter` cannot be exceed `_currentIndex + _spotMinted` times.
9901152
unchecked {
9911153
ERC721AStorage.layout()._burnCounter++;
9921154
}

contracts/IERC721AUpgradeable.sol

+25
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,31 @@ interface IERC721AUpgradeable {
7474
*/
7575
error OwnershipNotInitializedForExtraData();
7676

77+
/**
78+
* `_sequentialUpTo()` must be greater than `_startTokenId()`.
79+
*/
80+
error SequentialUpToTooSmall();
81+
82+
/**
83+
* The `tokenId` of a sequential mint exceeds `_sequentialUpTo()`.
84+
*/
85+
error SequentialMintExceedsLimit();
86+
87+
/**
88+
* Spot minting requires a `tokenId` greater than `_sequentialUpTo()`.
89+
*/
90+
error SpotMintTokenIdTooSmall();
91+
92+
/**
93+
* Cannot mint over a token that already exists.
94+
*/
95+
error TokenAlreadyExists();
96+
97+
/**
98+
* The feature is not compatible with spot mints.
99+
*/
100+
error NotCompatibleWithSpotMints();
101+
77102
// =============================================================
78103
// STRUCTS
79104
// =============================================================

0 commit comments

Comments
 (0)