Tecnologia

27 mar, 2023

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

Publicidade

Recentemente escrevi um tutorial aqui no blog ensinando os fundamentos sobre NFTs e explicando o padrão ERC-721, o primeiro e mais importante quando o assunto são smart contracts de NFTs. No tutorial de hoje, vamos avançar nossos estudos indo para a prática, ou seja, programando nosso Smart Contract seguindo o padrão. É importante você encarar este tutorial como uma parte 2 e que volte ao primeiro se não possuir domínio do assunto ainda.

Para este tutorial usarei a ferramenta Remix, mas você nem verá nada específico dela durante o tutorial, é apenas para abstrair qualquer aspecto de ambiente já que é a ferramenta mais crua que existe atualmente. Nada impede no entanto que você use dos mesmos códigos para escrever seu contrato em toolkits como Truffle e HardHat, o que eu mesmo pretendo fazer mais à frente aqui no blog e nos cursos.

Então abra o Remix, crie um novo arquivo que vamos chamar de MyNFT.sol e bora programar!

Primeiro a estrutura básica

Conforme explicitado na documentação oficial do padrão, todo contrato NFT deve implementar as interfaces ERC721 e ERC165. A primeira diz respeito às funções e eventos obrigatórios de todas NFTs, enquanto que a segunda diz respeito a como o contrato avisa que é compliance com determinados padrões/interfaces.

Embora não seja obrigatório (até onde sei) você ter a interface declarada no seu código, pode ser um bom ponto de partida fazê-lo, para ter certeza que estará aderente aos padrões acima citados e ajuda em algumas funções como a exigida pelo ERC165 que veremos mais à frente. Então, logo no topo do seu MyNFT.sol, declare as interfaces tal como fornecidas pela Ethereum.

Com estas interfaces no seu código, o próximo passo é criar logo abaixo o contrato em si, que deverá implementar as mesmas.

Repare que aqui eu implemento as interfaces previamente descritas com a keyword ‘is’. Agora é a hora que devemos estruturar as variáveis de estado do nosso contrato, necessárias para controle de propriedade dos NFTs e outros.

Aqui temos um primeiro mapping que relaciona para cada NFT existente (a chave é o tokenId, um uint256) com o seu dono (endereço da carteira).

Depois temos um segundo mapping que relaciona para cada carteira de dono, quantos NFTs ele possui.

A terceira variável é o mapeamento de tokenIds para operadores aprovados. Ou seja, é o equivalente ao allowance dos tokens ERC-20, mas binário (tem ou não tem permissão).

E por último temos um mapeamento de, dado um owner (primeiro address), tem-se o mapeamento de operadores com permissão total na coleção NFT daquele owner. Note que as primeiras três variáveis possuem _ no nome por serem internas (de uso interno do contrato), enquanto que a isApprovedForAll é pública e possui exatamente o mesmo nome definido no padrão, ou seja, uma função a menos para escrevermos!

E antes de entrarmos nas funções específicas da ERC-721, vamos implementar a única função exigida pela ERC-165, a supportsInterface.

Essa função retorna um booleano indicando se determinada interface passada por parâmetro está ou não implementada por este contrato. Como nosso contrato deverá implementar as interfaces ERC721 e ERC165, é com elas que faço a comparação para devolver se o contrato suporta ou não a interface informada.

Implementando as funções de propriedade e delegação

Por uma questão de organização vou quebrar as funções do contrato em quatro grupos: as funções de propriedade, as funções de delegação, as de transferência e as funções opcionais, não obrigatórias. Vamos começar pelas funções de propriedade e dentro desse grupo, com as duas funções de leitura/verificação de propriedade.

A função ownerOf, exigida pela interface, espera o id e um token e consulta esse id no mapping de owners para ver quem é o dono do mesmo. Caso o endereço retornado seja 0, optei por devolver um erro informando que o token em questão não existe. Além disso, repare que não usei a instrução return ao término da função. Ao invés disso, nomeei a variável da instrução returns na assinatura da função, assim ela fica “ligada” à variável de mesmo nome existente no corpo da mesma.

Já a função balanceOf, também exigida pela interface, recebe o endereço de uma carteira que usamos para procurar no mapping de balances para retornar quantos tokens aquele usuário possui, devolvendo um erro caso a carteira informada seja zero.

Além da propriedade, a ERC-721 determina que podemos delegar o controle de nossos tokens a outras carteiras, o que é especialmente útil para brokers, marketplaces, etc. Para isso, precisamos implementar algumas funções específicas que agem em cima de variáveis de estado que definimos antes.

