Como criar uma nova criptomoeda usando Solidity, TypeScript e HardHat

Web3 e Blockchain

Como criar uma nova criptomoeda usando Solidity, TypeScript e HardHat

Luiz Duarte
Escrito por Luiz Duarte em 08/01/2026
Junte-se a mais de 34 mil devs

Entre para minha lista e receba conteúdos exclusivos e com prioridade

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 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 token-erc20-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).

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 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.

Não sabe o que é OpenZeppelin? Explico no vídeo a seguir.

YouTube player

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.

Repare que estou usando a versão 0.8.28 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 LuizCoin.test.ts e deixando a estrutura abaixo nele.

Aqui estamos carregando as importações necessárias, definindo uma suíte de testes (describe) 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.

Agora temos o setup do nosso projeto de novo token pronto, é hora de programarmos ele!

Curso Web3 para Iniciantes

#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.

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.

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 (TypeScript)

É 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. Vamos fazer primeiro os testes em TypeScript e a seguir vou mostrar os mesmos testes em Solidity.

Agora abra o arquivo LuizCoin.test.ts na pasta test e vamos escrever nosso primeiro teste, que irá verificar se o saldo total do token foi transferido para a carteira de quem fez o deploy do contrato (vulgo admin/owner).

Começamos o teste pegando o saldo da carteira do owner e comparamos ele com o big number do total supply que é 1000 multiplicado por 10 na potência 18. Repare como uso o novo tipo do TypeScript para big numbers, suportado pela versão mais recente da biblioteca EthersJS. Para isso, basta adicionar o sufixo ‘n’ após o número. Isso é necessário pois os operadores tradicionais do JS não suportam big numbers. No final das contas a asserção é simples, com uso de .to.equal.

Eu não vou ficar citando, mas a cada teste codificado, o ideal é rodar a bateria toda com o comando ‘npx hardhat test mocha’ a ver se estão passando, ok? O teste seguinte é mais simples, para verificar se o nome do token foi definido corretamente.

Nada de especial aqui, então vamos avançar para o próximo que é igualmente simples: verificar se o symbol foi definido corretamente.

E também outro bem simples que verifica se os decimals estão corretos.

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.

A chamada da função transfer é bem direta e simples e ela é nativamente feita a partir (from) da conta owner 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 expects.

Agora vamos fazer o cenário de fracasso. Para isso, vou fazer o mesmo teste, mas tentando transferir mais do que a conta otherAccount possui. Isso deve dar um erro, então vamos capturar o erro e fazer o expect em cima dele com o to.be.revertedWithCustomError.

Repare que o big number que defini é apenas 1, mas a conta otherAccount nasce zerada, causando o erro de transferência inválida por fundos insuficientes. Já o nome do CustomError eu peguei diretamente da implementação da OpenZeppelin.

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.

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.

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 de primeiro usar a função connect para alterar a instância de conexão com o contrato, já que por padrão ele vem conectado com a conta owner. Neste exemplo, a carteira otherAccount está transferindo para si saldo da carteira owner conforme permissão prévia.

E para finalizar, vamos escrever os testes de falha em transferências delegadas, o que é bem simples de fazer bastando:

  • 1) não ter saldo suficiente na conta do transferFrom no primeiro cenário;
  • 2) não dar um approve antes do transferFrom no segundo cenário.

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 ‘npx hardhat test mocha’ e espero que você tenha o meso resultado que eu abaixo.

#4 – Testes do Contrato (Solidity)

Outra possibilidade é escrever os testes usando Solidity ao invés de TypeScript. Para isso, crie na pasta contracts um arquivo LuizCoin.t.sol e dentro dele escreva a seguinte estrutura inicial.

Aqui fiz as importações do contrato a ser testado, da biblioteca de testes do Forge e da biblioteca OpenZeppelin, mais especificamente do arquivo que contém os custom errors de Token ERC20 que vamos precisar para os testes de cenário de falha.

Depois de criarmos o contrato de teste (LuizCoinTest), definimos as variáveis locais da instância do contrato e duas carteiras que usaremos nos testes. Na função setUp inicializamos as duas carteiras e instância do contrato, usando o vm.prank para fazê-lo usando a conta do owner.

Por fim, adicionei uma função de comparação de strings, para usarmos em algumas validações que vamos ter.

Os primeiros 4 testes são bem objetivos e servem para testar se o nome, sigla, decimais e supply total estão corretamente definidos no contrato, como abaixo.

Os testes em si são bem simples, usando requires tradicionais do Solidity e para executá-los basta rodar o comando ‘npx hardhat test solidity’. A seguir, precisamos testar se o saldo da conta owner foi corretamente estabelecido, o que pode ser feito usando a função balanceOf.

Agora vamos para o teste de transferência bem sucedida, que é o mais complexo até o momento.

Começamos com o startPrank para definir que em toda a função vamos usar a conta owner para fazer as chamadas. Pegamos o saldo dela e do destinatário (otherAccount) antes e depois da transferência para fins de comparação e usamos requires para testar os valores corretos.

Para o teste de erro, usaremos o prank novamente para usar a otherAccount, pois como ela tem saldo zerado dará erro na transferência.

Como a OpenZeppelin usa custom erros, é bem chatinho de programar o expectRevert corretamente, precisando usar o abi.encodeWithSelector em cima do custom error correto (ERC20InsufficientBalance) passando nos parâmetros qual conta quer transferir, qual o saldo dela e qual o valor a ser transferido.

Agora vamos ao teste de aprovação, sem nenhuma grande novidade em relação aos testes que já fizemos antes.

Agora o teste de transferFrom, sempre mais complexo de fazer pois exige todo um setup inicial de aprovação e troca de contas. Já as comparações em si dependem da coleta dos saldos antes e depois, além do allowance restante.

Agora vamos aos dois cenários de teste do transferFrom: saldo insuficiente e allowance insuficiente. Sem grandes novidades nesses dois testes, mas exigem uma atenção especial expectRevert de cada um que deve ser corretamente codificado com os parâmetros corretos no encodeWithSelector: no caso de transferFrom, eles são o seletor, a conta que vai transferir, o saldo na conta e o valor a ser transferido.

Ao rodar o npx hardhat teste solidity uma última vez, você deve ter o resultado abaixo.

E com isso finalizamos mais este tutorial. Se quiser aprender como fazer deploy desta moeda em blockchain local, recomendo aprender a usar o HardHat Network neste outro 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.

E por fim, no vídeo abaixo eu ensino mais sobre a biblioteca OpenZeppelin Contracts, caso queira se aprofundar no assunto.

YouTube player

Um abraço e sucesso!

TAGS:

Olá, tudo bem?

O que você achou deste conteúdo? Conte nos comentários.

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *