WEB3DEV

Cover image for ERC20 Usando o Hardhat: Um Guia Abrangente Atualizado
Fatima Lima
Fatima Lima

Posted on

ERC20 Usando o Hardhat: Um Guia Abrangente Atualizado

O ambiente de desenvolvimento criptográfico está sempre evoluindo a uma velocidade vertiginosa, portanto, é a melhor hora para uma atualização com a próxima fusão da Ethereum. Para este fim, compilei um guia de end-to-end (ponta a ponta) sobre como criar seu próprio token ERC20 usando o Hardhat. Isto inclui:

  • Configuração e instalação do ambiente do Hardhat
  • Extensão do contrato ERC20 do OpenZeppelin
  • Implantação do Token para a Testnet Goerli (obtenção do ETH de teste, conexão com o nó, exportação da chave privada)
  • Interação programática com uma instância contratual local
  • Envio do Token ERC20 criado na Testnet Goerli via Metamask

Embora dirigido aos que estão iniciando no espaço, este guia requer alguma familiaridade básica com conceitos do Node.js e Solidity. Todo o código utilizado neste guia pode ser encontrado no github.

Guias Relacionados:

Configuração do Hardhat

Primeiro precisamos criar um diretório do projeto e instalar o Hardhat:

mkdir ERC20
cd ERC20
npm install --save-dev hardhat
Enter fullscreen mode Exit fullscreen mode

Uma vez que o pacote hardhat tenha sido instalado, podemos executar npx hardhat o que trará algumas opções para o bootstrapping (lançamento) do projeto:

Image description

Para esse tutorial, selecionamos a opção Create a JavaScript project. Você será questionado com uma série de perguntas para as quais você pode digitar enter. A última delas irá instalar as dependências do projeto.

npm install --save-dev @nomicfoundation/hardhat-toolbox@^1.0.1
Enter fullscreen mode Exit fullscreen mode

Após o comando npx hardhat, o projeto possuirá 3 pastas:

  • contracts/ é onde estarão os arquivos fonte do seu contrato.
  • test/ é onde se darão os testes.
  • scripts/ é para onde vão os scripts simples de automação.

Além disso, um arquivo hardhat.config.js terá sido gerado. Este arquivo irá gerenciar os plugins e dependências que podem ser visualizados pelo hardhat. Os plugins e dependências terão que ser instalados primeiro, seguidos pela solicitação dele no arquivo hardhat.config.js.

O comando npx hardhat também terá criado um modelo de contrato Lock que não será necessário para este guia. Assim, você poderá ir em frente e removê-lo:

rm contracts/Lock.sol test/Lock.js
Enter fullscreen mode Exit fullscreen mode

Agora que o Hardhat foi configurado, podemos começar a criar nosso token ERC20.

Contrato ERC20

O OpenZeppelin, que lidera o avanço do padrão ERC20, fornece uma biblioteca abrangente para o desenvolvimento seguro de contratos inteligentes, que usaremos para implementar nosso token. Para fazer isso, precisamos primeiro instalar o pacote de contratos OpenZeppelin.

npm install @openzeppelin/contracts
Enter fullscreen mode Exit fullscreen mode

Podemos então importar os contratos OpenZeppelin em nosso próprio contrato, prefixando seu caminho com @openzeppelin/contracts/....

Antes de iniciar nosso primeiro contrato de token ERC20, precisamos completar o passo mais importante: nomear seu token! A prática padrão é fazer corresponder o nome do arquivo do contrato inteligente com o nome do seu token. Para este guia, vou nomear o token FunToken (token fungível) com um símbolo de FUN e, portanto, meu nome de arquivo de contrato inteligente será FunToken.sol. Sinta-se à vontade para escolher seu próprio nome para o token, mas lembre-se de substituir qualquer instância do FunToken por seu próprio nome especialmente criado.

O novo contrato será colocado na pasta contracts/:

touch contracts/FunToken.sol
Enter fullscreen mode Exit fullscreen mode

Abra o FunToken.sol no seu editor de código e adicione o seguinte código. O código utilizará a função constructor do Solidity para cunhar o FunToken na implantação do contrato:

// contracts/FunToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract FunToken is ERC20 {

    // Define o suprimento do FunToken: 1,000,000
    uint256 constant initialSupply = 1000000 * (10**18);

    // O constructor será chamado na criação do contrato
    constructor() ERC20("FunToken", "FUN") {
        _mint(msg.sender, initialSupply);
    }
} 
Enter fullscreen mode Exit fullscreen mode

Importaremos o contrato ERC20 do OpenZeppelin através do caminho @openzeppelin/contracts/token/ERC20/ERC20.sol. Você pode encontrar o caminho para o contrato através do docs do OpenZeppelin ou em seu github.

Como parte da definição de initialSupply, o suprimento específico de FunToken é multiplicado por (10**18) pois o Solidity só suporta o uso de números inteiros. Como alternativa, a especificação ERC20 fornece um campo decimal que define as casas decimais do token. Para este guia, nos ateremos ao padrão de 18 casas decimais a fim de evitar ter que substituir a especificação. Sinta-se livre para alterar o fornecimento inicial selecionando um número inteiro diferente de 1000000 que foi utilizado, mas lembre-se de manter o (10**18).

Image description

ERC20.sol

Salve o contrato FunToken.sol e agora podemos compilar o contrato inteiro.

Compilando o Contrato

Para que a Máquina Virtual Ethereum (EVM) possa executar nosso código, precisamos compilar nosso código Solidity na EVM compatível com a bytecode EVM. Para garantir que não haja problemas com a versão, podemos especificar uma versão do Solidity no arquivo hardhat.config.js.

require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-ethers");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.9",
};
Enter fullscreen mode Exit fullscreen mode

Para compilar o contrato FunToke, podemos usar o comando compile disponível no Hardhat.

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

Image description

O Hardhat terá compilado o contrato em uma nova pasta artifacts/ criada. Apesar de nós só termos escrito o código para o FunToken.sol, o terminal mostra que existem 5 arquivos compilados. Isto se deve ao modo como o Solidity lida com a importação de @openzeppelin/contracts e suas dependências.

Implantando o Contrato Localmente

Antes de implantar o contrato em uma rede pública, é a melhor prática testar primeiro o contrato em uma blockchain local. O Hardhat simplifica o processo dessa configuração, tendo uma blockchain local embutida que pode ser facilmente executada através de uma única linha de código:

npx hardhat node
Enter fullscreen mode Exit fullscreen mode

Execute o comando acima em uma janela de comando separada.

Image description

A rede Hardhat imprimirá o endereço, bem como uma lista de contas geradas localmente. Observe que esta blockchain local armazena apenas as interações até que o terminal seja fechado, portanto, o estado não é preservado entre as execuções. Além disso, reserve um tempo para ler os detalhes das contas, pois é importante que você não use essas contas de amostra para enviar dinheiro real. Com a blockchain local em funcionamento, podemos então começar a escrever nossos scripts de implantação. O Hardhat atualmente não tem um sistema de implantação nativo e, portanto, necessita de scripts.

Para nosso script de implantação, estaremos usando o plugin hardhat-ethers. Você pode encontrar a documentação para o plugin aqui. Para utilizá-lo no projeto, precisamos primeiro instalá-lo:

npm install --save-dev @nomiclabs/hardhat-ethers ethers
Enter fullscreen mode Exit fullscreen mode

Navegando no diretório scripts/, você perceberá que o arquivo deploy.js foi previamente criado. Podemos substituir a função main() pelo seguinte:

// scripts/deploy.js
const { ethers } = require("hardhat");

async function main() {

  // Obter o proprietário do contrato
  const contractOwner = await ethers.getSigners();
  console.log(`Deploying contract from: ${contractOwner[0].address}`);

  // Assistente do Hardhat para obter o objeto contractFactory do ether
  const FunToken = await ethers.getContractFactory('FunToken');

  // Implantar o contrato
  console.log('Deploying FunToken...');
  const funToken = await FunToken.deploy();
  await funToken.deployed();
  console.log(`FunToken deployed to: ${funToken.address}`)
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exitCode = 1;
  });
Enter fullscreen mode Exit fullscreen mode

O Hardhat implantará o contrato usando a primeira conta criada quando nós inicializamos o nó acima. A função _mint() no FunToken.sol irá cunhar o suprimento total de FUN para esse endereço de conta. Para acessar esse array de contas, podemos utilizar a função auxiliar getSigners() fornecida pelo plugin hardhat-ethers.

