Aside from recent macroeconomic events that have caused the market to be slightly bearish, I think we can be confident that we are still in the middle of a bull market for NFTs.
In this bull market, hundreds of projects are launched every week, most of which are similar smart contracts. Since almost everything in this space is open source, it is easy to implement proven solutions.
However, this leads to projects copy-pasting the few NFT smart contract templates that are currently popular, without really understanding the different pros and cons that exist in each implementation.
To remove this confusion to some extent, it is worth researching the most popular templates, examining the pros and cons of each, and trying to draw some conclusions about the best contracts for different types of projects.
ERC721 and ERC721Enumerable
NFTs originated from the EIP-721 non-fungible token standard proposal. The OG implementation of this proposal that almost everyone uses is done by OpenZeppelin.
The functionality provided by ERC721 soon proved insufficient, and many projects started adopting the ERC721Enumerable extension. This extension enhances the functionality of the original ERC721 by adding enumerability of all token IDs in the contract, as well as a method to check all token IDs owned by an account.
However, this is where we start to run into trouble. The problem with ERC721Enumerable is that it does a lot of unnecessary things, causing gas costs to go up and costing the community millions of dollars.
ERC721Enumerable can optimize the read function, but it is not good for the write function.
ERC721Enumerable uses a lot of redundant storage, which not only increases the minting cost of the token, but also the transfer cost of the token.
These are state modification functions, executed every time an update or transfer occurs.
We can clearly see that ERC721Enumerable is not an ideal choice and should no longer be used by most projects.
Fortunately, some smart developers have noticed these inefficiencies and devised solutions.
It is recommended that you read the entire article and become familiar with the solution. The basic premise of this contract is that it reverses the focus of optimization and optimizes the cost of writing functions to the cost of reading functions. This is nice because if the read functions are called off-chain, they don’t cost us money. The rationale behind the
ERC721Enumerable可以访问following three functions:
We can live with the inefficiency and infinite looping of these functions because they are almost always called off-chain.
This implementation first replaces the
_ownersmap in ERC721 with an array.
Optimized ERC721Enumerable by Chance
_mint function changed to:
Accidentally optimized ERC721
The view function in ERC721Enumerable is changed to:
The tokenOfOwnerByIndex loop is very inefficient, but it doesn’t hurt either, since it’s almost always called off-chain and thus free.
As mentioned above, the version above also removes the
_balancesarray, thus reducing the extra storage write operations. Therefore, the balanceOf function loops through the entire owners array to determine the balance of the address. This is again a very inefficient read operation, however, as opposed to tokenOfOwnerByIndex, I can imagine dozens of situations where an address’s balance needs to be checked on-chain.
For example, with MetaMorphies, we are already developing NFT staking pools, a governance system where voting rights are granted in token ownership, and a mobile augmented reality application. Various parts of this complex system will check on-chain balances, so it has to be efficient.
Even if you don’t want to mount any other contracts, you should be careful if your mint function allows an address to check how many tokens it has before minting. This might be a good thing for the first few miners, but it would be a disaster for the 8756th miner.
Blindly copy-pasting code is not advisable when it comes to high-value operations. Just because a solution works perfectly in its author’s case doesn’t mean it will automatically apply to the special needs of our project.
Agency and Approval
Another optimization suggested by Chance is to pre-approve the OpenSea proxy registration contract to transfer tokens and allow the base contract owner to mount any other contract to the base contract in the future and have it automatically return true for it in isApprovedForAll.
This solution has serious problems in many ways:
- What to do if the Open Sea proxy registry is corrupted
- What to do if an installed contract is compromised
- What if the owner of the underlying contract decides to take action and take all the tokens from the contract
- What if the underlying contract owner’s wallet is stolen
Once this mode is implemented, if any of the above happens, the attacker can obtain all NFTs from each owner. This just leads to more attack vectors and trust assumptions, while saving $20 in approval transaction costs, at the potential risk of losing millions of dollars in value.
Optimization is a very good thing, but there is something more advanced, which is a fundamental premise in the world of blockchain and crypto: building trustless systems.
Another very clever and recently very popular solution is the ERC721A contract developed by Chiru Labs. Let’s see what happens in this smart contract.
The basic premise of ERC721A is the ability to mint multiple NFTs at the cost of minting a single NFT. Let’s see what optimizations are made and how the contract works.
According to the description of the development team, the first optimization is to remove the redundant storage introduced by ERC721Enumerable, similar to what Chance does.
The second and third optimization is to update the owner’s balance and token ownership data once per batch minting request.
The for loop was removed, so ERC721A delivered on its promise to mint multiple NFTs at the cost of minting a single NFT.
But this raises multiple questions: How does the contract store token ID and ownership data? How to determine ownership of tokens? How to transfer tokens?
The ERC721A utilizes two structures and two maps to store ownership data.
Look at these names to understand their purpose. TokenOwnership uses a single storage slot to store some information about token ownership, and AddressData uses a single storage slot to store information about minter addresses.
The approach taken by ERC721A may seem counter-intuitive at first glance, so let’s take a look at how storage is written and read during different operations to reach the correct state of the contract.
Let’s say we are the first address created from the contract and we mint 10 tokens. In this case, the following happens:
Figure 1: Mint operation in ERC721A
In our batch, the first ID is 0, so the contract configures the AddressData structure with the batch size and tokenownership structure for the token ID and timestamp. The data for the rest of the token IDs is empty. So how do we determine ownership? Isn’t that a problem?
To understand why not, let’s look at how the contract determines the ownership of the token.
Figure 2: Calling ownerOf() on ERC721A
Let’s assume this operation follows the previous operation to mint 10 tokens. We are interested in the owner of token ID 3, so we call ownerOf(3). The slot at ownerOf(3)] is empty, so the function moves to the previous ID. until a coin with an ownership address is found.
But what happens if I transfer token ID 0 to another address? Do all my tokens come with empty data? How is ownership determined in this case? Let’s see what’s going on inside the _transfer function.
Figure 3: _transfer() in ERC721A
When a token is transferred, the code checks if the next token has an owner set, and if not, sets the from address as the owner. We know that token IDs are assigned in ascending order at minting, so if an address mints multiple tokens, it must also own the next token if the ownership data is not initialized.
My first thought when examining the ERC721A contract was that it does keep the cost low for batch minting, but it has a nasty loop in ownershipOf and it calls ownershipOf every time a token transfer happens. It seems like something that can bite back on users.
_ownershipOf in ERC721A
This is indeed a legitimate concern. To illustrate how much these costs can increase, let’s imagine an unrealistic scenario of minting 350 tokens in one transaction, then checking the ownership of token ID 330 and making the transfer. (The getOwner function is a simple function that calls ownerOf and then writes something to storage to account for the cost of any write function calls).
Gas estimation for ERC721A
At $2708/ETH and 70 gwei/gas, checking ownership and transferring a single token with ID 330 costs more than minting 350 tokens. This is because the loop in _ownershipOf goes from 330 to 0 and every SLOAD operation consumes gas.
If we minted 350 tokens again, but transferred token ID 1 instead of 330, the numbers would look very different.
Gas estimation for ERC721A
We will prevent anyone from minting that many tokens from our contracts. Suppose we limit the maximum batch size to 10. If someone minted 10 tokens and then tried to transfer the token with ID 9, the numbers would look like this:
Gas estimation for ERC721A
Therefore, depending on the ID of the token being transferred, checking its ownership, the gas cost can vary widely.
For many projects considering implementing the contract, this could be a deal breaker. On the one hand, this can save a lot of money and be efficient if you limit the batch size to a small number. I think most sets have a finite batch size, let’s say 10, so for most people this shouldn’t be a problem.
Without limiting batch size, it boils down to whether you trust your own clients to think deeply about smart contracts before token transfers, and whether you have tampered with your project that a single token transfer would let them Pay a huge price.
If you plan to install this contract into any kind of complex ecosystem, you must also consider the potential pitfalls of the _ownershipOf function. As I said above, many contracts (like staking pools) check token balances and ownership, so if these functions are called on-chain, our goal should be to make them efficient.
Finally, let’s run a few tests to get an intuition of the potential gas cost of each solution. Below we create 1, 3, 5 and 10 tokens from each implementation 5 times each. Custom721A is our ERC721A implementation, CustomEnumerable is the optimized ERC721Enumerable, and OZEnumerable is the Open Zeppelin version.
Result of minting some tokens
These observations are consistent with what we discussed above. The Open Zeppelin version is very expensive and inefficient everywhere. If you mint only one token, Custom721A and CustomEnumerable cost about the same, if you mint multiple tokens, Custom721A’s cost remains almost the same and increases with the loop size of CustomEnumerable.
Now let’s try it from the hypothetical scenario above: we mint 350 tokens, then randomly pick 20 token IDs, then check ownership and transfer (a known weakness of ERC721A).
Results of minting and token transfers
This is consistent with what we discussed earlier. The cost of checking ownership is about 5 times that of ERC721A, and the cost of token transfers is about 4 times that of ERC721A.
We can also clearly see that batch minting is where ERC721A really shines. Minting 350 tokens costs only $147, while the same operation costs 11 times more for CustomEnumerable. However, it is very unrealistic to mint 350 tokens in one batch.
Conclusion: Rule all with one ERC721?
So, are you at a crossroads, which one to use? I can confidently say that it depends on the situation.
The most important point of this article should be that when it comes to smart contracts, you must understand the ins and outs of everything imported into your codebase. Please don’t copy-paste code blindly, even if it’s from a very reliable source, as their solution may not fit the specific needs of the project you’re working on.
Posted by:CoinYuppie，Reprinted with attribution to:https://coinyuppie.com/most-popular-erc721-template-comparison/
Coinyuppie is an open information publishing platform, all information provided is not related to the views and positions of coinyuppie, and does not constitute any investment and financial advice. Users are expected to carefully screen and prevent risks.