Recentemente ensinei como criar uma nova criptomoeda, ou melhor, um novo token, no padrão da rede Ethereum (ERC-20), o mais usado mundialmente. Naquele momento, usamos a ferramenta Remix e fizemos todo o processo até o deploy e a configuração do token na nossa carteira MetaMask. Recomendo fortemente que faça esse outro tutorial primeiro antes desse aqui, pois hoje vamos aprender novamente a criar novos tokens ERC-20, mas usando ferramentas mais profissionais como o Truffle e OpenZepellin.
Outro ponto importante é que este não deve ser o seu primeiro tutorial envolvendo a linguagem Solidity ou mesmo o toolkit Truffle. 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 Truffle. Para isso, crie uma pasta na sua máquina, que eu vou chamar de token-erc20-truffle. Dentro dela, rode o comando do Truffle para criar um template de projeto.
1 2 3 |
truffle unbox |
Com o projeto criado, revise a pasta contracts e deixe somente um contrato nela, cujo nome do arquivo vai ser o nome da sua cripto, no meu caso: LuizCoin. Se você fez meu outro tutorial de token ERC-20, 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.
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 |
Agora deixa na pasta migrations apenas uma, atualizando o código da mesma para fazer o deploy do seu contrato apenas.
1 2 3 4 5 6 7 |
const LuizCoin = artifacts.require("LuizCoin"); module.exports = function(deployer) { deployer.deploy(LuizCoin); }; |
E ajuste seu truffle-config.js para usar a versão mais recente do Solidity e também usar a rede de testes interna do Truffle, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
module.exports = { networks: { }, compilers: { solc: { version: "0.8.17", settings: { optimizer: { enabled: true, // Default: false runs: 200 // Default: 200 }, } } } }; |
E por fim, na pasta test, deixe somente um arquivo JS de testes, o que eu recomendo que tenha o mesmo nome do contrato com o sufixo “.test.js”, inicialmente com o conteúdo abaixo.
1 2 3 4 5 6 7 8 9 |
const LuizCoin = artifacts.require("LuizCoin"); contract('LuizCoin', function (accounts) { it("Should put total supply LuizCoin in the admin account", function () { assert(true, "Total supply wasn't in the first account"); }); }); |
Agora temos o setup do nosso projeto de novo token pronto, é hora de programarmos ele!
#2 – Contrato do Token
Agora vamos nos concentrar no arquivo LuizCoin.sol, que é a implementação do nosso token ERC20. Como vamos usar a biblioteca de contratos da OpenZeppelin, vamos começar importando a mesma, logo no topo do arquivo e dizendo que nosso contrato vai herdar todas as características do contrato ERC20.sol existente na biblioteca do OpenZeppelin. Fazemos isso com a keyword ‘is’ ao lado do nome do contrato novo e antes do nome do contrato-pai, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 |
//SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract LuizCoin is ERC20 { } |
Com esta implementação agora nossa LuizCoin tem acesso a todas características e funções compartilhadas pelo contrato ERC20.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.
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 |
constructor() ERC20("LuizCoin", "LUC"){ _mint(msg.sender, 1000 * 10 ** 18); } |
Aqui estou dizendo que o constructor de LuizCoin, quando chamado, deve invocar o constructor de ERC20 passando no primeiro argumento o nome da moeda e no segundo o symbol dela.
Também já realizei a implementação do código personalizado do constructor de LuizCoin, que vai fazer o minting, ou seja, a cunhagem/criação das moedas e a sua imediata transferência para uma carteira à sua escolha. Essa função _mint é herdade de ERC20 também e espera o endereço da carteira de destino e a quantidade de tokens a serem cunhados.
No exemplo, estou dizendo que a carteira do criador do contrato (lembrando que o constructor é chamado no deploy do contrato, então msg.sender é quem criou o mesmo na blockchain neste momento) vai receber 1000 tokens, o que será 0 nosso supply inicial. Eu multiplico a quantidade por 10 elevado na potência 18 porque quero que meu token tenha 18 casas decimais, como a maioria dos tokens ERC20 aliás, mas você pode alterar como quiser.
Com isso você já tem um token ERC20 funcional e se fizer deploy dele vai funcionar normalmente, como manda a especificação e tenho a certeza que você achou que seria mais complicado, certo?
Dá pra complicar, mas a base é essa aí mesmo.
Antes de pensar em complicar, vamos testar para ver se funciona mesmo.
#3 – Testes do Contrato
É aqui onde você vai escrever a maior quantidade de código e é onde também se mostrará necessário você ter feito o outro tutorial de token ERC20 onde escrevemos todo o contrato do zero, pois ali construímos o conhecimento de como tudo funciona, permitindo que a gente consiga saber como testar um token também. Para alguns dos testes vamos precisar manipular big numbers ou números gigantes, então recomendo que você instale no seu projeto a mesma biblioteca que o Truffle usa internamente, a BN.js.
1 2 3 |
npm install bn.js |
Agora abra o arquivo LuizCoin.test.js (ou equivalente) na pasta test e vamos escrever a estrutura básica para nossos testes, que consiste na função contract e um beforeEach, já que quero manter cada teste independente eu vou sugerir que o contrato seja inicializado individualmente antes de cada um. Além disso, já vou usar a biblioteca BN para inicializar uma constante DECIMALS corretamente (não esquecendo de carregá-la no topo do arquivo).
1 2 3 4 5 6 7 8 9 10 11 12 |
const BN = require("bn.js"); contract('LuizCoin', function (accounts) { const DECIMALS = new BN(18); beforeEach(async () => { contract = await LuizCoin.new(); }) } |
Essa constante DECIMALS será usada em alguns testes e embora ela não seja realmente um número gigante, vou inicializá-la dessa forma a fim de que facilite os testes e uso posterior.
Falando em testes, nosso primeiro teste irá verificar se o saldo total do token foi transferido para a carteira de quem fez o deploy do contrato (vulgo admin/owner).
1 2 3 4 5 6 7 |
it("Should put total supply LuizCoin in the admin account", async () => { const balance = await contract.balanceOf(accounts[0]); const totalSupply = new BN(1000).mul(new BN(10).pow(DECIMALS)); assert(balance.eq(totalSupply), "Total supply wasn't in the first account"); }); |
Começamos o teste pegando o saldo do contrato e comparamos ele com o big number do total supply que é 1000 multiplicado por 10 na potência 18. Repare como uso as funções do objeto BN para multiplicar e depois para fazer a potenciação. Isso é necessário pois os operadores tradicionais do JS não suportam big numbers. No final das contas a asserção é simples, mas exige que a gente use a função eq para comparação entre os big numbers.
Eu não vou ficar citando, mas a cada teste codificado, o ideal é rodar a bateria toda com o comando ‘truffle test’ a ver se estão passando, ok?
O teste seguinte é mais simples, para verificar se o nome do token foi definido corretamente.
1 2 3 4 5 6 |
it("Should has the correct name", async () => { const name = await contract.name(); assert(name === "LuizCoin", "The name is wrong"); }); |
Nada de especial aqui, então vamos avançar para o próximo que é igualmente simples: verificar se o symbol foi definido corretamente.
1 2 3 4 5 6 |
it("Should has the correct symbol", async () => { const symbol = await contract.symbol(); assert(symbol === "LUC", "The symbol is wrong"); }); |
E também outro bem simples que verifica se os decimals estão corretos.
1 2 3 4 5 6 |
it("Should has the correct decimals", async () => { const decimals = await contract.decimals(); assert(decimals.eq(DECIMALS), "The decimals are wrong"); }); |
E agora sim, finalmente entramos em um teste mais complicado: transfer. Aqui teremos dois cenários: um de sucesso e um de fracasso. No primeiro cenário, sucesso, vamos fazer uma transferência de 1 token para outra das carteiras disponíveis nos testes. No entanto, antes de fazermos esta transferência, sugiro pegar os saldos do from e do to a fim de podermos comparar se a transferência deu certo depois.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
it("Should transfer", async () => { const qty = new BN(1).mul(new BN(10).pow(DECIMALS)); const balanceAdminBefore = await contract.balanceOf(accounts[0]); const balanceToBefore = await contract.balanceOf(accounts[1]); await contract.transfer(accounts[1], qty); const balanceAdminNow = await contract.balanceOf(accounts[0]); const balanceToNow = await contract.balanceOf(accounts[1]); assert(balanceAdminNow.eq(balanceAdminBefore.sub(qty)), "The admin balance is wrong"); assert(balanceToNow.eq(balanceToBefore.add(qty)), "The to balance is wrong"); }); |
A chamada da função transfer é bem direta e simples e ela é nativamente feita a partir (from) da accounts[0] que é quem faz o deploy do mesmo. Como é uma transferência direta e de um valor pequeno (que cabe no saldo), ela será aceita normalmente e como resultado os saldos estarão ajustados após a mesma, o que podemos conferir pedindo novamente o saldo e comparando nos asserts com as funções da BN.js (já que é tudo big number não rola de comparar com os operadores do JS).
Agora vamos fazer o cenário de fracasso. Para isso, vou fazer o mesmo teste, mas tentando transferir mais do que a accounts[0] possui. Isso deve dar um erro, então vamos capturar o erro e fazer o assert em cima dele.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
it("Should NOT transfer", async () => { const aboveSupply = new BN(1001).mul(new BN(10).pow(DECIMALS)); try { await contract.transfer(accounts[1], aboveSupply); assert.fail("The transfer should have thrown an error"); } catch (err) { assert.include(err.message, "revert", "The error message should contain 'revert'"); } }); |
Repare que o big number que defini é acima do total supply em 1 token, já causando o erro de transferência inválida por fundos insuficientes. Isso que nem é considerado nos testes as taxas de gás, caso contrário eu nem precisaria ter usado 1001.
Avançando nos testes, agora vamos fazer o teste da função approve, que serve para dar permissão a outra carteira transferir uma quantidade x de nosso fundos. Para podermos fazer o “antes e depois”, temos de pegar o allowance que nada mais é do que a quantidade permitida em transferências delegadas.
1 2 3 4 5 6 7 8 9 10 |
it("Should approve", async () => { const qty = new BN(1).mul(new BN(10).pow(DECIMALS)); await contract.approve(accounts[1], qty); const allowedAmount = await contract.allowance(accounts[0], accounts[1]); assert(qty.eq(allowedAmount), "The allowed amount is wrong");`` }); |
Nada muito diferente aqui se você já entende como funciona a função approve e a função allowance, então vamos avançar para a próxima, que é a função transferFrom, que permite transferências delegadas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
it("Should transfer from", async () => { const qty = new BN(1).mul(new BN(10).pow(DECIMALS)); const allowanceBefore = await contract.allowance(accounts[0], accounts[1]); const balanceAdminBefore = await contract.balanceOf(accounts[0]); const balanceToBefore = await contract.balanceOf(accounts[1]); await contract.approve(accounts[1], qty); await contract.transferFrom(accounts[0], accounts[1], qty, { from: accounts[1] }); const allowanceNow = await contract.allowance(accounts[0], accounts[1]); const balanceAdminNow = await contract.balanceOf(accounts[0]); const balanceToNow = await contract.balanceOf(accounts[1]); assert(allowanceBefore.eq(allowanceNow), "The allowance is wrong"); assert(balanceAdminNow.eq(balanceAdminBefore.sub(qty)), "The admin balance is wrong"); assert(balanceToNow.eq(balanceToBefore.add(qty)), "The to balance is wrong"); }); |
Aqui temos um teste bem semelhante ao transfer, porém com mais passos e mais verificações. No “antes e depois” eu quero ver se o allowance depois da transferência estará igual a antes da mesma, já que ele será “queimado”. Além disso, pego as variáveis de saldo antes e depois também para ver se a transferência de fato aconteceu. Agora sobre a transferência em si, precisamos primeiro aprovar ela e depois executá-la chamando o transferFrom, sem esquecer do parâmetro adicional que é o objeto de configurações da transação e onde posso dizer qual carteira será o from. Neste exemplo, a carteira accounts[1] está transferindo para si saldo da carteira accounts[0] conforme permissão prévia.
E para finalizar, vamos escrever o teste de falha em uma transferência delegada, o que é bem simples de fazer bastando não dar um approve antes do transferFrom.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
it("Should NOT transfer from", async () => { const qty = new BN(1).mul(new BN(10).pow(DECIMALS)); try { await contract.transferFrom(accounts[0], accounts[1], qty); assert.fail("The transferFrom should have thrown an error"); } catch (err) { assert.include(err.message, "revert", "The error message should contain 'revert'"); } }); |
Como essa tentativa de transferência irá disparar erro, vamos capturá-lo e fazer nossa asserção em cima do mesmo.
Agora experimente rodar a bateria de testes com ‘truffle test’ e espero que você tenha o meso resultado que eu abaixo.
E com isso finalizamos mais este tutorial. Se quiser aprender como fazer deploy desta moeda em blockchain local, recomendo aprender a usar o Ganache neste tutorial. Agora se quiser colocar em uma blockchain pública, recomendo fazer este tutorial aqui. E se quiser aprender a reduzir o consumo de gás nos seus smart contracts, este é o artigo certo.
Um abraço e sucesso!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.
Como sempre, post muito bom. Só gostaria de solicitar a verificação dos links no final sobre o deploy. Os links estão com erro.
Outra detalhe: não explica em qual pasta devem ser instalados os pacotes e o import do BN.js não consta do artigo.
O link do Ganache realmente está quebrado, aquele tutorial ainda não saiu no blog, mas deve sair em breve. Possivelmente em algum momento troquei a ordem de publicação deles. O outro parece estar funcionando normalmente.
Sobre a pasta de instalação dos pacotes, em qualquer aplicação Node.js (o que inclui React e React Native também), você sempre instala os pacotes na raiz do projeto. Sobre o import, é o padrão mesmo, só usar um require, de qualquer forma incluí no artigo para facilitar.
Eu quero criar uma criptomoeda
Se já tiver conhecimentos básicos de programação, só seguir o tutorial e depois fazer deploy em produção.
Por gentileza, gostaria de verificar a possibilidade de uma videoconferência –
falo em nome da Anadips.
Queremos que avalie um projeto da nossa entidade.
Muito grato
Nery
61982207837
[email protected]
Boa noite Nery, tudo bem? Infelizmente não consigo absorver mais projetos do que os que já estou envolvido atualmente. Caso possa me passar um resumo da sua demanda e/ou do perfil que está buscando de profissional eu posso divulgar para alguns contatos na minha rede e eles entram em contato contigo. Pode enviar para [email protected]