Recentemente ensinei como criar NFTs (tokens não-fungíveis), ou melhor, um contrato para cunhar NFTs, no padrão da rede Ethereum (ERC-721), o mais usado mundialmente. Naquele momento, usamos a ferramenta Remix e fizemos todo o processo até o deploy e a configuração do NFT na nossa carteira MetaMask. Recomendo fortemente que faça esse outro tutorial primeiro antes desse aqui, pois hoje vamos aprender novamente a criar novos NFTs ERC-721, mas usando ferramentas mais profissionais como o HardHat e OpenZeppelin.
Outro ponto importante é que este não deve ser o seu primeiro tutorial envolvendo a linguagem Solidity ou mesmo o toolkit HardHat. Se esse for o seu caso, recomendo começar por este outro aqui.
#1 – Criando o Projeto
O primeiro passo é você criar um novo projeto HardHat. Para isso, crie uma pasta na sua máquina, que eu vou chamar de nft-erc721-hardhat. Abra o terminal e navegue até ela, executando os comandos para criação do projeto HardHat de exemplo (selecione a opção TypeScript e o resto deixe default).
1 2 3 4 5 |
npm init -y npm i -D hardhat npx hardhat |
Com o projeto criado, limpe as pastas contracts, scripts e test.
Na pasta contracts, crie somente um contrato nela, cujo nome do arquivo vai ser o nome da sua coleção, no meu caso: MyNFT. Se você fez meu outro tutorial de ERC-721, pode inclusive copiar e colar o código-fonte de lá para ganhar tempo, mas a abordagem que vou propor aqui é diferente e mais profissional pois vamos usar contratos da OpenZeppelin.
1 2 3 4 5 6 7 8 |
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract MyNFT { } |
A OpenZeppelin é uma empresa conhecida mundialmente na área de cripto e blockchain por oferecer produtos e serviços na área de segurança de aplicações decentralizadas. Como alguns códigos Solidity são muito frequentes, como os códigos de padrões ERC, e é muito fácil de você deixar brechas em smart contracts para atacantes, eles resolvem os dois problemas fornecendo bibliotecas de contratos open-source já testados e auditados extensivamente para você usar, além de serviços de auditoria requisitados por grandes players do mercado.
Assim, vamos instalar a biblioteca de contratos deles em nosso projeto e você verá os ganhos que ela vai nos trazer logo mais.
1 2 3 |
npm install @openzeppelin/contracts |
Repare que estou usando a versão 0.8.20 ou superior no meu contrato, você deve usar a versão que possuir instalada na sua máquina, a partir da que informei acima. E por fim, vamos deixar a estrutura de nossos testes preparada, criando na pasta test um arquivo MyNFT.test.ts e deixando a estrutura abaixo nele.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; import { ethers } from "hardhat"; describe("MyNFT", () => { async function deployFixture() { const [owner, otherAccount] = await ethers.getSigners(); const MyNFT = await ethers.getContractFactory("MyNFT"); const myNFT = await MyNFT.deploy(); return { myNFT, owner, otherAccount }; } }); |
Aqui estamos carregando as importações necessárias, definindo uma suíte de testes (describe) e criando a função de deploy fixture, que serve para o contrato ser provisionado apenas uma vez e limpo a cada teste, além de termos acesso às carteiras de teste.
Agora na pasta scripts vamos criar um arquivo deploy.ts colocando o código necessário para fazer o deploy do seu contrato apenas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { ethers } from "hardhat"; async function main() { const MyNFT = await ethers.getContractFactory("MyNFT"); const myNFT = await MyNFT.deploy(); await myNFT.waitForDeployment(); const address = await myNFT.getAddress(); console.log(`Contract MyNFT deployed to ${address}`); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main().catch((error) => { console.error(error); process.exitCode = 1; }); |
Agora temos o setup do nosso projeto de novo token pronto, é hora de programarmos ele!
#2 – Contrato da coleção NFT
Agora vamos nos concentrar no arquivo MyNFT.sol, que é a implementação da nossa coleção de NFTs ERC721. Como vamos usar a biblioteca de contratos da OpenZeppelin, vamos começar importando a mesma, ou melhor, os contratos que precisaremos, logo no topo do arquivo e dizendo que nosso contrato vai herdar todas as características deles. Fazemos isso com a keyword ‘is’ ao lado do nome do contrato novo e antes do nome dos contratos-pai, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; contract MyNFT is ERC721, ERC721Enumerable, ERC721URIStorage, ERC721Burnable { } |
Com esta implementação agora nossa coleção MyNFT tem acesso a todas características e funções compartilhadas pelos contratos ERC721.sol, ERC721Enumerable.sol, ERC721URIStorage.sol e ERC721Burnable.sol, acelerando bastante o nosso desenvolvimento e reduzindo drasticamente a nossa chance de deixar brechas em nosso código pois ele será mais de customização do que de implementação.
Mas para que servem todos esses contratos afinal?
- ERC721.sol: implementa de forma abstrata o padrão ERC-721, a base para os NFTs mais algumas funções auxiliares, como para minting;
- ERC721Enumerable.sol: implementa de forma abstrata a extensão Enumerable do padrão ERC-721, permitindo acessar NFTs por índice;
- ERCURIStorage.sol: implementa de forma abstrata a extensão Metadata do padrão ERC-721, permitindo registrar junto do token id a URI com os metadados do mesmo;
- ERC721Burnable.sol: implementa funções auxiliares de burn de tokens;
- Strings.sol: library que fornece funções utilitárias para manipular strings;
Repare que os quatro primeiros contratos descritos são implementações abstratas de contratos (abstract contract), ou seja, incompletas, e servem como base para o nosso, onde faremos a versão final que pode ser feito o deploy. Esse processo de implementação concreta (o oposto da abstrata) requer que a gente herde dos contratos anteriores, o que fazemos com a keyword ‘is’ logo no início. Ao fazer isso assumimos o compromisso de implementar a versão final das funcionalidades que exijam sobrescrita, marcadas como virtual.
Por exemplo, para definir o nome e symbol do nosso token, basta chamarmos o constructor da superclasse/classe-pai junto ao nosso constructor, como abaixo, passando os seus parâmetros.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
uint256 private _nextTokenId; constructor() ERC721("MyNFT", "MYN") {} function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable, ERC721URIStorage) returns (bool) { return super.supportsInterface(interfaceId); } |
Aqui estou dizendo que o constructor de MyNFT, quando chamado, deve invocar o constructor de ERC721 passando no primeiro argumento o nome da coleção e no segundo o symbol dela.
Aproveitei também para declarar que vamos usar um contador para gerar ids de tokens auto-incrementais, o que vai ser muito útil em nossa função de minting logo a seguir.
E mais ao final deste primeiro bloco de código eu sobrescrevi (override) a função supportsInterface presente de forma virtual nos contratos abstratos ERC721 e ERC721Enumerable e que é uma exigência da ERC-165, dos quais o 721 deve respeitar também.
Agora vamos implementar as funções que servem para montar a URL do JSON de metadados que é a forma mais comum de armazenar as informações do ativo digital que o NFT representa. Eu não vou criar aqui o JSON com você ou sequer hospedá-lo pois foge do escopo deste tutorial, mas você pode facilmente usar qualquer linguagem de programação para fazê-lo, usando como base o formato sugerido na própria ERC-721, na parte de extensão metadata.
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/nft/"; } function tokenURI( uint256 tokenId ) public view override(ERC721, ERC721URIStorage) returns (string memory) { return string.concat(super.tokenURI(tokenId), ".json"); } |
Aqui sobrescrevi duas funções exigidas pelos contratos que herdamos do OpenZeppelin. A primeira, _baseURI deve retornar a URL base dos seus tokens. Aqui estou usando um exemplo didático, eu não tenho essa URL no meu servidor. A partir dele montaremos na função tokenURI o caminho completo até o JSON com os metadados. Repare que usei a função string.concat para juntar os pedaços da string, algo que existe no Solidity a partir da versão 0.8.12.
Agora temos todos os ingredientes para criar nossa função de mint, como segue.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function mint() public { uint256 tokenId = ++_tokenIdCounter; _safeMint(msg.sender, tokenId); _setTokenURI(tokenId, Strings.toString(tokenId)); } function _update(address to, uint256 tokenId, address auth) internal override(ERC721, ERC721Enumerable) returns (address) { return super._update(to, tokenId, auth); } function _increaseBalance(address account, uint128 value) internal override(ERC721, ERC721Enumerable) { super._increaseBalance(account, value); } |
Minha função de mint começa pegando o próximo id de token e incrementando o contador. Usamos a informação do msg.sender e o novo id para mintar o token usando _safeMint, uma função herdada dos contratos que carregamos. Após mintar o token, registramos a URI dele (só a parte que é diferente entre os nfts) usando _setTokenURI, outra função herdada.
Aproveitei a deixa para incluir outras duas funções que são obrigatórias sobrescrevermos, a _update e a _increaseBalance. Dá pra complicar, mas a base da construção de um contrato de NFT ERC-721 mintable, burnable, enumerable e com metadados baseados em URL é esse aí mesmo.
Agora uma etapa que pode ser bem interessante de ser feita é escrevermos testes do nosso contrato. Como esse tutorial já está bem grandinho, vou deixar isso para a parte 2, ok?
Acesse a parte 2 aqui.
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.