Your guide to ERC721A (July 2022)

Tom Lehman (middlemarch.eth)
7 min readJul 1, 2022

--

How does the ERC721A contract work? How do I use it? What are the HIDDEN GEMS within it I can use to get Free Money??

ERC721 is the most common standard for NFTs (non-fungible tokens). It governs how NFT contracts must behave, but now how NFT contracts should implement this behavior.

For a long time the OpenZeppelin ERC721 implementation was synonymous with the ERC721 standard. ERC721A, which launched in Jan 2022, was the first mainstream contract to offer a real alternative approach.

How does ERC721A work? How is it different from OpenZeppelin ERC721?

The main idea behind ERC721A is to provide low-gas minting at the cost of making token transfers more expensive. It accomplished this by reimagining how token ownership is stored.

Token mints in ERC721A

Here’s an example. When you mint 10 tokens on an OpenZeppelin contract, you write down that you own each token individually. By contrast, when you mint 10 tokens with ERC721A, you store only that you own the *first* token you minted:

Ownership storage: OpenZeppelin v. ERC721A

Storing 10x fewer addresses obviously makes minting a lot cheaper. But how do you determine who owns (for example) token #6?

With OZ it’s easy: you look up the entry for token #6 in the table and see middlemarch.eth next to it. With ERC721A, however, this entry is blank. What now?

Who owns token #6? OpenZeppelin v. ERC721A

To figure out the owner of #6 you must count backwards, checking each entry in the table until you find one that is non-blank.

In this case you would check 5, check 4, 3, 2, 1, and 0 where you finally observe a non-blank ownership entry (middlemarch.eth). This address is the owner of token #6.

This extra work makes determining who owns a token more expensive—going from O(1) in the OZ case to O(mintBatchSize) in the ERC721A case.

When someone mints a lot of tokens at once this becomes quite significant. To use an extreme example, if someone mints 10,000 tokens at once it could cost over 1 eth to determine who owns the 10,000th token!

In many cases ownerOf() is called in a view context and is therefore free. However, it is called in a write context in an important use-case: transferring a token.

Token transfers in ERC721A

Transferring token #6 in ERC721A is doubly expensive. First, you have to figure out who owns token #6 as described above. Then you must make two writes: one to update the ownership of token #6, and another to update the ownership of token #7 (if it is blank):

This is because blank ownership means “count down to the first non-blank ownership and that is the owner” and the first non-blank ownership for token #7 has changed from middlemarch.eth to vitalik.eth, even though #7 is still owned by middlemarch.eth.

Should I use ERC721A?

ERC721A defers the initialization of token ownership from minting to transferring. ERC721A also costs more gas overall. Does this translate into higher or lower fees in eth? As always with trade-offs, “it depends.” But here are some considerations.

Consideration 1: Will gas prices be higher during mints or transfers?

For competitive mints, users might not have the option to wait and mint when gas is low. Hyper-competitive mints might even themselves affect overall Ethereum gas fees making it *impossible* to mint for less. To the extent gas costs will be lower during transfers than mints, ERC721A becomes more attractive.

The ERC721A docs uses this example:

Consideration 2: Will your NFT be worth more than zero?

NFTs that are worth zero do not incur transfer fees because no one will pay gas to get them! However, you still have to pay to mint something that is worth zero. If your project is just for fun, or you’re not sure how it will perform, ERC721A could be attractive as it makes users spend more gas only in the case the NFT is worth something.

Consideration 3: Do you like free money?

Beyond the main “cheaper mints, more expensive transfers” idea, ERC721A contains several completely independent innovations.

These are features I would love to see in more general ERC721 implementations, and ones you can use in ERC721A even if you want to opt out of its main idea by minting in batches of 1!

ERC721A Tips, Tricks, and free money!

Now that we’ve covered ERC721A basics and trade-offs, how can we mitigate downsides and even find opportunities for zero tradeoff free money in ERC721A? Read on and also read ERC721A tips page!

