Idioma

Benchmark de uma Arquitetura ERC-721 Histórica

Revisitando uma arquitetura Solidity em que trabalhei em 2021 e medindo seus trade-offs de gas contra o OpenZeppelin ERC721Enumerable.

Código do projeto ↗

7 min de leitura

Em 2021, trabalhei em uma implementação de ERC-721 criada para reduzir um dos maiores custos do mint de NFTs: escritas persistentes no estado da blockchain.

O contexto importa. Aquele foi o ano em que coleções de NFTs chegaram a um público muito maior, e cada operação de mint disputava espaço em blocos da Ethereum. O usuário não pagava apenas pelo item: pagava também pela quantidade de trabalho e armazenamento que o contrato exigia da EVM. Em Ethereum, gas é a unidade que mede esse esforço computacional; o valor efetivamente pago combina o gas consumido com o preço por unidade de gas.1

Muitos projetos adotavam o ERC721Enumerable da OpenZeppelin. Ele oferecia uma conveniência importante: descobrir todos os tokens existentes ou enumerar os tokens de um endereço diretamente pelo contrato. Nós seguimos outra abordagem: IDs sequenciais, menos bookkeeping durante operações que alteram estado e enumeração calculada no momento da leitura.

Anos depois, recuperei a implementação e reconstruí um ambiente controlado para responder a uma pergunta mais rigorosa do que “o contrato era barato?”:

Quanto da economia vinha de remover o Enumerable, quanto vinha do modelo sequencial e qual era o custo real desse trade-off?

O que o ERC-721 realmente exige

O ERC-721 (abre em uma nova aba) padroniza uma interface para tokens não fungíveis: ownership, transferências, aprovações e eventos que permitem a outras aplicações reconhecer e movimentar ativos únicos. O par formado pelo endereço do contrato e pelo tokenId identifica cada NFT.2

A enumeração completa, porém, não pertence ao núcleo obrigatório. A própria especificação define ERC721Enumerable como uma extensão opcional, composta por totalSupply, tokenByIndex e tokenOfOwnerByIndex. Isso significa que um contrato pode ser compatível com ERC-721 sem manter índices on-chain para responder a essas consultas. Para uma visão histórica e introdutória do padrão, há também o verbete ERC-721 na Wikipedia (abre em uma nova aba), mas a especificação EIP é a referência normativa.

Essa distinção é o centro do benchmark. Não estamos comparando um ERC-721 válido com um contrato “incompleto”; estamos comparando estratégias diferentes para oferecer — ou derivar — capacidade de enumeração.

Três arquiteturas, não apenas duas

O benchmark fixa Solidity 0.8.10, optimizer com 200 runs, OpenZeppelin Contracts v4.4.1 e Foundry. Cada cenário mede uma única ação e cria novas instâncias no setUp, evitando que estado de um teste contamine o seguinte.

As três implementações são:

  1. OpenZeppelin ERC721Enumerable — baseline enumerável convencional.
  2. OpenZeppelin ERC721 sequencial — controle sem Enumerable, mas ainda baseado no núcleo conservador da OpenZeppelin.
  3. Historical ERC721S — reconstrução mínima da arquitetura sequencial de ownership que usei em produção em 2021.

O controle intermediário é essencial. Sem ele, toda diferença poderia ser atribuída simplesmente à remoção do ERC721Enumerable. Com ele, conseguimos separar o custo da extensão enumerável do efeito produzido pelo modelo histórico de armazenamento.

Onde o Enumerable gasta

Na versão 4.4.1, o ERC721Enumerable.sol (abre em uma nova aba) mantém quatro estruturas adicionais:3

  • um mapping do proprietário e índice para o token possuído;
  • um mapping do token para sua posição na lista do proprietário;
  • um array global com todos os token IDs;
  • um mapping do token para sua posição nesse array global.

O hook _beforeTokenTransfer atualiza essas estruturas durante mint, transferência e burn. Isso torna as consultas enumeráveis diretas e com custo previsível, mas faz operações de escrita executarem mais atualizações de storage.

Esse detalhe é economicamente relevante porque storage persiste entre transações e é replicado pela rede. A documentação da Ethereum mostra, por exemplo, que gravar uma palavra de 32 bytes em um slot anteriormente vazio pode custar 22.100 gas no regime descrito.4 Esse número não deve ser multiplicado ingenuamente para prever uma transação inteira — existem slots quentes e frios, refunds e outros efeitos —, mas explica por que evitar estado redundante costuma produzir diferenças mensuráveis.

O modelo histórico

O HistoricalERC721S armazena ownership em um único array:

Solidity
address[] internal _owners;
 
function _mint(address to) internal returns (uint256 tokenId) {
    tokenId = _owners.length;
    _owners.push(to);
    emit Transfer(address(0), to, tokenId);
}

O índice do array é o próprio tokenId. Não há um contador separado de IDs, um mapping de balances, listas por proprietário ou índices globais auxiliares. ownerOf(tokenId) é uma leitura direta; balanceOf, totalSupply, tokenByIndex e tokenOfOwnerByIndex percorrem _owners quando consultados.

