Arquitetura de Informação

28 abr, 2023

Tutorial de Smart Contract ERC-721 (NFT) com Solidity (Parte 3)

Publicidade

Este tutorial é a parte 3 de uma série onde estou ensinando como construir smart contracts para coleções NFT usando o padrão ERC-721 com a linguagem Solidity. Caso queira, confira a Parte 1 e a Parte 2.

Tivemos introduções teóricas, construção da estrutura principal e obrigatória da especificação 721 e na parte 2 chegamos inclusive a construir uma função de mint seguindo sugestões da especificação como o uso de URIs JSON MetaData. Nesta terceira parte, vamos trazer mais algumas funções comuns, mas opcionais, que valem a pena ser estudadas e que também são sugeridas na especificação.

Burning

Outra função extremamente relevante para alguns projetos é a de burning. Burning é o ato de destruir (queimar, no inglês) um token por qualquer que seja o motivo. Na nossa estrutura de contrato pode implementar uma função de burn da seguinte maneira.

Comecei pegando o endereço do owner atual e verificando duas coisas com ele:

  • primeiro, ele não pode ser zero, caso contrário quer dizer que o token não foi mintado ainda;
  • segundo, ele deve ser o requisitante do burn ou ter dado permissão ao requisitando (approve ou approveForAll);

Após essas duas verificações, eu reduzo o balance do dono do token, destruo o registro de sua propriedade setando o endereço para zero e por último limpo a uri dele para salvarmos espaço no bloco. Também aproveito para limpar a delegação/aprovação de controle, se houver, e emitir os eventos que sou obrigado pelo padrão ERC721, avisando que houve uma transferência e uma mudança de autorização, ambas ligadas à carteira de endereço zero.

Enumerable Extension

Outras funções extremamente úteis e inclusive sugeridas na especificação são as relacionadas à extensão Enumerable. Mas o que seria um contrato NFT com enumeração? É basicamente um contrato que possui funções que permitem acessar os NFTs dos owners pelo seu índice ao invés de tokenId, facilitando a construção de dapps, marketplaces, etc. Sem esse tipo de funcionalidade você tem de saber o id de cada token para poder acessá-lo, o que muitas vezes é inviável no caso deles serem hashes.

Abaixo segue a interface ERC721Enumerable tal como sugerida na especificação:

Ela prevê as seguintes funções:

  • totalSupply: quantidade total de NFTs existentes no contrato (owner != 0);
  • tokenByIndex: acessa um token pelo seu índice, dentre todos da coleção;
  • tokenOfOwnerByIndex: acessa um token de um owner pelo seu índice;

Para implementarmos esta extensão, copie a interface acima e coloque no nosso MyNFT.sol, junto das demais interfaces. Depois, modifique a função supportsInterface para inclui-la.

E agora vamos implementar as três novas funções, começando por algumas variáveis de estado novas que serão necessárias.

Estas variáveis controlam o estado dos índices, sendo elas.

  • _ownedTokens: mapping que relaciona para cada owner todos os índices e tokens dele;
  • _ownedTokensIndex: mapping que relaciona para cada token, o seu índice de owner;
  • _allTokens: array contendo todos os tokens existentes;
  • _allTokensIndex: mapping que relaciona para cada token, o seu índice global;

Agora podemos usar estas variáveis na implementação das funções da interface.

As três funções são bem simples mas atendem ao seu propósito de fornecer mecanismos de enumeração dos tokens.

A primeira, totalSupply, apenas conta quantos tokens nos temos no array _allTokens.

A segunda, tokenByIndex, retorna o tokenId dado uma posição no array global de tokens, mas não sem antes verificar se aquele índice existe.

E a terceira e última, tokenOwnerByIndex, faz o mesmo que a anterior, mas apenas dentro do escopo de tokens de um owner específico, que também tem o índice validado antes de continuar.

Repare que estas funções dependem que as variáveis de controle estejam devidamente atualizadas, o que por sua vez exige que a gente atualize as funções existentes de mint e de burn em nosso contrato.