Salve o arquivo e agora estamos prontos para implantar o contrato FunToken!

npx hardhat run --network localhost scripts/deploy.js
Enter fullscreen mode Exit fullscreen mode

Image description

Como mencionado, estamos implantando o contrato para nosso localhost. Tome nota do endereço do contrato implantado, pois precisaremos dele na próxima seção para interagir com nosso programa de contrato de forma programática.

Interagindo com o Contrato

Esta seção é opcional, mas é uma boa introdução a como interagir com contratos inteligentes usando o pacote ethers.js. Por meio desta interação, você também poderá experimentar por si mesmo o conjunto de funções padrão que sua ficha ERC20 herdou do contrato OpenZeppelin.

Obtendo o Contrato Implantado

Para obter a instância de nosso contrato de implantação, você precisará do endereço do contrato retornado pelo comando de implantação. Você pode substituir o contractAddress no código abaixo pelo fornecido por sua máquina local.

// scripts/interact.js
const { ethers } = require("hardhat");

async function main() {
    console.log('Getting the fun token contract...');
    const contractAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3';
    const funToken = await ethers.getContractAt('FunToken', contractAddress);
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exitCode = 1;
    });
Enter fullscreen mode Exit fullscreen mode

Com isso, nós configuramos o objeto funToken que é uma abstração de um contrato implantado na rede Ethereum, neste caso uma rede local. Este objeto funToken permite uma maneira simples de serializar chamadas e transações para um contrato on-chain e desserializar seus registros de resultados. Você pode consultar a documentação ethers para mais informações.

As seções abaixo mostram como chamar as funções públicas padrão que nosso contrato FunToken herdou do contrato ERC20 do OpenZeppelin. Lembre-se de adicionar cada seção à função main() a fim de chamar o contrato. A fim de executar o script interact.js, digite o seguinte comando em seu terminal:

npx hardhat run --network localhost scripts/interact.js
Enter fullscreen mode Exit fullscreen mode

name()

name() retorna o nome do token utilizado no constructor FunToken.sol.

// name()
  console.log('Querying token name...');
  const name = await funToken.name();
  console.log(`Token Name: ${name}\n`);
Enter fullscreen mode Exit fullscreen mode

Image description

symbol()

symbol() retorna o símbolo do token usado no constructor FunToken.sol.

// symbol()
    console.log('Querying token symbol...');
    const symbol = await funToken.symbol();
    console.log(`Token Symbol: ${symbol}\n`);
Enter fullscreen mode Exit fullscreen mode

Image description

decimals()

decimals() retorna os decimais padrão especificados pelo contrato ERC20.

// decimals()
    console.log('Querying decimals...');
    const decimals = await funToken.decimals();
    console.log(`Token Decimals: ${decimals}\n`);
Enter fullscreen mode Exit fullscreen mode

Image description

totalSupply()

totalSupply() retorna o initialSupply que foi usado no constructor FunToken.sol.

// totalSupply()
    console.log('Querying token supply...');
    const totalSupply = await funToken.totalSupply();
    console.log(`Total Supply including all decimals: ${totalSupply}`);
    console.log(`Total supply including all decimals comma separated: ${ethers.utils.commify(totalSupply)}`);
    console.log(`Total Supply in FUN: ${ethers.utils.formatUnits(totalSupply, decimals)}\n`);
Enter fullscreen mode Exit fullscreen mode

Como mencionado enquanto o contrato FunToken.sol foi criado, o Solidity suporta apenas números inteiros. No mundo Ethereum os números são geralmente representados em “wei” com o objetivo de manter precisão nos cálculos de valores. O Wei é a unidade base do ETH e 1 ETH = 10¹⁸ wei.

Consequentemente, observe que o tamanho da quantidade de tokens retornado não é ideal para leitura. Como tal, utilizamos os utils nativos para o plugin hardhat-ethers a fim de tornar os números exibidos mais legíveis. Este utils será coberto com mais detalhes quando começarmos a realizar transferências.

Image description

balanceOf(address account)

balanceOf() devolve a quantidade de tokens de propriedade pelo address de entrada.

