NFT contracts explained

lunaray
8 min readJul 6, 2022

--

We considered use cases of NFTs being owned and transacted by individuals as well as consignment to third-party brokers/wallets/auctioneers (“operators”). NFTs can represent ownership over digital or physical assets. We considered a diverse universe of assets, and we know you will dream up many more:

  • Physical property — houses, unique artwork
  • Virtual collectibles — unique pictures of kittens, collectible cards
  • “Negative value” assets — loans, burdens, and other responsibilities

In general, all houses are distinct and no two kittens are alike. NFTs are distinguishable and you must track the ownership of each one separately.

A standard interface allows wallet/broker/auction applications to work with any NFT on Ethereum.

This standard is inspired by the ERC-20 token standard and builds on two years of experience since EIP-20 was created. EIP-20 is insufficient for tracking NFTs because each asset is distinct (non-fungible) whereas each of a quantity of tokens is identical (fungible).

Most NFT contracts are the most common patterns as below:

  • 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:

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, and 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 have updated in before/after transfer hooks). The code above is (wrongly) optimized for reading functions while it should have been optimized for write functions (because read functions are mostly free). Most contract developers are too lazy and inherit all 3 interfaces from OpenZeppelin. But you can do better.

Solution: If the only function you need from Enumerable is totalSupply, then you can 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)

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

  • Crypto Coven
  • Azuki

Use ERC721A for efficient batch mints

Gas prices on Ethereum have been consistently high for months, and the dev community needs to adapt. When popular NFT projects begin to mint, gas prices spike up, resulting in the entire ecosystem paying millions in gas fees to transact. 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.

Instead of using OpenZeppelin’s popular default implementations of IERC721 and IERC721Enumerable, we’ve written our version (which we’ll refer to as ERC721A for the rest of this post) and are excited to announce that the Azuki contract will enable minting multiple NFTs for essentially the exact gas cost of minting a single NFT.

Realizing that there are some possible optimizations for batch minting, the Azuki team created ERC721A — an implementation of ERC721 optimized for batch minting.

MEASUREMENTS

Below are measured the gas costs and prices for minting, comparing OpenZeppelin’s ERC721Enumerable vs ERC721A. In the measurements, the same application-level logic is used, the only difference being the _safeMint function called.

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. was not part of the original ERC721A)

if you want to know more information about it please click below:

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.

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

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:

Protect against bots

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

  1. Limit mints per wallet
  2. 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 information : https://ethereum.stackexchange.com/questions/1891/whats-the-difference-between-msg-sender-and-tx-origin/1892#1892

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.

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

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

Summary

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

reference :https://www.solidnoob.com/blog/good-nft-contract-patterns

https://www.azuki.com/erc721a

--

--

lunaray

Lunaray takes a leading position in smart contract auditing and consulting service for blockchain security.