Primeiro vamos falar da função setApprovalForAll, que é obrigatória do padrão e que começa já setando que, para o requisitante da transação (msg.sender, chamado de owner), vamos definir que o operator tem controle total (approved = true) ou nenhum controle (approved = false) sobre toda coleção NFT do owner. Ao término da execução dessa transação, emitimos um evento conforme manda a especificação.

Já a segunda função, approve, faz a mesma coisa mas para apenas uma NFT do owner. Ela começa pegando a informação de quem é o owner do token que vamos delegar controle e se esse owner não for o requisitante ou não estiver como controlador total da coleção, dará erro de não autorizado. Caso ele possua permissão, então o operador informado (spender) é adicionado como tendo aprovação sobre o token de id também informado. Ao término da função, o evento Approval é disparado como exigido pelo padrão.

A terceira função serve para retornar quem é o operador/controlador aprovado para um determinado token. Caso o token não exista, isso é informado como um erro também. Caso não exista aprovação para o token em questão, o endereço zero será retornado, como é de praxe no Solidity.

Já a quarta e última função desse grupo NÃO É do padrão ERC-721, é uma função própria, muito útil para algumas verificações mais adiante. Ela retorna um booleano indicando se determinada carteira (spender) é o owner do token ou alguém aprovado pelo owner. Nada demais.

Implementando as funções de transferência

Uma das principais vantagens de se usar NFTs padronizadas é que não apenas podemos registrar nossas criações e propriedades na blockchain como também podemos transferi-las para outras pessoas, o que é realizado através de três funções de transferência exigidas pelo padrão ERC-721, sendo a primeira delas a transferFrom, que vou implementar ligeiramente diferente do padrão, mas já explico o porque.

A função transferFrom espera que você informa quem é o dono atual do token, quem vai ser o novo dono e o id do token. Ela não assume que o requisitante seja o dono automaticamente porque ele pode ser o controlador aprovado do mesmo, e não o dono original, mas ainda assim ele deve saber quem é o dono. Esse dono será validado na primeira linha da implementação, bem como se o novo dono é uma carteira válida.

Depois, verificamos através de outro require se o msg.sender é o dono ou possui permissão para fazer esta transferência. Ele tendo, o balance do owner atual é diminuído, o balance do novo dono é incrementado, e a posse do tokenId é alterada no mapping de owners.

Por fim, como manda o padrão, todas as aprovações existentes para este token são revogadas e um evento de transferência é emitido.

Eu implementei esta função ligeiramente diferente do padrão pois preciso de uma função para uso interno no contrato, de modo a reaproveitar a lógica nas próximas funções. Mesmo assim, é importante que tenhamos outra transferFrom, essa sim como mandar o contrato para estarmos 100% aderentes, como segue.

Aí sim nós temos a função externa com o nome especificado no ERC721 e a interna para uso do próprio contrato e quem faz o trabalho duro de fato. Agora vamos falar das duas funções safeTransferFrom que são versões mais seguras de transferir NFTs.

Repare que a safeTransferFrom possui exatamente a mesma assinatura da transferFrom original e que ela inclusive chama a outra internamente. Mas repare também que ela é considerada “safe” por causa de uma validação adicional ao final da mesma que evita que seja transferido para um recipiente inválido. Mas o que seria um recipiente inválido?

A primeira coisa a se testar é se o to.code, ou seja, os bytes de código-fonte ligados àquele endereço são zero. Se forem zero, quer dizer que o endereço ‘to’ é uma conta comum e não um contrato, o que é totalmente válido. Caso seja um smart contract (code.length > 0), então deve ser feito outro teste, onde verificamos se a resposta à chamada da função onERC721Received indica que o token foi recebido com sucesso. Lembrando que mesmo este require estando após a transferência, que se ele der negativo, a transação como um todo será desfeita.

E agora vamos falar da segunda função de transferência segura, que muda apenas um elemento na assinatura.

Aqui temos um parâmetro a mais chamado data e com isso uma sobrecarga de função (overload), permitindo chamar safeTransferFrom com ou sem esse último parâmetro que serve para passar dados adicionais, à gosto do desenvolvedor. Além disso vale ressaltar que o local de armazenamento dele foi definido como calldata que é mais econômico que memory (em gás) mas não permite alteração desse parâmetro internamente na função (é immutable).

E com isso nós finalizamos toda a implementação obrigatória de funções e eventos definidos pela ERC-721. Com o que implementamos já é possível implementar um contrato de NFT e podemos definir no constructor do contrato que ao ser criado a nossa NFT já será transferida para o criador do mesmo, ou seja, um único mint no deploy mesmo.

Dessa forma, você já tem todas as funcionalidades de NFT, mas não da forma como normalmente são feitas as NFTs comerciais, as grandes coleções, etc. Falaremos sobre algumas funções opcionais/adicionais que certamente você sentiu falta, na próxima parte deste tutorial.