// balanceOf(address account)
    console.log('Getting the balance of contract owner...');
    const signers = await ethers.getSigners();
    const ownerAddress = signers[0].address;
    let ownerBalance = await funToken.balanceOf(ownerAddress);
    console.log(`Contract owner at ${ownerAddress} has a ${symbol} balance of ${ethers.utils.formatUnits(ownerBalance, decimals)}\n`);
Enter fullscreen mode Exit fullscreen mode

Observe que todos os tokens FUN foram inicialmente cunhados para o endereço que foi implantado no contrato.

Image description

transfer(address to, uint256 amount)

transfer() move o amount de token do endereço from para o endereço to.

// transfer(to, amount)
    console.log('Initiating a transfer...');
    const recipientAddress = signers[1].address;
    const transferAmount = 100000;
    console.log(`Transferring ${transferAmount} ${symbol} tokens to ${recipientAddress} from ${ownerAddress}`);
    await funToken.transfer(recipientAddress, ethers.utils.parseUnits(transferAmount.toString(), decimals));
    console.log('Transfer completed');
    ownerBalance = await funToken.balanceOf(ownerAddress);
    console.log(`Balance of owner (${ownerAddress}): ${ethers.utils.formatUnits(ownerBalance, decimals)} ${symbol}`);
    let recipientBalance = await funToken.balanceOf(recipientAddress);
    console.log(`Balance of recipient (${recipientAddress}): ${ethers.utils.formatUnits(recipientBalance, decimals)} ${symbol}\n`);
Enter fullscreen mode Exit fullscreen mode

Devido ao formato de número inteiro da EVM, uma limitação que encontramos quando interagimos com nosso contrato via JavaScript é que os números JS são representados usando o formato de ponto flutuante de dupla precisão. Em termos leigos, os números após 2⁵³ -1 (9,007,199,254,740,991) não são seguros de usar, especialmente porque estamos essencialmente movendo tokens que (espera-se) terão um valor monetário. A fim de assegurar o manuseio correto dos números em nossos arquivos JS, podemos usar o util nativo BigNumber fornecido pelos ethers. O util BigNumber também ajudará a converter todos os valores finais em string, o que é fundamental para evitar qualquer tipo de erro ao interagir com ETH usando JS.

Image description

approve(address spender, uint256 amount)

approve() define amount como a permissão do spender sobre o token do chamador.

 // approve(address spender, uint256 amount)
    console.log(`Setting allowance amount of spender over the caller\'s ${symbol} tokens...`);
    const approveAmount = 10000;
    console.log(`This example allows the contractOwner to spend up to ${approveAmount} of the recipient\'s ${symbol} token`);
    const signerContract = funToken.connect(signers[1]); // Cria uma nova instância do contrato conectado ao destinatário
    await signerContract.approve(ownerAddress, ethers.utils.parseUnits(approveAmount.toString(), decimals));
    console.log(`Spending approved\n`);
Enter fullscreen mode Exit fullscreen mode

Observe que esta função se aplica aos saldos do chamador da transação. Como tal, precisamos criar uma nova instância do objeto funToken, signerContract que, em vez disso, está conectada à conta do destinatário.

Devido a possíveis condições de disputa quando a rede está confirmando esta transação, é recomendado o uso das funções increaseAllowance() e decreaseAllowance().

Image description

allowance(address owner, address spender)

allowance() retorna a quantidade restante do owner dos tokens que o spender tem permissão de gastar.

// allowance(address owner, address spender)
    console.log(`Getting the contracOwner spending allowance over recipient\'s ${symbol} tokens...`);
    let allowance = await funToken.allowance(recipientAddress, ownerAddress);
    console.log(`contractOwner Allowance: ${ethers.utils.formatUnits(allowance, decimals)} ${symbol}\n`);
Enter fullscreen mode Exit fullscreen mode

Image description

transferFrom(address from, address to, uint256 amount)

transferFrom() move o amount de tokens do endereço from para o endereço to.

