Recentemente ensinei como criar contratos multi-token no padrão da rede Ethereum (ERC-1155), o mais usado mundialmente. Naquele momento, usamos a ferramenta Remix e fizemos todo o processo do zero até o deploy. Recomendo fortemente que faça esse outro tutorial primeiro antes desse aqui, pois hoje vamos aprender novamente a criar novos tokens ERC-1155, mas usando ferramentas mais profissionais como o HardHat e OpenZeppelin e sem entrar nos detalhes do padrão, apenas partindo pra ação.
Para você entender o que vamos criar, será um contrato de tokens semi-fungíveis, aqueles em que são NFTs com várias unidades disponíveis de cada uma, geralmente usadas como tokens utilitários ou coleções de figurinhas em que várias pessoas conseguem completar o álbum inteiro. Esta é apenas uma das inúmeras possibilidades da ERC-1155, sinta-se livre para modificar o código proposto conforme suas necessidades.
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 multitoken-erc1155-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-1155, 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 MyTokens { } |
A OpenZeppellin é 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 MyTokens.test.ts e deixando a estrutura abaixo nele.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; import { ethers } from "hardhat"; describe("MyTokens", () => { async function deployFixture() { const [owner, otherAccount] = await ethers.getSigners(); const MyTokens = await ethers.getContractFactory("MyTokens"); const contract = await MyTokens.deploy(); const contractAddress = await contract.getAddress(); return { contract, 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 21 22 |
import { ethers } from "hardhat"; async function main() { const MyTokens = await ethers.getContractFactory("MyTokens"); const myTokens = await MyTokens.deploy(); await myTokens.waitForDeployment(); const address = await myTokens.getAddress(); console.log( `Contract 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 Multi-token
Agora vamos nos concentrar no arquivo MyTokens.sol, que é a implementação do nosso contrato multi-token ERC1155. 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 |
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; contract MyTokens is ERC1155, ERC1155Burnable { } |
Com esta implementação agora nosso contrato MyTokens tem acesso a todas características e funções compartilhadas pelos contratos ERC1155.sol, ERC1155Burnable.sol e Strings.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?
- ERC1155.sol: implementa de forma abstrata o padrão ERC-1155, a base para contratos multi-token mais algumas funções auxiliares, como para minting;
- ERC1155Burnable.sol: implementa funcionalidades de burn;
- Strings.sol: biblioteca de funções sobre strings;
Repare que os contratos descritos (deixando de lado a lib) devem ser encarados como implementações incompletas, servindo como base para o nosso, onde faremos a versão final que pode ser feito o deploy. Esse processo de implementação 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.
Vamos começar declarando algumas variáveis de estado e nosso construtor, que vai ser disparado quando fizermos deploy do contrato.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
uint public constant NFT_0 = 0; uint public constant NFT_1 = 1; uint public constant NFT_2 = 2; uint[] public currentSupply = [50, 50, 50]; uint public tokenPrice = 0.01 ether; address payable public immutable owner; constructor() ERC1155("https://www.luiztools.com.br/tokens/{id}.json") { owner = payable(msg.sender); } |
As primeiras três constantes são os ids fixos dos nossos tokens semi-fungíveis que teremos neste contrato. Depois, declarei um array contendo o supply corrente para cada um dos três, usando o id deles como posição do array e defini o preço que vou cobrar por cada unidade. Usaremos estas variáveis na hora que implementarmos nosso mint, mais adiante.
Já o construtor apenas pega o endereço do owner e salva-o em uma variável para consulta futura.
A seguir, vamos escrever nossa função de mint. A OpenZeppelin fornece uma função de mint genérica mas que não é pública, justamente para criarmos a nossa e apenas chamar a deles quando for necessário. Optei por uma abordagem minimalista: uma função payable que minta apenas uma unidade do token com o id passado, mas você pode facilmente modificar para receber a quantidade de unidades que deseja.
1 2 3 4 5 6 7 8 9 10 |
function mint(uint256 id) external payable { require(id < 3, "This token does not exists"); require(msg.value >= tokenPrice, "Insufficient payment"); require(currentSupply[id] > 0, "Max supply reached"); _mint(msg.sender, id, 1, ""); currentSupply[id]--; } |
Começamos fazendo validações de id, de pagamento e de estoque disponível. Tudo dando certo, fazemos o mint de uma unidade e decrementamos o supply corrente (afinal são tokens semi-fungíveis).
A próxima função é a de saque, importante para que possa retirar os fundos do contrato (oriundos dos mints) para a sua carteira futuramente.
1 2 3 4 5 6 7 8 9 |
function withdraw() external { require(msg.sender == owner, "You do not have permission"); uint256 amount = address(this).balance; (bool success, ) = owner.call{value: amount}(""); require(success == true, "Failed to withdraw"); } |
Aqui nós validamos o requisitante, para garantir que somente o owner possa fazer essa chamada. Outra alternativa seria você criar um function modifier para isso, mas não há necessidade para este evento já que não repetiremos este teste.
Agora vamos sobrescrever a função que serve 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-1155, na parte de extensão metadata.
1 2 3 4 5 6 7 8 9 10 11 |
function uri(uint256 id) public pure override returns (string memory) { require(id < 3, "This token does not exists"); return string.concat( "https://www.luiztools.com.br/tokens/", Strings.toString(id), ".json" ); } |
Aqui sobrescrevi a função existente da OpenZeppelin adicionando uma validação de id e retornando sempre seguindo um padrão próprio de URL. Aqui estou usando um exemplo didático, eu não tenho essa URL no meu servidor. 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 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, que você confere aqui.
Até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.