How to use ERC721A: the complete guide

Where does ERC721A come from?

ERC721A is a smart contract that appeared first when Azuki collection went out.

The contract was a bit revolutionary at this time and allowed to save a massive amount of gas during the mint process. Since then, they decided to open source the project.
Of course, anyone could copy/paste the original contract, but it opened the door to many improvements that would be shared with the community.

Why you should use ERC721A

ERC721A is not a new standard but a new and pretty innovative implementation of the EIP-721 standard. It provides full support for ERC721Metadata as well.

The main focus was to reduce significantly gas fees for NFT collections during the mint process. And it’s pretty efficient.

They use several techniques to save gas, not only during the mint process. One of them is the use of custom errors.

You’ll find many comparisons of ERC721A vs OpenZeppelin’s ERC721Enumerable. This is pretty unfair since ERC721Enumerable provides more features, then stores more data. So I updated the comparison with the latest version of ERC721A vs OpenZeppelin’s ERC721 vs ERC721Enumerable.

OperationERC721AOZ’s ERC721OZ’s ERC721Enumerable
Mint 1 token90,44368,654140,093
Mint 5 tokens98,239169,534598,417
Mint 20 tokens127,474547,8352,317,133

Unless your project only authorizes one single item per mint, the ERC721A wins in all other cases.

If gas fees are optimized during mint, what for transfers?

OperationERC721AOZ’s ERC721ERC721Enumerable
Transfer 1 tokenfrom 86,322
up to 110,475
62,17596,372
Subsequent transfer61,23157,37583,290

Wait a minute. Why is the cost variable for an ERC721A transfer?
Simply because the cost depends on the position of the token. It’s a bit tricky and will explain that in more detail in a further article.
Please note that the cost is affected on the first transfer and is lower on subsequent transfers.
On average, it’s still better than ERC721Enumerable, and around 50% more than the basic OpenZeppelin implementation.
Compared to it, the ERC721A implementation mostly delays gas consumption. It’s a kind of lazy minting. You only use the gas if really required. And that makes a huge difference during the mint process: it avoids a gas war if many people want to mint at the same time. And if the collection dies, yeah, that happens, the gas will be saved forever.

The ERC721A is really optimized for art / PFP collections with massive mint phases. In other cases, like a P2E game, it might be worth looking at the basic OpenZeppelin implementation.

When not to use ERC721A

Given its particular way of storing data, NFT ids MUST be sequential. For most NFT collections, this won’t be an issue. But if you want to generate the ID or have some non-sequential ids, this will be a problem.

For example in a game, if the id is generated during the minting process, or if you want to have different families in your collection, then you can’t use an ERC721A implementation.

One other reason is if you mint items one by one: the gas saving won’t be that efficient.

But please, unless you really need to know the list of tokens belonging to a specific address on-chain, meaning from another contract, please don’t use ERC721Enumerable.
Yeah, I know, it’s a bit more work on the front end if you want to display the list of tokens owned by someone: Use events instead!

ERC721A code sample

Setup

npm install --save-dev erc721a

Sample

Do you want to use it? Here is a very simple implementation:

import 'erc721a/contracts/ERC721A.sol';

contract SimpleERC721 is ERC721A {
  constructor()
    ERC721A("Simple ERC721","SIMPLERC721")
  {}

  function mint(uint256 count) external {
    _safeMint(msg.sender, count);
  }
}

Hey, be careful, this is not production ready : anyone can mint as many items he wants for a low gas fee. I guess you’ll add some restrictions around it.

Special features

The ERC721A has some very nice features embedded. If you need more details, you can read the official documentation.

_startTokenId

Unless you explicitly define it otherwise, the token ids will start at 0.
Why is that? Here is the answer :

Why tokenId start at 0 ?
To value dev job.

Here is a (more serious?) thread about why starting at 0.
But if you prefer your ids to start at 1, like SahalAmeen & I do, simply override the _startTokenId() method in your contract:

function _startTokenId() internal view virtual override returns (uint256) {
    return 1;
}

startTimestamp

‘startTimestamp’ is an attribute you can get for each token. It stores the date on the last transfer and allows to count how long it was held.

It’s available through the ‘_ownershipOf’ method. Since it’s internal, be sure to make it available through one of your methods if you want to make it available.

It can be useful to reward holders. For example, imagine you want to expose how long a token was held :

function holdDuration(uint256 tokenId) external view returns (uint256) {
  return block.timestamp - _ownershipOf(tokenId).startTimestamp;
}

Natively protected against Reentrancy

The _safeMint function is supposed to check if the destination address is able to receive an ERC721. But it’s not necessarily safe from a security perspective.
By the way, the OpenZeppelin does not protect against reentrancy. You’ll have to handle it yourself.
Whereas if you use the ERC721A _safeMint, it’s protected by default from reentrancy attacks.

Aux data field

You can read and write a uint64 field on each owner using this method:

function _setAux(address owner, uint64 aux) internal
function _getAux(address owner) internal view returns (uint64)

It can be very useful if you want to store data related to the holder, like the number of free mints.
Please note it’s internal: you’ll have to make it available in case you want to read it from outside the contract.