// transferFrom(address from, address to, uint256 amount)
    const transferFromAmount = 100;
    console.log(`contracOwner transfers ${transferFromAmount} ${symbol} from recipient\'s account into own account...`);
    await funToken.transferFrom(recipientAddress, ownerAddress, ethers.utils.parseUnits(transferFromAmount.toString(), decimals));
    ownerBalance = await funToken.balanceOf(ownerAddress);
    console.log(`New owner balance (${ownerAddress}): ${ethers.utils.formatUnits(ownerBalance, decimals)} ${symbol}`);
    recipientBalance = await funToken.balanceOf(recipientAddress);
    console.log(`New recipient balance (${recipientAddress}): ${ethers.utils.formatUnits(recipientBalance, decimals)} ${symbol}`);
    allowance = await funToken.allowance(recipientAddress, ownerAddress);
    console.log(`Remaining allowance: ${ethers.utils.formatUnits(allowance, decimals)} ${symbol}\n`);
Enter fullscreen mode Exit fullscreen mode

Observe que esta função pode ser chamada por qualquer pessoa, pois serve para que os contratos possam transferir tokens em nome das contas de usuários aprovados. O valor será deduzido de acordo com a falha da transação se o amount exceder o valor permitido.

Image description

increaseAllowance(address spender, uint256 addedValue)

increaseAllowance() atomicamente aumenta o valor concedido ao spender pelo chamador.

 // increaseAllowance(address spender, uint256 addedValue)
    const incrementAmount = 100;
    console.log(`Incrementing contractOwner allowance by ${incrementAmount} ${symbol}...`);
    await signerContract.increaseAllowance(ownerAddress, ethers.utils.parseUnits(incrementAmount.toString(), decimals));
    allowance = await funToken.allowance(recipientAddress, ownerAddress);
    console.log(`Updated allowance: ${ethers.utils.formatUnits(allowance, decimals)} ${symbol}\n`);
Enter fullscreen mode Exit fullscreen mode

Image description

decreaseAllowance(address spender, uint256 subtractedValue)

decreaseAllowance() atomicamente reduz o valor concedido ao spender pelo chamador.

// decreaseAllowance(address spender, uint256 subtractedValue)
    const subtractAmount = 100;
    console.log(`Subtracting contractOwner allowance by ${subtractAmount} ${symbol}...`);
    await signerContract.decreaseAllowance(ownerAddress, ethers.utils.parseUnits(subtractAmount.toString(), decimals));
    allowance = await funToken.allowance(recipientAddress, ownerAddress);
    console.log(`Updated allowance: ${ethers.utils.formatUnits(allowance, decimals)} ${symbol}\n`);    
Enter fullscreen mode Exit fullscreen mode

Image description

...

Testando o Contrato

Dado que os tokens terão um valor monetário, escrever testes automatizados é fundamental para garantir que o contrato esteja funcionando como esperamos que esteja. Faremos uso dos pacotes chai e hardhat-chai-matchers para rapidamente escrevermos nossos próprios testes.

$ npm install --save-dev chai
$ npm install --save-dev @nomicfoundation/hardhat-chai-matchers
Enter fullscreen mode Exit fullscreen mode

Uma vez instalados, nós podemos criar um arquivo script de teste:

touch test/FunToken.test.js
Enter fullscreen mode Exit fullscreen mode

Como a maioria das funções do contrato foi implementada na seção acima, o código abaixo pretende ser mais um ponto de partida para que você possa então escrever um conjunto abrangente de testes.

const { expect } = require('chai');
const { ethers } = require("hardhat");

