Most Popular ERC721 Template Comparison

Most Popular ERC721 Template Comparison

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.

Most Popular ERC721 Template Comparison

ERC721Enumerable.sol

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.

Optimized ERC721Enumerable

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: totalSupplytokenByIndexand tokenofOwnerByIndex.

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 _balancessum _ownersmap in ERC721 with an array.

Most Popular ERC721 Template Comparison

Optimized ERC721Enumerable by Chance

_mint function changed to:

Most Popular ERC721 Template Comparison

Accidentally optimized ERC721

The view function in ERC721Enumerable is changed to:

Most Popular ERC721 Template Comparison

The tokenOfOwnerByIndex loop is very inefficient, but it doesn’t hurt either, since it’s almost always called off-chain and thus free.

a reminder

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.

ERC721A

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.

Most Popular ERC721 Template Comparison

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?

ERC721A storage

The ERC721A utilizes two structures and two maps to store ownership data.

Most Popular ERC721 Template Comparison

ERC721A structure

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:

Most Popular ERC721 Template Comparison

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.

Most Popular ERC721 Template Comparison

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.

Most Popular ERC721 Template Comparison

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.

Most Popular ERC721 Template Comparison

_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).

Most Popular ERC721 Template Comparison

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.

Most Popular ERC721 Template Comparison

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:

Most Popular ERC721 Template Comparison

Gas estimation for ERC721A

Therefore, depending on the ID of the token being transferred, checking its ownership, the gas cost can vary widely.

Most Popular ERC721 Template Comparison

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.

cost comparison

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.

Most Popular ERC721 Template Comparison

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).

Most Popular ERC721 Template Comparison

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.

Like (0)
Donate Buy me a coffee Buy me a coffee
Previous 2022-03-17 09:00
Next 2022-03-17 09:04

Related articles