Benchmarking a Historical ERC-721 Architecture
Revisiting a Solidity architecture I worked on in 2021 and measuring its gas trade-offs against OpenZeppelin ERC721Enumerable.
In 2021, I worked on an ERC-721 implementation designed to reduce one of the largest costs in NFT minting: persistent writes to blockchain state.
The context matters. That was the year NFT collections reached a much larger audience, and every mint competed for space in Ethereum blocks. A user paid not only for the item, but also for the amount of computation and storage the contract required from the EVM. On Ethereum, gas is the unit used to measure that effort; the actual fee combines the gas consumed with the price per unit of gas.1
Many projects adopted OpenZeppelin's ERC721Enumerable. It offered an important convenience: discovering every existing token or enumerating an address's tokens directly through the contract. We took a different approach—sequential IDs, less bookkeeping during state-changing operations, and enumeration derived at read time.
Years later, I recovered the implementation and rebuilt a controlled environment to answer a more rigorous question than “was the contract cheap?”:
How much of the saving came from removing Enumerable, how much came from the sequential model, and what did that trade-off actually cost?
What ERC-721 actually requires
ERC-721 (opens in a new tab) standardizes an interface for non-fungible tokens: ownership, transfers, approvals, and events that let other applications recognize and move unique assets. The pair formed by a contract address and a tokenId identifies each NFT.2
Full enumeration, however, is not part of the required core. The specification defines ERC721Enumerable as an optional extension made up of totalSupply, tokenByIndex, and tokenOfOwnerByIndex. A contract can therefore be ERC-721 compliant without maintaining on-chain indexes for those queries. For broader historical context, see the Wikipedia article on ERC-721 (opens in a new tab); the EIP remains the normative technical source.
That distinction is the center of this benchmark. It does not compare a valid ERC-721 with an “incomplete” contract. It compares different strategies for providing—or deriving—enumeration.
Three architectures, not merely two
The benchmark pins Solidity 0.8.10, optimizer at 200 runs, OpenZeppelin Contracts v4.4.1, and Foundry. Every scenario measures one action and deploys fresh instances in setUp, preventing state from one test from contaminating another.
The three implementations are:
- OpenZeppelin ERC721Enumerable — the conventional enumerable baseline.
- OpenZeppelin sequential ERC721 — a control without
Enumerable, still based on OpenZeppelin's conservative core. - Historical ERC721S — a minimal reconstruction of the sequential ownership architecture I used in production in 2021.
The middle control is essential. Without it, every difference could be attributed to removing ERC721Enumerable. With it, the cost of enumerable bookkeeping can be separated from the effect of the historical storage model.
Where Enumerable spends gas
In version 4.4.1, ERC721Enumerable.sol (opens in a new tab) maintains four additional structures:3
- a mapping from owner and index to owned token;
- a mapping from token to its position in the owner's list;
- a global array containing every token ID;
- a mapping from token to its position in that global array.
The _beforeTokenTransfer hook updates these structures during mints, transfers, and burns. This makes enumerable queries direct and predictably priced, but it also makes write operations update more storage.
That detail matters economically because storage persists across transactions and is replicated by the network. Ethereum's documentation notes, for example, that writing a 32-byte word into a previously empty slot can cost 22,100 gas under the described schedule.4 That figure should not be multiplied naively to predict a complete transaction—warm and cold slots, refunds, and other effects matter—but it explains why avoiding redundant state can produce measurable differences.
The historical model
HistoricalERC721S stores ownership in one array:
address[] internal _owners;
function _mint(address to) internal returns (uint256 tokenId) {
tokenId = _owners.length;
_owners.push(to);
emit Transfer(address(0), to, tokenId);
}The array index is the tokenId. There is no separate ID counter, balance mapping, per-owner list, or auxiliary global index. ownerOf(tokenId) is a direct read; balanceOf, totalSupply, tokenByIndex, and tokenOfOwnerByIndex scan _owners when queried.
This moves work from a paid, persistent operation into view calls. When a wallet or indexer executes those reads off-chain, they create no transaction and charge no gas to the user. The computation does not disappear, though: if another contract invokes those functions on-chain, or if the collection grows too large, the linear scan becomes a real limitation.
Results
| Operation | OZ Enumerable | OZ Sequential | Historical ERC721S | Reduction vs Enumerable |
|---|---|---|---|---|
| Mint 1 | 109,237 | 77,445 | 54,792 | 49.84% |
| Mint 5 | 568,912 | 179,366 | 153,680 | 72.99% |
| Mint 10 | 1,143,463 | 306,737 | 277,310 | 75.75% |
transferFrom | 65,540 | 57,381 | 39,029 | 40.45% |
The clearest result appears in the batch scenario: as quantity grows, the enumerable baseline repeats more bookkeeping for each token. For a 10-token mint, the historical implementation consumes 277,310 gas against the baseline's 1,143,463—a 75.75% reduction in this controlled scenario.
Removing ERC721Enumerable already explains much of the saving: the OZ Sequential control falls to 306,737 gas for a 10-token mint. The historical model remains below that control, however. For a one-token mint, it uses another 29.25% less gas than the non-enumerable baseline.
Deployment points in the same direction:
| Contract | Deployment gas | Bytecode size |
|---|---|---|
| OZ Enumerable | 1,445,971 | 6,605 bytes |
| OZ Sequential | 1,220,918 | 5,563 bytes |
| Historical ERC721S | 1,024,755 | 4,531 bytes |
That is 29.13% less deployment gas than the enumerable baseline and 2,074 fewer bytes of bytecode.
Reading the numbers without overselling them
This is an architecture benchmark, not a promise that any NFT transaction will be 75.75% cheaper. Payments, allowlists, royalties, metadata, reveal mechanics, marketplace integrations, and collection-specific rules are deliberately excluded. Including them would blur the ownership model's cost with product logic.
These values should also not be compared directly with measurements produced by a different compiler, OpenZeppelin revision, or EVM hard fork. Solidity's optimizer trades deployment bytecode size against execution cost according to its runs setting, while EVM changes can reprice opcodes.5
Foundry provides per-contract and per-function gas reports as well as versionable snapshots for detecting regressions.6 Here, the results come from isolated scenarios and a pinned historical environment precisely so that the comparison can be reproduced.
The trade-off the table cannot show
ERC721Enumerable buys cheap queries with more expensive writes. The historical model buys cheaper mints and transfers by accepting linear-time queries. Neither choice is universally superior.
The sequential architecture is a reasonable fit when:
- IDs are contiguous and controlled by the contract;
- burns are absent or handled deliberately;
- most enumeration happens off-chain;
- mint transaction cost is a priority;
- indexers and events can serve interface queries.
It is a poorer fit when other contracts must enumerate large collections on-chain, arbitrary IDs are required, or linear complexity can make a call impractical. EIP-721 itself warns that loops with growing cost may undermine scalability; moving a loop into a view function changes who pays and where it runs, but not its complexity.2
Code, history, and reproduction
The ERC-721 Gas Benchmark repository (opens in a new tab) contains all three contracts, tests, compiler configuration, and commands to reproduce the experiment:
forge test -vv
forge test --gas-reportThe benchmark contract is a minimal reconstruction of the architecture, not a complete copy of the product. The verified 2021 production contract on Mainnet (opens in a new tab) preserves historical context and attribution; features unrelated to the experiment were removed from the reproducible version.
Revisiting the old code was a reminder that optimization is rarely one isolated trick. It is a decision about where computation should happen, which state truly needs to exist, and which guarantees the workload requires.
Footnotes
-
Ethereum.org, “Gas and fees” (opens in a new tab), a technical overview of gas, fees, and transaction execution on Ethereum. ↩
-
Entriken et al., “ERC-721: Non-Fungible Token Standard” (opens in a new tab), including the optional enumeration extension and its gas and complexity discussion. ↩ ↩2
-
OpenZeppelin Contracts v4.4.1,
ERC721Enumerable.sol(opens in a new tab), enumeration structures and transfer hooks. ↩ -
Ethereum.org, “Blockchain data storage strategies” (opens in a new tab), including an example cost for writing a new storage slot. ↩
-
Solidity, “Using the compiler” (opens in a new tab), optimizer behavior and the
runssetting. ↩ -
Foundry, “Gas reports” (opens in a new tab), official Forge gas-report documentation. ↩