O mercado NFT evoluiu muito desde o seu surgimento em 2018-19 até o seu pico máximo (até o momento) em 2021. E com ele, os padrões e tecnologias envolvendo NFT também evoluíram. A principal especificação para contratos de NFTs, a ERC-721, já foi profundamente estudada aqui no blog neste post e é leitura obrigatória para entender o artigo de hoje pois vamos falar de uma proposta de evolução da mesma.
Mas qual o problema com a ERC-721 que necessite de uma evolução?
Quando grandes coleções NFT são lançadas é comum que aconteça um grande hype no entorno das mesmas, principalmente quando grandes marcas e nomes estão envolvidos. E toda vez que um grande hype aparece, as blockchains sofrem do sucesso de tais projetos, com congestionamentos e a taxa de gás subindo nas alturas porque todo mundo quer ter um ou mais NFTs da nova coleção e conforme a rede vai congestionando, vai se tornando mais caro mintar, prejudicando o lançamento. Isso acaba obviamente se tornando um problema do ecossistema como um todo e não apenas do dono da coleção, pois fica caro e lento pra todo mundo.
Não apenas isso, mesmo em situações normais, o custo de gás é um vilão que deve sempre ser combatido em nossos projetos de NFT pois eles acabam prejudicando muito a experiência dos usuários e a evolução proposta pelo time da Azuki, uma famosa empresa de NFT e Metaverso, ajuda muito na economia de gás em projetos, principalmente os que permitam e/ou encorajam a aquisição de várias NFTs. Neste tipo de cenário pode-se economizar muito gás (mais de 78k de economia por mint) se você tiver algum mecanismo que permita mintar mais de um NFT por transação usando de apenas três otimizações no contrato ERC-721 da OpenZeppelin em uma abordagem que eles denominaram como ERC-721A (A de Azuki). Esta não é uma ERC oficial, mas uma variação da ERC feita pela “comunidade” e validada por especialistas, o que a tem tornado muito popular e fornece benefícios mesmo para o minting de apenas uma NFT como explicarei a seguir.
ERC-721A na Teoria
Antes de usarmos na prática a proposta da Azuki, é importante entender o que ele tem de diferente em relação à implementação ERC721Enumerable da OpenZeppelin, a forma mais popular de desenvolver contratos NFT atualmente. São três as melhorias que o time da Azuki implementou no contrato deles de ERC721Enumerable, a saber:
Otimização 1 – Remover armazenamento duplicado
A implementação da IERC721Enumerable pela OpenZeppelin inclui armazenamento redundante dos metadados de cada token. Esta abordagem desnormalizada permite otimizar a leitura de informações a um custo significativamente maior nas escritas, o que não é ideal dado que os usuários geralmente não terão de pagar pelas calls/leituras. Adicionalmente, o fato de que os tokens da Azuki (e de vários outros projetos) possuem ids incrementais iniciando em zero permite remover também alguns armazenamentos redundantes da implementação base tendo a premissa de que id é igual ao índice do token na coleção.
Otimização 2 – Atualizar o saldo do owner apenas uma vez por batch mint
Suponha que Alice tem 2 tokens e quer comprar mais 5. Em Solidity, custa gás para atualizar um valor armazenado na blockchain. No entanto, se estamos rastreando quantos tokens Alice possui, é mais barato atualizar o saldo de Alice de 2 para 7 com um único update ao invés de incrementar o saldo 5 vezes. Embora este seja um conceito relativamente simples, a maioria dos bulk mints no mercado NFT não adotou esta otimização porque ela não está incluída na implementação padrão da OpenZeppelin.
Otimização 3 – Atualizar os dados do owner apenas uma vez por batch mint
Esta otimização é similar à ideia da anterior. Suponha que a Alice quer comprar 3 tokens – token #100, #101 e #102. Ao invés de salvar a Alice como owner de cada um deles, totalizando 3 updates, nós podemos salvar que a Alice é owner apenas uma vez, de alguma forma que seja entendido que os 3 ids estão inclusos nesta alteração.
Como? Suponha que Alice faça o mint dos tokens #100, #101, e #102, enquanto que o Bob faz o mint dos tokens #103 e #104. O registro interno de owner se parecerá com isso:
- Token #100: owner=Alice
- Token #101: owner=<none>
- Token #102: owner=<none>
- Token #103: owner=Bob
- Token #104: owner=<none>
Assim, se você quiser saber o owner do token #101, deverá decrementar o id até encontrar o último owner preenchido, sendo a mudança mais significativa na função ownerOf, conforme imagem abaixo criada pelo próprio time mantenedor do projeto.
Esta otimização envolve algumas lógicas adicionais além dessa, especialmente no que tange transferências, já que uma mudança de dono exigirá escritas adicionais para preencher as lacunas de dono, mas a ideia geral é essa.
Com essas três otimizações implementadas, o cálculo de economia de gás estimado é o seguinte, entendendo que quanto mais tokens mintados no mesmo lote, maior a economia:
- 1 token: 78.124 de economia de gás;
- 2 tokens: 191.520
- 3 tokens: 303.916
- 4 tokens: 418.312
- 5 tokens: 531.708
Repare como não é uma progressão aritmética, mas sim geométrica, com a economia sendo cada vez maior conforme o tamanho do batch mint aumenta.
ERC721-A na Prática
Era 12 de janeiro de 2022 quando o time da Azuki liberou para a venda uma de suas famosas coleções, contendo 8700 tokens. Todos os tokens foram mintados em questão de minutos e para alegria do time e demais usuários da rede, não afetou o fee base da rede. Era a prova de que o ERC-721A recém implementado tinha sido uma sucesso. De lá para cá outros projetos adotaram este padrão, como no caso de um airdrop recente da Adidas e esperamos que mais projetos adotem no futuro (confira aqui), já que é um projeto recente.
Mas e agora, como podemos ser beneficiados pelo trabalho de pesquisa e desenvolvimento do ERC-721A?
Assim como já fazemos com os contratos da OpenZeppelin, podemos importar o contrato-base diretamente do GitHub, sendo que o endereço do projeto é esse aqui. Neste exemplo eu vou criar um contrato de NFT no Remix, para manter tudo mais simples e prático, mas não há diferenças em relação a você usar com Truffle ou HardHat exceto na importação.
Vamos começar então criando nosso contrato e importando a ERC721A do GitHub.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; import "https://github.com/chiru-labs/ERC721A/blob/main/contracts/ERC721A.sol"; contract LuizToolsNFT2 is ERC721A { constructor() ERC721A("LuizTools NFT 2.0", "LNFT") {} function mint(uint256 quantity) external payable { // `_mint`'s second argument now takes in a `quantity`, not a `tokenId`. _mint(msg.sender, quantity); } } |
Repare que a “magia” toda acontece no contrato ERC721A.sol que importamos, cabendo a quem for extender as funcionalidades deste contrato implementar o constructor, que é quase idêntico ao do ERC721 da OpenZeppelin, e implementar uma nova função de mint esperando a quantidade de tokens a serem mintados ao invés do tokenId como implementado no contrato padrão da OZ. Além dessa lógica “obrigatória” você pode implementar as características de mint do seu projeto, como a exigência de pagamento, por exemplo ou no caso abaixo, o uso de URLs para os metadados.
1 2 3 4 5 6 7 8 9 10 11 |
function _baseURI() internal pure override returns (string memory) { return "https://www.luiztools.com.br/nfts/"; } function tokenURI( uint256 tokenId ) public view override(ERC721A) returns (string memory) { return string.concat(super.tokenURI(tokenId), ".json"); } |
Ou o exemplo abaixo expondo uma função burn pública. Repare no segundo parâmetro da super._burn, ele indica se você deseja ou não que seja validado a permissão de quem está fazendo o burn do token (se colocar false, qualquer um pode queimar).
1 2 3 4 5 |
function burn(uint256 tokenId) external { super._burn(tokenId, true); } |
A recomendação que eu faço é que você teste todas as funcionalidades da implementação acima via Remix mesmo, ou se tiver alguma bateria de testes pronta para seu contrato no Truffle ou HardHat, você pode rodá-la contra esta implementação também e com isso ter a certeza de que atende aos requisitos do seu projeto NFT, combinado?
Para instalar o contrato no seu projeto Truffle/HardHat, use o comando abaixo.
1 2 3 |
npm install -D erc721a |
E depois importe da seguinte maneira:
1 2 3 |
import "erc721a/contracts/ERC721A.sol"; |
Um ponto importante que a implementação da Azuki difere da OpenZeppelin e que você deve prestara atenção nos seus testes automatizados é com relação aos erros. Enquanto que na OZ são usados requires tradicionais, na Azuki eles optaram por usar ifs com instruções revert, que lançam custom errors. Essa abordagem economiza em torno de 20 gás em relação aos requires, por chamada. No entanto, os testes dos cenários de erro devem ser ajustados para lidar com custom errors, como abaixo, neste exemplo tirado do HardHat em que uma transferência não é possível por que o token Id não existe (não foi mintado).
1 2 3 4 5 6 7 |
it("Should NOT transfer (exists)", async function () { const { contract, owner, otherAccount } = await loadFixture(deployFixture); await expect(contract.transferFrom(owner.address, otherAccount.address, 1)) .to.be.revertedWithCustomError(contract, "OwnerQueryForNonexistentToken"); }); |
Lembrando que algumas funcionalidades como o acesso por índice (ERC721Enumerable) não estão disponíveis nessa implementação, em virtude dos ajustes que o time da Azuki realizou.
E se quiser aprender a reduzir o consumo de gás nos seus smart contracts, este é o artigo certo. Uma outra dica é hospedar seus metadados e mídias dos NFTs no IPFS, falo disso no vídeo abaixo.
Então até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.