Nazar Ilamanov's avatar

Nazar Ilamanov • solidnoob

Common patterns in good NFT contracts

One stop shop for NFT devs

I’ve read a lot of NFT contracts and here are the most common patterns I saw in the best ones:

  • Replace ERC721Enumerable with a counter for gas savings
  • Use ERC721A for efficient batch mints
  • Use mint instead of safeMint
  • Implement allowlists using Merkle trees
  • Upgradeable/swappable metadata contract
  • Protect against bots
  • Prevent NFT sniping
  • Other miscellaneous stuff

Replace ERC721Enumerable with a counter for gas savings

First, a short background. ERC-721 standard consists of 2 extensions:

  • ERC721Metadata
  • ERC721Enumerable

The core 721 standard is pretty simple. You just need to implement the following functions in order to be core 721 compliant:

The 2 extensions add these functions on top:

So, what’s the problem with ERC721Enumerable?

OpenZeppelin provides off-the-shelf implementations of all these interfaces. None of them are perfect but the 2 implementations (ERC721 and ERC721Metadata) do a pretty good job. The implementation of ERC721Enumerable, however, is very wasteful. It eats up a lot of gas and wastes storage. Look at how they implement the ERC721Enumerable interface:

You can imagine how wasteful it gets to keep track of so many mappings and arrays. (BTW, they are updated in before/after transfer hooks). The code above is (wrongly) optimized for read functions while it should have been optimized for write functions (because read functions are mostly free). Most contract developers are too lazy and just inherit all 3 interfaces from OpenZeppelin. But you can do better.

Solution: If the only function that you need from Enumerable is totalSupply, then you can just use an integer and use it as a counter to keep track of the number of NFTs minted. Your contract will no longer implement the entire Enumerable interface but you will save a ton of gas. And the good news is that you only need to implement the core ERC721 interface in order to be ERC721 compliant. (i.e. NFT marketplaces won’t have a problem parsing your contract even if you don’t implement the Enumerable interface)

The caveat is that you will no longer have the mapping from token IDs to owners and vice versa, but can keep track of this information off-chain using Ethereum events. I think OpenSea API already provides this. Also, The Graph can simplify your web2 infra related to Ethereum events.

Contacts using a counter instead of inheriting OZ’s ERC721Enumerable:

  • Crypto Coven
  • Azuki

Credit to Shiny Object for discovering this.

Use ERC721A for efficient batch mints

Most of the NFT contracts extend the OpenZeppelin implementation. But it wasn’t optimized for batch minting. Batch minting allows some significant savings compared to one-at-a-time minting.

For example, instead of emitting a transfer event for every single mint, you can emit just one event and specify the entire batch that was minted in the event. Another example is to update the owner’s balance just once per batch instead of after every single mint.

Realizing that there are some possible optimizations for batch minting, the Azuki team created ERC721A - an implementation of ERC721 optimized for batch minting. Here is how it compares to OZ’s implementation.

How was ERC721A able to achieve this kind of savings? Mainly using these optimizations:

  • Getting rid of OZ’s ERC721Enumerable
  • Updating data only once per batch instead of after every single mint
  • Using a more efficient layout for storage: if consecutive NFTs have the same owner, don’t store redundant information about the owner (store it just once for the very first owned NFT). This data can be inferred at run time by reading to the left until you find the owner info.
  • Emit just one transfer event per batch. (This is a more recent change and was not part of the original ERC721A)

You can read more about ERC721A on the Azuki website.

If you need all the functionality in the Enumerable interface, you can use Azuki’s ERC721AQueryable interface which is an optimized version of ERC721Enumerable.

Contacts using ERC721A:

  • Azuki
  • goblintown
  • wagdie
  • Moonbirds

Use mint instead of safeMint

safeMint was originally added to prevent loss of NFT into the ether. If the receiver of the NFT is a contract and it doesn’t know how to transfer the NFT, the NFT will forever be stuck inside of the contract.

