Skip to content

Commit 91cb359

Browse files
Merge pull request #129 from ethscriptions-protocol/rename
More efficient enumerable
2 parents e3106d2 + c0b8975 commit 91cb359

6 files changed

+351
-66
lines changed

contracts/src/CollectionsERC721.sol

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22
pragma solidity 0.8.24;
33

44
// import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
5-
import "./ERC721EthscriptionsUpgradeable.sol";
5+
import "./ERC721EthscriptionsEnumerableUpgradeable.sol";
66
// import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
77
// import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
88
import "./Ethscriptions.sol";
99
import {LibString} from "solady/utils/LibString.sol";
1010
import {Base64} from "solady/utils/Base64.sol";
1111
import "./CollectionsProtocolHandler.sol";
12+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
13+
import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
1214

1315
/// @title CollectionsERC721
1416
/// @notice ERC-721 contract for an Ethscription collection
1517
/// @dev Maintains internal state but overrides ownerOf to delegate to Ethscriptions contract
16-
contract CollectionsERC721 is ERC721EthscriptionsUpgradeable {
18+
contract CollectionsERC721 is ERC721EthscriptionsEnumerableUpgradeable {
1719
using LibString for *;
1820

1921
/// @notice The main Ethscriptions contract
@@ -118,7 +120,12 @@ contract CollectionsERC721 is ERC721EthscriptionsUpgradeable {
118120
}
119121

120122
// Override ownerOf to delegate to Ethscriptions contract
121-
function ownerOf(uint256 tokenId) public view override returns (address) {
123+
function ownerOf(uint256 tokenId)
124+
public
125+
view
126+
override(ERC721EthscriptionsUpgradeable, IERC721)
127+
returns (address)
128+
{
122129
// Check if token exists in collection
123130
if (!_tokenExists(tokenId)) {
124131
revert("Token does not exist");
@@ -137,7 +144,12 @@ contract CollectionsERC721 is ERC721EthscriptionsUpgradeable {
137144
}
138145

139146
// Override tokenURI to generate full metadata JSON
140-
function tokenURI(uint256 tokenId) public view override returns (string memory) {
147+
function tokenURI(uint256 tokenId)
148+
public
149+
view
150+
override(ERC721EthscriptionsUpgradeable)
151+
returns (string memory)
152+
{
141153
if (!_tokenExists(tokenId)) {
142154
revert("Token does not exist");
143155
}
@@ -221,20 +233,36 @@ contract CollectionsERC721 is ERC721EthscriptionsUpgradeable {
221233
}
222234

223235
// Block external transfers - only internal _transfer is allowed for syncing
224-
function transferFrom(address, address, uint256) public pure override {
236+
function transferFrom(address, address, uint256)
237+
public
238+
pure
239+
override(ERC721EthscriptionsUpgradeable, IERC721)
240+
{
225241
revert TransferNotAllowed();
226242
}
227243

228-
function safeTransferFrom(address, address, uint256, bytes memory) public pure override {
244+
function safeTransferFrom(address, address, uint256, bytes memory)
245+
public
246+
pure
247+
override(ERC721EthscriptionsUpgradeable, IERC721)
248+
{
229249
revert TransferNotAllowed();
230250
}
231251

232252
// Block approvals - not needed for non-transferable tokens
233-
function approve(address, uint256) public pure override {
253+
function approve(address, uint256)
254+
public
255+
pure
256+
override(ERC721EthscriptionsUpgradeable, IERC721)
257+
{
234258
revert TransferNotAllowed();
235259
}
236260

237-
function setApprovalForAll(address, bool) public pure override {
261+
function setApprovalForAll(address, bool)
262+
public
263+
pure
264+
override(ERC721EthscriptionsUpgradeable, IERC721)
265+
{
238266
revert TransferNotAllowed();
239267
}
240268
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.24;
3+
4+
import "./ERC721EthscriptionsUpgradeable.sol";
5+
import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol";
6+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
7+
8+
/**
9+
* @dev Enumerable mixin for collections that require general token ID tracking and burns.
10+
*/
11+
abstract contract ERC721EthscriptionsEnumerableUpgradeable is ERC721EthscriptionsUpgradeable, IERC721Enumerable {
12+
/// @inheritdoc ERC165Upgradeable
13+
function supportsInterface(bytes4 interfaceId)
14+
public
15+
view
16+
virtual
17+
override(ERC721EthscriptionsUpgradeable, IERC165)
18+
returns (bool)
19+
{
20+
return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId);
21+
}
22+
23+
/// @inheritdoc IERC721Enumerable
24+
function tokenOfOwnerByIndex(address owner, uint256 index)
25+
public
26+
view
27+
virtual
28+
override(IERC721Enumerable, ERC721EthscriptionsUpgradeable)
29+
returns (uint256)
30+
{
31+
return super.tokenOfOwnerByIndex(owner, index);
32+
}
33+
34+
/// @inheritdoc IERC721Enumerable
35+
function totalSupply() public view virtual override returns (uint256) {
36+
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
37+
return $._allTokens.length;
38+
}
39+
40+
/// @inheritdoc IERC721Enumerable
41+
function tokenByIndex(uint256 index) public view virtual override returns (uint256) {
42+
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
43+
if (index >= $._allTokens.length) {
44+
revert ERC721OutOfBoundsIndex(address(0), index);
45+
}
46+
return $._allTokens[index];
47+
}
48+
49+
function _afterTokenMint(uint256 tokenId) internal virtual override {
50+
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
51+
$._allTokensIndex[tokenId] = $._allTokens.length;
52+
$._allTokens.push(tokenId);
53+
}
54+
55+
function _beforeTokenRemoval(uint256 tokenId, address) internal virtual override {
56+
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
57+
uint256 tokenIndex = $._allTokensIndex[tokenId];
58+
uint256 lastTokenIndex = $._allTokens.length - 1;
59+
uint256 lastTokenId = $._allTokens[lastTokenIndex];
60+
61+
$._allTokens[tokenIndex] = lastTokenId;
62+
$._allTokensIndex[lastTokenId] = tokenIndex;
63+
64+
delete $._allTokensIndex[tokenId];
65+
$._allTokens.pop();
66+
}
67+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.24;
3+
4+
import "./ERC721EthscriptionsUpgradeable.sol";
5+
import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol";
6+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
7+
8+
/**
9+
* @dev Enumerable mixin for Ethscriptions-style collections where token IDs are
10+
* sequential, start at zero, and tokens are never burned.
11+
*/
12+
abstract contract ERC721EthscriptionsSequentialEnumerableUpgradeable is ERC721EthscriptionsUpgradeable, IERC721Enumerable {
13+
/// @dev Raised when a mint attempts to skip or reuse a token ID.
14+
error ERC721SequentialEnumerableInvalidTokenId(uint256 expected, uint256 actual);
15+
/// @dev Raised if a contract attempts to remove a token from supply.
16+
error ERC721SequentialEnumerableTokenRemoval(uint256 tokenId);
17+
18+
/// @custom:storage-location erc7201:ethscriptions.storage.ERC721SequentialEnumerable
19+
struct ERC721SequentialEnumerableStorage {
20+
uint256 _mintCount;
21+
}
22+
23+
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC721SequentialEnumerableStorageLocation")) - 1)) & ~bytes32(uint256(0xff))
24+
bytes32 private constant ERC721SequentialEnumerableStorageLocation = 0x154e8d00bf5f00755eebdfa0d432d05cad242742a46a00bbdb15798f33342700;
25+
26+
function _getERC721SequentialEnumerableStorage()
27+
private
28+
pure
29+
returns (ERC721SequentialEnumerableStorage storage $)
30+
{
31+
assembly {
32+
$.slot := ERC721SequentialEnumerableStorageLocation
33+
}
34+
}
35+
36+
/// @inheritdoc ERC165Upgradeable
37+
function supportsInterface(bytes4 interfaceId)
38+
public
39+
view
40+
virtual
41+
override(ERC721EthscriptionsUpgradeable, IERC165)
42+
returns (bool)
43+
{
44+
return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId);
45+
}
46+
47+
/// @inheritdoc IERC721Enumerable
48+
function tokenOfOwnerByIndex(address owner, uint256 index)
49+
public
50+
view
51+
virtual
52+
override(IERC721Enumerable, ERC721EthscriptionsUpgradeable)
53+
returns (uint256)
54+
{
55+
return super.tokenOfOwnerByIndex(owner, index);
56+
}
57+
58+
/// @inheritdoc IERC721Enumerable
59+
function totalSupply() public view virtual override returns (uint256) {
60+
ERC721SequentialEnumerableStorage storage $ = _getERC721SequentialEnumerableStorage();
61+
return $._mintCount;
62+
}
63+
64+
/// @inheritdoc IERC721Enumerable
65+
function tokenByIndex(uint256 index) public view virtual override returns (uint256) {
66+
if (index >= totalSupply()) {
67+
revert ERC721OutOfBoundsIndex(address(0), index);
68+
}
69+
return index;
70+
}
71+
72+
function _afterTokenMint(uint256 tokenId) internal virtual override {
73+
ERC721SequentialEnumerableStorage storage $ = _getERC721SequentialEnumerableStorage();
74+
75+
uint256 expectedId = $._mintCount;
76+
if (tokenId != expectedId) {
77+
revert ERC721SequentialEnumerableInvalidTokenId(expectedId, tokenId);
78+
}
79+
80+
unchecked {
81+
$._mintCount = expectedId + 1;
82+
}
83+
}
84+
85+
function _beforeTokenRemoval(uint256 tokenId, address) internal virtual override {
86+
revert ERC721SequentialEnumerableTokenRemoval(tokenId);
87+
}
88+
}

contracts/src/ERC721EthscriptionsUpgradeable.sol

Lines changed: 14 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ pragma solidity 0.8.24;
33

44
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
55
import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
6-
import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol";
76
import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
87
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
98
import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol";
@@ -22,7 +21,7 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini
2221
* - No burn function (transfer to address(0) instead)
2322
* - Keeps only core transfer and ownership logic
2423
*/
25-
abstract contract ERC721EthscriptionsUpgradeable is Initializable, ContextUpgradeable, ERC165Upgradeable, IERC721, IERC721Metadata, IERC721Enumerable, IERC721Errors {
24+
abstract contract ERC721EthscriptionsUpgradeable is Initializable, ContextUpgradeable, ERC165Upgradeable, IERC721, IERC721Metadata, IERC721Errors {
2625
// Errors for enumerable functionality
2726
error ERC721OutOfBoundsIndex(address owner, uint256 index);
2827
error ERC721EnumerableForbiddenBatchMint();
@@ -62,7 +61,7 @@ abstract contract ERC721EthscriptionsUpgradeable is Initializable, ContextUpgrad
6261
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC721Enumerable")) - 1)) & ~bytes32(uint256(0xff))
6362
bytes32 private constant ERC721EnumerableStorageLocation = 0x645e039705490088daad89bae25049a34f4a9072d398537b1ab2425f24cbed00;
6463

65-
function _getERC721EnumerableStorage() private pure returns (ERC721EnumerableStorage storage $) {
64+
function _getERC721EnumerableStorage() internal pure returns (ERC721EnumerableStorage storage $) {
6665
assembly {
6766
$.slot := ERC721EnumerableStorageLocation
6867
}
@@ -88,7 +87,6 @@ abstract contract ERC721EthscriptionsUpgradeable is Initializable, ContextUpgrad
8887
return
8988
interfaceId == type(IERC721).interfaceId ||
9089
interfaceId == type(IERC721Metadata).interfaceId ||
91-
interfaceId == type(IERC721Enumerable).interfaceId ||
9290
super.supportsInterface(interfaceId);
9391
}
9492

@@ -132,13 +130,7 @@ abstract contract ERC721EthscriptionsUpgradeable is Initializable, ContextUpgrad
132130
*/
133131
function tokenURI(uint256 tokenId) public view virtual returns (string memory);
134132

135-
/// @inheritdoc IERC721Enumerable
136-
function totalSupply() public view virtual returns (uint256) {
137-
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
138-
return $._allTokens.length;
139-
}
140-
141-
/// @inheritdoc IERC721Enumerable
133+
/// @dev Return token owned by `owner` at `index`.
142134
function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual returns (uint256) {
143135
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
144136
if (index >= balanceOf(owner)) {
@@ -147,15 +139,6 @@ abstract contract ERC721EthscriptionsUpgradeable is Initializable, ContextUpgrad
147139
return $._ownedTokens[owner][index];
148140
}
149141

150-
/// @inheritdoc IERC721Enumerable
151-
function tokenByIndex(uint256 index) public view virtual returns (uint256) {
152-
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
153-
if (index >= totalSupply()) {
154-
revert ERC721OutOfBoundsIndex(address(0), index);
155-
}
156-
return $._allTokens[index];
157-
}
158-
159142
/**
160143
* @dev Approval functions removed - not needed for Ethscriptions.
161144
* These can be added back in child contracts if needed.
@@ -256,8 +239,8 @@ abstract contract ERC721EthscriptionsUpgradeable is Initializable, ContextUpgrad
256239
// This is a mint
257240
$._existsFlag[tokenId] = true;
258241

259-
// Add to all tokens enumeration
260-
_addTokenToAllTokensEnumeration(tokenId);
242+
// Allow derived contracts to adjust enumeration state for newly minted token
243+
_afterTokenMint(tokenId);
261244

262245
// Add to owner enumeration (also increments balance)
263246
_addTokenToOwnerEnumeration(to, tokenId);
@@ -335,9 +318,10 @@ abstract contract ERC721EthscriptionsUpgradeable is Initializable, ContextUpgrad
335318
if (!exists && $._existsFlag[tokenId]) {
336319
address owner = $._owners[tokenId];
337320

321+
_beforeTokenRemoval(tokenId, owner);
322+
338323
// Remove from enumerations (balance is decremented inside _removeTokenFromOwnerEnumeration)
339324
_removeTokenFromOwnerEnumeration(owner, tokenId);
340-
_removeTokenFromAllTokensEnumeration(tokenId);
341325

342326
// Clear owner storage for cleanliness
343327
delete $._owners[tokenId];
@@ -378,16 +362,6 @@ abstract contract ERC721EthscriptionsUpgradeable is Initializable, ContextUpgrad
378362
}
379363
}
380364

381-
/**
382-
* @dev Private function to add a token to this extension's token tracking data structures.
383-
* @param tokenId uint256 ID of the token to be added to the tokens list
384-
*/
385-
function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
386-
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
387-
$._allTokensIndex[tokenId] = $._allTokens.length;
388-
$._allTokens.push(tokenId);
389-
}
390-
391365
/**
392366
* @dev Private function to remove a token from this extension's ownership-tracking data structures.
393367
* Also decrements the owner's balance.
@@ -429,28 +403,12 @@ abstract contract ERC721EthscriptionsUpgradeable is Initializable, ContextUpgrad
429403
}
430404

431405
/**
432-
* @dev Private function to remove a token from this extension's token tracking data structures.
433-
* This has O(1) time complexity, but alters the order of the _allTokens array.
434-
* @param tokenId uint256 ID of the token to be removed from the tokens list
406+
* @dev Hook for derived contracts to react to token minting.
435407
*/
436-
function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
437-
ERC721EnumerableStorage storage $ = _getERC721EnumerableStorage();
438-
// To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
439-
// then delete the last slot (swap and pop).
440-
441-
uint256 lastTokenIndex = $._allTokens.length - 1;
442-
uint256 tokenIndex = $._allTokensIndex[tokenId];
443-
444-
// When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
445-
// rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
446-
// an 'if' statement (like in _removeTokenFromOwnerEnumeration)
447-
uint256 lastTokenId = $._allTokens[lastTokenIndex];
408+
function _afterTokenMint(uint256) internal virtual {}
448409

449-
$._allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
450-
$._allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
451-
452-
// This also deletes the contents at the last position of the array
453-
delete $._allTokensIndex[tokenId];
454-
$._allTokens.pop();
455-
}
456-
}
410+
/**
411+
* @dev Hook for derived contracts to react to token removal.
412+
*/
413+
function _beforeTokenRemoval(uint256, address) internal virtual {}
414+
}

0 commit comments

Comments
 (0)