Tip 1: Choose reasonable batch sizes for your users

Minting a lot of tokens at once makes transferring extremely expensive. One solution here is to limit the amount a user can mint in a single transaction. However, I don’t consider this option very fun.

If you want to allow users to mint as many as they want in a single transaction, you should mint in batches like this:

This allows the user to mint an unlimited number of tokens without ever requiring more than 10 lookups to determine token ownership in the future.

Free money through more efficient data storage

How can ERC721A make you free money? In addition to deferring mint costs, ERC721A is cleverer than OpenZeppelin’s implementation in terms of how it stores data.

For example, OZ stores the number of NFTs each user owns in a 256 bit integer:

This line of code seems innocuous but it actually contains a controversial assumption: that your contract needs to support a user owning up to 2 ^ 256 — 1tokens. That’s more tokens than there are atoms in the known universe!

Removing this requirement allows ERC721A to store more useful information in the same 256 bits. Specifically, ERC721A uses 64 bits for the user’s balance and the remaining 192 bits to store:

  • Number of tokens minted by the address
  • Number of tokens burned by the address
  • 64 bits of “Aux Data” for whatever the dev wants

Tip 2: use _numberMinted(address)

How can you use this info? Suppose you want to allow only one mint per wallet, like goblintown.wtf. Instead of using a separate mapping, you can use the ERC721A built-in _numberMinted(address):

Tip 3: use _setAux(address)

Suppose users can mint as many as they like but each wallet only gets one for free. You can use aux data for this (as suggested in the Tweet that inspired this article):

The benefit here comes from the fact that minting and setting aux data both update the same storage slot which is 20k gas more efficient than updating two separate slots as is the case with the separate freeMintClaimed mapping.

Tip 4: use _extraData() for generative art

The next enhancement comes from optimizing the way OZ stores ownership information. Here I’m not referring to the idea of writing ownership info lazily, but rather to what data is stored when it is written.

OpenZeppelin stores ownership information like this:

This is simple but wasteful. An address is 160 bits, but storing it costs the same as storing 256 bits of information. This leaves 96 bits on the table. What does ERC721A do with these 96 bits? Most interestingly:

  • The timestamp of the last transfer
  • 24 bits of “Extra Data” for whatever you want!

I’m actually not sure why ERC721A stores the time the current owner received the token. This information allows you to, for example, reward long-term holders with a cheaper price in another mint, but use-cases like this seem rather niche to justify the 64 bits of data it requires.

This is doubly true as the timestamp concept could be captured with the more general and IMO more useful “extra data” concept. Extra data allows you to store 24 bits of data on a per-batch basis, meaning that any two tokens that share the same ownership record will share the same “extra data.”

How might this be useful? The original implementer was motivated by storing seeds for generative art. Here is the basic idea:

  1. User mints 5 NFTs (for example).
  2. Contract stores blockchain state like block.timestamp as extraData in the ownership record corresponding to the batch.
  3. To render the NFT, generate a unique seed by pulling the ownership record for the token, grabbing the extra data, and hashing it with the id of the token (because all tokens in the batch share the same extraData).
  4. Use this unique seed to generate the art.

Here’s how to do it, adapted from Ethereal States, programmed by the legendary sfremaux / dievardump.

Note that your mint() function doesn’t need to be aware of extraData as _extraData() will be called internally by ERC721A in _mint().

And that’s it! All these “hidden gems” can be used independently from the main batch minting use-case. If you want OZ-esque minting and transferring behavior you can mint in 1 token batches or call _initializeOwnershipAt() on every id minted in a batch.

Crucial thanks goes out to dievardump and squeebo who taught me about all of this stuff!

--

--

Tom Lehman (middlemarch.eth)

Facet co-founder | Facet Discord: https://discord.gg/facet | Ethscriptions co-founder | @_capsule21 co-founder | http://Genius.com co-founder & former CEO