So, receiving contracts are meant to implement the ERC721Receiver interface which will allow for the NFT contract to check if the NFT was correctly received by the receiver. If a contract implements the Receiver interface, it signals that it knows how to handle the NFT once received.

You don’t need to use safeMint if your receiver is just a regular account, not a contract. You also don’t need to use safeMint if you’re 100% certain that the receiver contract can handle the NFT.

By using mint instead of safeMint, you can save some gas. The same goes for using transfer instead of safeTransfer.

Contracts doing this:

  • Crypto Coven

Implement allowlists using Merkle trees

You can save lots of storage (and gas) if you use Merkle trees to implement your allowlists. Merkle trees are an efficient data structure that allows you to store a bunch of addresses at the cost of just a single one. The tradeoff is that the lookup time is not O(1)O(1). But it’s still pretty good at O(n)O(n).

All you need to add to your contract are these functions:

You can use OpenZeppelin’s MerkleProof library for the verification step.

You would then modify your mint function like this:

Basically, you add one additional parameter to the mint function: merkleProof. It’s an array of hashes of addresses that make up the path from the minter’s address to the root address. You can compute this path off-chain on your website for every allowlisted minter. Read more about it here.

Contracts using Merkle trees:

  • Crypto Coven
  • OKPC

Upgradeable/swappable metadata contract

If you later want to upgrade how your NFT looks or switch between on-chain and off-chain rendering, you should make your metadata contract swappable. Like this:

Contracts using this:

  • OKPC
  • Watchfaces

Protect against bots

2 safeguards you can take to prevent bots from minting out all your tokens:

  1. Limit mints per wallet
  1. Check for msg.sender == tx.origin. When a contract calls your mint function, msg.sender will be the contract address but tx.origin will be the address of the person who is calling that contract. More info here.

Prevent NFT sniping

NFT sniping is when someone knows which tokens are rare and knows the order in which tokens are minted. So they go ahead and mint a bunch of NFTs at the right time hoping to snipe the rare ones.

You want to avoid NFT sniping in order to guarantee a fair distribution of tokens to everyone. Let’s talk about how to prevent NFT sniping (at least to some extent). NFT sniping consists of 2 problems:

  1. Revealing your token metadata (allows the snipers to infer the rarity of a token)
  1. Minting tokens in a deterministic order (allows the snipers to infer the right time to mint the rare token)

You can fix the first problem by revealing the metadata only after the token has been minted (more here). Or you can use batched gradual reveals. All on-chain data is bound to be read and exploited. So don’t verify your contract until just before mint begins.

The second problem can be fixed by randomizing the mint order. On-chain randomization is hard. Ethereum does not have a built-in random number generator so people have been using all kinds of tricks like using the current block number as the seed and/or combining it with the minter address for additional randomness. These types of tricks are easily fooled by advanced snipers because it’s not true randomness.

You can use an oracle for randomization (Chainlink), but even then, advanced snipers can get around it by “peeking” NFTs and reverting the transactions if the minted NFT turns out to be not rare (example here). So, there is no 100% way to get around the second problem, unfortunately. One thing you could do is add allowlists but this will only work if the entire NFT collection can be restricted to an allowlisted community.

Ethereum is really a dark forest and if you’re not careful, you will be sniped. Read more about NFT sniping attacks here.

Misc

  • Make your contract be able to withdraw any ERC-721 and ERC-20: Most of the contracts just implement ETH withdrawing functionality and forget about ERC-721s and ERC-20s. But sometimes people send arbitrary tokens to contracts either by mistake or who knows why. Add an ability to withdraw them so that they are not stuck in your contract. (For an example implementation, check the Crypto Coven contract)
  • Make your data immutable: Either create your NFTs on-chain or use a provenance hash if using off-chain rendering.
  • Pre-approve OpenSea for 0 fee listing: (outdated since Seaport). You used to be able to pre-approve the OpenSea contract so that your NFT holders don’t need to call setApproval. But with the introduction of Seaport, this is no longer necessary. (For an example implementation, check the Crypto Coven contract)

That’s it! Let me know if I’m missing anything by commenting on my tweet. Appreciate it!