Aqui na função de mint eu adicionei quatro novas linhas pouco antes de emitir o evento de transferência, visando garantir que cada novo token mintado receba um índice ao final, tanto do array global quanto do array virtual de cada owner. Primeiro adicionamos o novo token id no array global. Depois, adicionamos a posição do novo token no mapping de índices globais, usando a quantidade de tokens para saber a última posição existente. Estas duas primeiras instruções atualizam as variáveis de estado ligadas à enumeração global de tokens.

Na sequência, adicionamos o novo id no mapping que relaciona ids com índices, usando o saldo de tokens do owner para sempre atribuir a última posição como índice. E por último, adicionamos o novo id no mapping que relaciona as posições dos tokens de cada owner. Estas duas últimas instruções atualizam as variáveis de estado ligadas à enumeração de tokens de cada owner.

Mas e o burn?

Burn com Enumerable

Aqui o desafio é um pouco maior, mas não muito. Isso porque ao destruirmos um NFT isso pode deixar “buracos” na organização dos índices quando o NFT destruído não estava no final da lista global ou até mesmo da lista virtual de algum dos owners. Neste caso nós temos três abordagens do que podemos fazer:

  • nada: isto é, deixar os buracos. Assim, pode ser que ao acessar um índice de NFT, global ou de owner, venha o id de um NFT já destruído, o que não gera custo adicional de gás mas acaba consumindo espaço desnecessário e prejudica a experiência do usuário;
  • reordenar: isto é, reorganizar todos os itens do array global e do virtual por owner para que o buraco seja preenchido sem perder a ordem original dos tokens (ordem de emissão). Esta abordagem possui custo de gás linear (O(n)) e a melhor experiência para o usuário, além de não desperdiçar espaço em disco;
  • preencher o buraco: isto é, reorganizar apenas um elemento no array a fim de preencher o buraco deixado pelo burn de outro. Esta abordagem é um meio termo pois possui um custo de gás estável (O(1)), não desperdiça espaço em disco, mas pode causar alguma estranheza no usuário pois a ordem dos NFTs não irá respeitar a ordem de emissão após ele fazer um ou mais burns.

Dadas estas características, eu vou fazer aqui a terceira opção, mas sinta-se livre para modificar a sua implementação conforme suas preferências e objetivos com este tipo de contrato.

Eu incluí alguns comentários pois é um código de legibilidade questionável devido à relativa complexidade da lógica. Ainda assim, um rápido resumo seria que a estratégia consiste em fazer uma cópia do último elemento do array global para a posição a ser excluída, sobreescrevendo-a e em seguida removendo o elemento duplicado que estava no final. Isso é feito tanto para o array global quanto para o array virtual do owner que teve o token queimado.

O resultado prático desta lógica é que o último token é movido para a posição que o burn tornou vaga, a fim de preenchê-la.

Com estas novas funções seu contrato de NFT certamente ficou mais completo e abaixo seguem algumas sugestões de outras funções que pode ser interessante você criar:

  • withdraw: função para fazer os saques, no caso de NFTs payable;
  • pause: função para pausar novos mints, a fim de permitir alguma manutenção com segurança;

Outra dica é você hospedar tanto os metadados quanto a mídia dos seus NFTs no IPFS, falo disso no vídeo abaixo.

E com isso finalizamos mais esta etapa no desenvolvimento de nosso contrato de NFT. Conseguimos cobrir o principal nessas três partes e espero ter te trazido um melhor entendimento sobre este tipo de implementação que é um dos mais requisitados para os profissionais web3 atualmente.

Quer aprender uma forma ainda mais profissional de implementar? Confira neste tutorial com HardHat e OpenZeppelin.

Quer aprender uma forma com menos taxas de gás no minting? Confira neste tutorial do ERC-721a (Azuki).

Até a próxima!

 

*O conteúdo deste artigo é de responsabilidade do(a) autor(a) e não reflete necessariamente a opinião do iMasters.