// Iniciar o bloco de teste
describe('FunToken', function () {
  before(async function () {
    this.FunToken = await ethers.getContractFactory('FunToken');
  });

  beforeEach(async function () {
    this.funToken = await this.FunToken.deploy();
    await this.funToken.deployed();

    this.decimals = await this.funToken.decimals();

    const signers = await ethers.getSigners();

    this.ownerAddress = signers[0].address;
    this.recipientAddress = signers[1].address;

    this.signerContract = this.funToken.connect(signers[1]);
  });

  // Casos de teste
  it('Creates a token with a name', async function () {
    expect(await this.funToken.name()).to.exist;
    // expect(await this.funToken.name()).to.equal('FunToken');
  });

  it('Creates a token with a symbol', async function () {
    expect(await this.funToken.symbol()).to.exist;
    // expect(await this.funToken.symbol()).to.equal('FUN');
  });

  it('Has a valid decimal', async function () {
    expect((await this.funToken.decimals()).toString()).to.equal('18');
  })

  it('Has a valid total supply', async function () {
    const expectedSupply = ethers.utils.parseUnits('1000000', this.decimals);
    expect((await this.funToken.totalSupply()).toString()).to.equal(expectedSupply);
  });

  it('Is able to query account balances', async function () {
    const ownerBalance = await this.funToken.balanceOf(this.ownerAddress);
    expect(await this.funToken.balanceOf(this.ownerAddress)).to.equal(ownerBalance);
  });

  it('Transfers the right amount of tokens to/from an account', async function () {
    const transferAmount = 1000;
    await expect(this.funToken.transfer(this.recipientAddress, transferAmount)).to.changeTokenBalances(
        this.funToken,
        [this.ownerAddress, this.recipientAddress],
        [-transferAmount, transferAmount]
      );
  });

  it('Emits a transfer event with the right arguments', async function () {
    const transferAmount = 100000;
    await expect(this.funToken.transfer(this.recipientAddress, ethers.utils.parseUnits(transferAmount.toString(), this.decimals)))
        .to.emit(this.funToken, "Transfer")
        .withArgs(this.ownerAddress, this.recipientAddress, ethers.utils.parseUnits(transferAmount.toString(), this.decimals))
  });

  it('Allows for allowance approvals and queries', async function () {
    const approveAmount = 10000;
    await this.signerContract.approve(this.ownerAddress, ethers.utils.parseUnits(approveAmount.toString(), this.decimals));
    expect((await this.funToken.allowance(this.recipientAddress, this.ownerAddress))).to.equal(ethers.utils.parseUnits(approveAmount.toString(), this.decimals));
  });

  it('Emits an approval event with the right arguments', async function () {
    const approveAmount = 10000;
    await expect(this.signerContract.approve(this.ownerAddress, ethers.utils.parseUnits(approveAmount.toString(), this.decimals)))
        .to.emit(this.funToken, "Approval")
        .withArgs(this.recipientAddress, this.ownerAddress, ethers.utils.parseUnits(approveAmount.toString(), this.decimals))
  });

  it('Allows an approved spender to transfer from owner', async function () {
    const transferAmount = 10000;
    await this.funToken.transfer(this.recipientAddress, ethers.utils.parseUnits(transferAmount.toString(), this.decimals))
    await this.signerContract.approve(this.ownerAddress, ethers.utils.parseUnits(transferAmount.toString(), this.decimals))
    await expect(this.funToken.transferFrom(this.recipientAddress, this.ownerAddress, transferAmount)).to.changeTokenBalances(
        this.funToken,
        [this.ownerAddress, this.recipientAddress],
        [transferAmount, -transferAmount]
      );
  });

  it('Emits a transfer event with the right arguments when conducting an approved transfer', async function () {
    const transferAmount = 10000;
    await this.funToken.transfer(this.recipientAddress, ethers.utils.parseUnits(transferAmount.toString(), this.decimals))
    await this.signerContract.approve(this.ownerAddress, ethers.utils.parseUnits(transferAmount.toString(), this.decimals))
    await expect(this.funToken.transferFrom(this.recipientAddress, this.ownerAddress, ethers.utils.parseUnits(transferAmount.toString(), this.decimals)))
        .to.emit(this.funToken, "Transfer")
        .withArgs(this.recipientAddress, this.ownerAddress, ethers.utils.parseUnits(transferAmount.toString(), this.decimals))
  });

  it('Allows allowance to be increased and queried', async function () {
    const initialAmount = 100;
    const incrementAmount = 10000;
    await this.signerContract.approve(this.ownerAddress, ethers.utils.parseUnits(initialAmount.toString(), this.decimals))
    const previousAllowance = await this.funToken.allowance(this.recipientAddress, this.ownerAddress);
    await this.signerContract.increaseAllowance(this.ownerAddress, ethers.utils.parseUnits(incrementAmount.toString(), this.decimals));
    const expectedAllowance = ethers.BigNumber.from(previousAllowance).add(ethers.BigNumber.from(ethers.utils.parseUnits(incrementAmount.toString(), this.decimals)))
    expect((await this.funToken.allowance(this.recipientAddress, this.ownerAddress))).to.equal(expectedAllowance);
  });

  it('Emits approval event when alllowance is increased', async function () {
    const incrementAmount = 10000;
    await expect(this.signerContract.increaseAllowance(this.ownerAddress, ethers.utils.parseUnits(incrementAmount.toString(), this.decimals)))
        .to.emit(this.funToken, "Approval")
        .withArgs(this.recipientAddress, this.ownerAddress, ethers.utils.parseUnits(incrementAmount.toString(), this.decimals))
  });

  it('Allows allowance to be decreased and queried', async function () {
    const initialAmount = 100;
    const decrementAmount = 10;
    await this.signerContract.approve(this.ownerAddress, ethers.utils.parseUnits(initialAmount.toString(), this.decimals))
    const previousAllowance = await this.funToken.allowance(this.recipientAddress, this.ownerAddress);
    await this.signerContract.decreaseAllowance(this.ownerAddress, ethers.utils.parseUnits(decrementAmount.toString(), this.decimals));
    const expectedAllowance = ethers.BigNumber.from(previousAllowance).sub(ethers.BigNumber.from(ethers.utils.parseUnits(decrementAmount.toString(), this.decimals)))
    expect((await this.funToken.allowance(this.recipientAddress, this.ownerAddress))).to.equal(expectedAllowance);
  });

  it('Emits approval event when alllowance is decreased', async function () {
    const initialAmount = 100;
    const decrementAmount = 10;
    await this.signerContract.approve(this.ownerAddress, ethers.utils.parseUnits(initialAmount.toString(), this.decimals))
    const expectedAllowance = ethers.BigNumber.from(ethers.utils.parseUnits(initialAmount.toString(), this.decimals)).sub(ethers.BigNumber.from(ethers.utils.parseUnits(decrementAmount.toString(), this.decimals)))
    await expect(this.signerContract.decreaseAllowance(this.ownerAddress, ethers.utils.parseUnits(decrementAmount.toString(), this.decimals)))
        .to.emit(this.funToken, "Approval")
        .withArgs(this.recipientAddress, this.ownerAddress, expectedAllowance)
  });

});
Enter fullscreen mode Exit fullscreen mode