Isso desloca trabalho de uma operação paga e persistente para chamadas view. Quando executadas off-chain por uma carteira ou indexador, essas leituras não criam uma transação nem cobram gas do usuário. Mas o custo computacional não desaparece: se outro contrato chamar essas funções on-chain, ou se a coleção crescer demais, a varredura linear passa a ser uma limitação real.

Resultados

OperaçãoOZ EnumerableOZ SequentialHistorical ERC721SRedução vs Enumerable
Mint 1109.23777.44554.79249,84%
Mint 5568.912179.366153.68072,99%
Mint 101.143.463306.737277.31075,75%
transferFrom65.54057.38139.02940,45%

O resultado mais expressivo aparece no batch: conforme a quantidade cresce, o baseline enumerável repete mais bookkeeping por token. No mint de 10 unidades, a implementação histórica consome 277.310 gas contra 1.143.463 do baseline, uma redução de 75,75% neste cenário controlado.

Remover ERC721Enumerable já explica uma parte grande da economia: o controle OZ Sequential cai para 306.737 gas no mint de 10. Ainda assim, a arquitetura histórica permanece abaixo dele. No mint unitário, por exemplo, ela reduz mais 29,25% em relação ao controle não enumerável.

O deploy apresenta a mesma direção:

ContratoGas de deployTamanho do bytecode
OZ Enumerable1.445.9716.605 bytes
OZ Sequential1.220.9185.563 bytes
Historical ERC721S1.024.7554.531 bytes

Isso representa 29,13% menos gas de deploy que o baseline enumerável e um bytecode 2.074 bytes menor.

Como ler os números sem exagerá-los

Este é um benchmark de arquitetura, não uma promessa de que qualquer NFT custaria 75,75% menos. Pagamentos, allowlists, royalties, metadata, reveal, integrações de marketplace e regras específicas de coleção foram excluídos deliberadamente. Incluí-los confundiria o custo do modelo de ownership com lógica de produto.

Também não se deve comparar estes valores diretamente com medições feitas usando outra versão do compilador, outra revisão da OpenZeppelin ou outro hard fork. O optimizer do Solidity troca tamanho de bytecode por custo de execução de acordo com o parâmetro runs, e mudanças na EVM podem alterar a precificação de opcodes.5

O Foundry fornece relatórios de gas por contrato e função, além de snapshots que podem ser versionados para detectar regressões.6 Neste projeto, os resultados vêm de cenários isolados e o ambiente histórico está fixado justamente para tornar a comparação reproduzível.

O trade-off que os números não mostram

O ERC721Enumerable compra consultas baratas com escritas mais caras. O modelo histórico compra mints e transferências mais baratos aceitando consultas lineares. Nenhuma escolha é universalmente superior.

A arquitetura sequencial faz sentido quando:

  • IDs são contíguos e controlados pelo contrato;
  • burns são inexistentes ou tratados conscientemente;
  • a maior parte da enumeração acontece off-chain;
  • o custo das transações de mint é prioridade;
  • indexadores e eventos podem atender consultas de interface.

Ela é menos apropriada quando outros contratos precisam enumerar grandes coleções on-chain, quando IDs arbitrários são um requisito ou quando a complexidade linear pode tornar uma chamada impraticável. A própria EIP-721 alerta que loops com custo crescente podem comprometer escalabilidade; mover um loop para uma função view muda quem paga e onde ela roda, mas não muda sua complexidade.2

Código, história e reprodução

O repositório ERC-721 Gas Benchmark (abre em uma nova aba) contém os três contratos, testes, configuração do compilador e instruções para executar:

Shell
forge test -vv
forge test --gas-report

O contrato de benchmark é uma reconstrução mínima da arquitetura, não uma cópia integral do produto. O contrato de produção de 2021 verificado na Mainnet (abre em uma nova aba) preserva o contexto histórico e a atribuição; recursos sem relação com o experimento foram removidos da versão reproduzível.

Revisitar esse código antigo foi um lembrete de que otimização raramente é um truque isolado. Ela é uma decisão sobre onde a computação deve acontecer, qual estado realmente precisa existir e quais garantias o workload exige.

Footnotes

  1. Ethereum.org, “Gas and fees” (abre em uma nova aba): visão técnica de gas, taxas e execução de transações na Ethereum. ↩

  2. Entriken et al., “ERC-721: Non-Fungible Token Standard” (abre em uma nova aba), incluindo a extensão opcional de enumeração e a seção sobre gas e complexidade. ↩ ↩2

  3. OpenZeppelin Contracts v4.4.1, ERC721Enumerable.sol (abre em uma nova aba), estruturas e hooks usados para enumeração. ↩

  4. Ethereum.org, “Blockchain data storage strategies” (abre em uma nova aba), exemplo do custo de escrita em um slot novo. ↩

  5. Solidity, “Using the compiler” (abre em uma nova aba), documentação do optimizer e do parâmetro runs. ↩

  6. Foundry, “Gas reports” (abre em uma nova aba), documentação oficial dos relatórios de gas do Forge. ↩

SolidityEthereumERC-721Benchmark