You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

4.0 KiB

Design

ERC721A enables a near constant gas cost for batch minting via a lazy-initialization mechanism.

Token IDs are minted in sequential order (e.g. 0, 1, 2, 3, ...).

Regardless of the quantity minted, the _mint function only performs 3 SSTORE operations:

  • Initialize the ownership slot at the starting token ID with the address.
  • Update the address' balance.
  • Update the next token ID.

A Transfer event will still be emitted for each NFT minted.
However, emitting an event is an order of magnitude cheaper than a SSTORE operation.

Lower Fees

ERC721A defers the initialization of token ownership slots from minting to transferring.

This allows users to batch mint with low, near-constant transaction fees, and pick a good time to transfer when the network's BASEFEE is lower.

Although this has a higher total gas cost (minting + transfers), it gives significantly lower overall transaction fees in practice.

Transaction Fee = (BASEFEE + Max Priority Per Gas) * Gas Used

Gas savings during high BASEFEE periods matter more.

As such, ERC721A prioritizes gas savings for the minting phase.

Benchmark

To illustrate, we compare OpenZeppelin's ERC721 with ERC721A.

ERC721 ERC721A
Batch Mint 5 Tokens 155949 gas 63748 gas
Transfer 5 Tokens 226655 gas 334450 gas
Mint BASEFEE 200 gwei 200 gwei
Transfer BASEFEE 40 gwei 40 gwei
Total Transaction Fees 0.0403 ether 0.0261 ether

Even for conservatively small batch sizes (e.g. 5), we can observe decent savings over the barebones implementation.

In practice, the Mint BASEFEE for ERC721 can be much higher.

When consecutive blocks hit the block gas limit, the BASEFEE increases exponentially.

First Transfer vs Subsequent Transfers

The main overhead of transferring a token only occurs during its very first transfer for an uninitialized slot.

ERC721 ERC721A
First transfer 45331 gas 92822 gas
Subsequent transfers 45331 gas 44499 gas

Here, we bulk mint 10 tokens, and compare the transfer costs of the 5th token in the batch.

To keep the cost of the SSTORE writing to the balance mapping constant, we ensure that the destination addresses have non-zero balances during all transfers.

The first transfer with ERC721A will incur the storage overheads:

  • 2 extra SSTOREs (initialize current slot and next slot, both of which are empty).
  • 5 extra SLOADs (read previous slots and next slot).

Balance Mapping

ERC721A maintains an internal mapping of address balances. This is an important and deliberate design decision:

  • The balanceOf function is required by the ERC721 standard.

    We understand that it is tempting to remove the mapping -- it can save a SSTORE during mints, and 2 SSTOREs during transfers.

    However, this degrades the balanceOf function to become O(n) in complexity -- it must bruteforce through the entire mapping of ownerships. This hampers on-chain interoperability and scalability.

  • While it is possible to emulate the balanceOf function by listening to emitted events, this requires users to use centralized indexing services.

    In the case of service disruption, the data can get out-of-sync and hard to reconstruct.

  • In the context of saving gas, we are able to allow whitelist minting achieve the same amount of SSTOREs when compared to implementations without the mapping. See ERC721A._getAux and ERC721A._setAux.

    The address balance mapping is also used to store the mint and burn counts per address with negligible overhead, which can be very useful for tokenomics.

In all, the address balance mapping gives a good balance of features and gas savings, which makes it desirable to keep.