Ao salvar o código acima em FunToken.test.js, podemos executar os testes usando:

npx hardhat test
Enter fullscreen mode Exit fullscreen mode

Se tudo der certo, você deve ver os resultados do teste impressos no seu terminal:

Image description

Brinque com os vários cenários de teste que são adequados para seu token. Você pode até mesmo tentar implementar casos de teste negativos que não foram incluídos na amostra acima.

Implantando o Contrato Publicamente

Agora que você está confiante de que o contrato está funcionando como esperado, finalmente chegou o momento de implantar FunToken na rede pública! Esta é a parte empolgante, pois qualquer pessoa conectada à rede conseguirá interagir com seu token. Para o objetivo deste guia, estaremos implantando o contrato na testnet Goerli. A testnet Goerli funciona de forma idêntica à mainnet (rede principal) exceto pelo valor em dólares vinculado ao ETH. Como tal, é o ambiente perfeito para experimentar seus contratos antes de implantar na rede principal. Observe que também estamos implantando para Goerli, já que muitos dos outros testes (Ropsten, Rinkeby, Kiln) serão (N.T. foram) interrompidos após a fusão da ETH.

A fim de implantar a Goerli, precisaremos primeiro de uma fonte de ETH Goerli, aqui referida apenas como ETH. Isto é necessário porque as transações na rede pública precisam ser processadas por um minerador que exige uma taxa de gas a ser paga. Até agora, temos testado o contrato em nossa rede local onde todas as contas foram geradas localmente com uma quantidade definida de ETH. Nas redes públicas, todos os novos ETH gerados são cunhados para os mineradores e, por isso, precisamos obter ETH para executar nosso código de contrato.

A maneira mais fácil de adquirir algum ETH é através das faucets (torneiras). As faucets são sites financiados pela comunidade onde os usuários podem solicitar que o ETH seja enviado para uma carteira privada. Observe que é melhor enviar estes fundos para uma carteira de desenvolvedor separada, pois as chaves privadas serão necessárias em texto simples mais tarde. As faucets são usadas no ambiente de teste como uma forma de circular ETH de teste para que os desenvolvedores possam usar. Você pode facilmente encontrar tais sites fazendo uma busca, mas eu tenho links para 2 desses sites abaixo:

