December 23, 2023
11 mins.
We are well known for the fact that ERC721 is the standard for the NFTs, which are used to represent the unique digital assets on the Ethereum Blockchain. In addition to that ERC721 Enumerable is an extension of ERC721 that enhances its capabilities by enabling efficient enumeration of NFTs for advanced tracking, management and optimal for the operations on the blockchain.
ERC721 Enumerable is designed to track and enumerate tokens more effectively. It’s particularly useful for applications requiring knowledge of all tokens owned by an address or the total supply of tokens.
It is similar to the creation of an NFT smart contract using ERC721 but with some additional functions that we need to override so we can enjoy the benefits of ERC721 Enumerable.
You can learn how to implement and use ERC721 here
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.0;34import "@openzeppelin/contracts/token/ERC721/ERC721.sol";5import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";67contract MyNFT is ERC721, ERC721Enumerable {8 // Your contract code goes here9}
1function _beforeTokenTransfer(address from, address to, uint256 tokenId)2 internal3 override(ERC721, ERC721Enumerable)4{5 super._beforeTokenTransfer(from, to, tokenId);6}78function supportsInterface(bytes4 interfaceId)9 public10 view11 override(ERC721, ERC721Enumerable)12 returns (bool)13{14 return super.supportsInterface(interfaceId);15}
1function mint(address to, uint256 tokenId) public {2 _safeMint(to, tokenId);3 // Additional logic can be added here4}
1function getTokenByIndex(uint256 index) public view returns (uint256) {2 return tokenByIndex(index);3}45function getTokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256) {6 return tokenOfOwnerByIndex(owner, index);7}
This basic structure covers the essentials of using ERC721 Enumerable in a smart contract. With this, you can easily get the totalSupply of an NFT collection, fetch any token by its index or fetch tokens held by an owner.
We have discussed a bit about the uses of ERC721 Enumerable and how we can use it to create an NFT contract. Now, let us dive deep into it and see how and why it is useful, in what ways and how is it better than other approaches which can give the same result to get totalSupply, fetching tokens by index or owner.
To answer the simple question of ”why?” with a simple answer:
ERC721 Enumerable is designed for efficiency and reduced gas costs, crucial in Ethereum transactions.We know that you are curious to know the detailed explanation too. Here it is.
We refer to ERC721 and ERC721 Enumerable by Open Zeppelin for this.
1abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors {2 using Strings for uint256;34 // Token name5 string private _name;67 // Token symbol8 string private _symbol;910 mapping(uint256 tokenId => address) private _owners;1112 mapping(address owner => uint256) private _balances;
1abstract contract ERC721Enumerable is ERC721, IERC721Enumerable {2 mapping(address owner => mapping(uint256 index => uint256)) private _ownedTokens;3 mapping(uint256 tokenId => uint256) private _ownedTokensIndex;45 uint256[] private _allTokens;6 mapping(uint256 tokenId => uint256) private _allTokensIndex;
If you observe here, the Enumerable version has 4 extra storage variables i.e., _ownedTokens
, _ownedTokensIndex
, _allTokens
and _allTokensIndex
_ownedTokens
: Maps an owner’s address to their owned token IDs.
Example: If Alice (address 0x1234...alice
) owns tokens 1, 2, and 5, _ownedTokens[0x1234...alice]
could return [1, 2, 5]._ownedTokensIndex
: Maps a token ID to its index in the owner’s tokens list.
Example: If Alice’s 3rd token ID is 5, _ownedTokensIndex[5]
returns 2 (the index of token 5 in Alice’s list in the example above)._allTokens
: An array of all token IDs in the contract.
Example: If there are three minted tokens with IDs 1, 2, and 5, _allTokens
could be [1, 2, 5]._allTokensIndex
: Maps each token ID to its index in _allTokens
.
Example: Token ID 2 is the second token in _allTokens
(from the example above), _allTokensIndex[2]
returns 1.These 4 storage variables are the reason why ERC721 Enumerable is gas-efficient, optimal for tracking and management of tokens owned by addresses.
For example,
_allTokens
._allTokens[index]
._ownedTokens[owner][index]
Imagine you want to know the number of tokens that have been minted so far in ERC721. We have to go through all key-value pairs in the _owners
mapping or calculate the sum of all values in _balances
mapping.
But, the catch here is we cannot simply iterate over them, we must get all the keys or values (for which you have to maintain another storage variable, as Solidity doesn’t have an implementation to get all keys/values from a mapping) and do the necessary sum operations to get the total supply which is very high in gas cost. So in short this is quite inefficient.
On the other side having an extra storage variable which alters the state of the blockchain would cost you some gas on writes and reads but at least they are far lesser when compared to the earlier approach.
1mapping (address owner => uint[]) private ownerTokens;
This approach would significantly increase gas costs due to high storage requirements and reads/writes on array change with the number of elements in an array.
As the number of tokens owned by the owner increases it will increase the gas cost.
Also when setting the token IDs in an array, we must either define a fixed-length array or use a dynamic-length array. This again has a problem for each approach.
Directly returning a list is not gas-efficient.
It again needs an extra storage variable to support us to iterate over all the tokens held by an address.
Consider an example where the dApp wants to show details of a single NFT owned by an address, they might have to get a list of all tokens and iterate over them in frontend to pick one token and display its data. Here we are sending data that is unnecessary, also this affects gas costs drastically as the number of tokens owned increases for an address. Because it would take more memory and more unnecessary processing power on the node.
ERC721 Enumerable Approach | Other Approaches |
---|---|
Use of multiple extra mapping storage. | Use of single extra array storage. |
Reads/writes have almost constant gas costs. | Gas cost increases with the array sizes. |
Has granularity to get data for a single token with constant gas cost. | Have to iterate and pick the token, which varies the gas cost. |
Easy for tracking. | Need extra processing and would need more storage variables to do so. |
Not easy to iterate over all tokens of the owner by using single storage. | Can easily iterate over tokens owned by an address. |
It completely depends on the particular use of the contract, how the reads and writes are done, what is the frequency of each operation, the purpose of the NFT contract and various other things.
But when we look at the ease of implementation, efficiency and usability, ERC721 Enumerable has figured out most of them and solves most of the issues. Unless the project has some specific and sophisticated requirements which get benefit from the use of arrays for the storage of tokens for an address, ERC721ERC721 Enumerable should do the job in most cases.
Many NFT projects leverage ERC721 Enumerable to manage large collections efficiently, ensuring users can quickly access ownership data.
Some popular NFT project examples are:
You can go through the verified code of the above contracts to see how they made use of ERC721Enumerable
ERC721 Enumerable addresses the challenge of efficiently tracking and enumerating NFTs in a collection, particularly vital for projects with extensive NFT assets.
The ERC721 Enumerable extension, while useful, introduces some potential security concerns and issues:
Example: Imagine someone holds a large number of tokens in a collection and getting all of them in a single go would cost more gas [1]. Sometimes it may cross the block limit [2]. This makes the whole project sesceptible for more failure of transactions for a use case with large dataset, causing the issues in scaling the project [5]. This can be considered as a drawback due to the openness and transperency provided by the blockchain. Also due to this a front-runner attacker who might know about rare tokenId
s can look into mempool and frontrun to get those NFTs as we store tokenId
and index
in storage while miniting [4]. With additional data structures there is always a risk for bugs in implementation [3].
Though these all can be mitigated too:
All the issues are quite common in most of the smart contracts. We always got to tackle them anyway :wink:.
ERC721 Enumerable is ideal for projects
tokenID
or owner
The job of NFT marketplaces like OpenSea and Rarible would be very tough and they might had to go for a centralised API to keep track of NFTs if ERC721Enumerable doesn’t exist
Understand Gas Implications: Be aware of the gas costs associated with large-scale enumeration.
Scalability Planning: Plan for potential scalability challenges as your NFT collection grows.
ERC721 Enumerable extends the basic ERC721 functionality to efficiently track and enumerate tokens. It balances the need for detailed information with the practical considerations of gas costs and blockchain efficiency. Understanding its use and underlying structure is crucial for developing robust and efficient NFT projects.
By Krushi Raj Tula A developer, geek, enthusiast, who loves to solve problems and fix things with technology. You can reach out to him on Twitter!