Uma vez que você tenha algum ETH, vamos em frente e adicione a rede ao nosso hardhat.config.js para que o Hardhat saiba aonde fazer deploy.

require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-ethers");
require("@nomicfoundation/hardhat-chai-matchers")

// Vá para https://www.alchemyapi.io, inscreva-se, crie
// um novo App no seu dashboard e substitua "KEY" pela chave
const ALCHEMY_API_KEY = "YOUR OWN API KEY HERE";

// Substitua esta chave privada pela chave privada de sua conta Goerli
// Para exportar sua chave privada do Metamask, abra a Metamask e
// vá para Account Details > Export Private Key
// Cuidado: NUNCA ponha Ether real em suas contas de teste
const GOERLI_PRIVATE_KEY = "SUA PRÓPRIA CHAVE PRIVADA AQUI";

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.9",
  networks: {
    goerli: {
      url: `https://eth-goerli.alchemyapi.io/v2/${ALCHEMY_API_KEY}`,
      accounts: [GOERLI_PRIVATE_KEY]
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Para que o Hardhat possa implantar o contrato na Goerli, ele precisa de duas coisas adicionais:

  • Um nó Goerli ao qual possa se conectar a fim de enviar a transação à rede
  • A carteira que será utilizada para a implantação do contrato

Para que a API se conecte ao nó, você pode gerar um, inscrevendo-se em alchemy.io, crie um aplicativo e copie a API KEY (CHAVE API) sob o "VIEW KEY" (VISUALIZAR CHAVE) no dashboard do aplicativo. Observe que você só precisa se inscrever na conta gratuita. Então, você pode simplesmente ignorar as informações de pagamento.

Image description

alchemy.io

A seguir, precisaremos da chave privada da conta que vai pagar pela implantação do contrato. A fim de obter a chave privada da conta que recebeu o ETH da faucet, você pode exportá-la diretamente da Metamask sob os "Account details" (Detalhes da conta). Copie e cole essa chave privada no arquivo config.

Image description

Com isso, estamos agora prontos para implantar o contrato, utilizando o seguinte comando:

npx hardhat run scripts/deploy.js --network goerli
Enter fullscreen mode Exit fullscreen mode

Você deve ter percebido que, ao contrário de nosso ambiente local, a confirmação da transação não foi instantânea e isto porque a transação teve que ser processada pela testnet da Goerli. Isto lhe dá uma ideia de como o ciclo de desenvolvimento pode ser demorado se não for para um ambiente local. Observe também que os saldos do ETH na conta especificada diminuíram em um pequeno montante devido às taxas de gas.

Uma vez implantado, seu terminal deve fornecer o endereço do contrato recém implantado:

Image description

Lembre-se de que o FunToken é cunhado para a carteira que implantou o contrato. Portanto, para visualizar o token, precisaremos adicionar nosso contrato à Metamask. Abra a Metamask e navegue até a aba de ativos onde você verá um link "Import tokens" (Importar tokens). Selecione o link e digite o endereço do contrato. Os campos relevantes deverão então ser preenchidos automaticamente.

Image description

Uma vez importado, você poderá ver 1.000.000 FUN tokens em sua carteira!

Image description

A partir daqui, você pode enviar e receber token. Por exemplo, eu enviei metade dos tokens para minha outra carteira dev:

Image description

Todas essas transações podem ser visualizadas publicamente, portanto você pode usar ferramentas como o Etherscan para visualizar e analisar as transações. Por exemplo, a transação que acabei de enviar pode ser encontrada aqui:

Image description

Parabéns, você criou com sucesso seu próprio token ERC20! Lembre-se de que o valor de um token está relacionado ao que ele permite que os usuários façam. Como tal, continue adicionando suas funcionalidades de token para se adequar ao que mais o estimula. Tudo o que há de melhor lá fora!

Obrigado por ficar até o final. Adoraria ouvir seus pensamentos/comentários. Portanto, deixe um comentário. Eu estou ativo no twitter @AwKaiShin se você quiser receber petiscos mais digeríveis sobre informações criptográficas ou visitar meu site pessoal se quiser meus serviços :)

Esse artigo foi escrito por Aw Kai Shin e traduzido por Fátima Lima. O original pode ser lido aqui.

Top comments (0)