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:
- ERC721 Using Hardhat: An Updated Comprehensive Guide To NFTs
- Creating Truly Decentralised NFTs — A Comprehensive Guide to ERC721 & IPFS
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
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:
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
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
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
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
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);
}
}
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)
.
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",
};
Para compilar o contrato FunToke
, podemos usar o comando compile disponível no Hardhat.
npx hardhat compile
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
Execute o comando acima em uma janela de comando separada.
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
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;
});
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
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;
});
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
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`);
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`);
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`);
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`);
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.
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`);
Observe que todos os tokens FUN
foram inicialmente cunhados para o endereço que foi implantado no contrato.
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`);
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.
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`);
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()
.
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`);
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`);
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.
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`);
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`);
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
Uma vez instalados, nós podemos criar um arquivo script de teste:
touch test/FunToken.test.js
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)
});
});
Ao salvar o código acima em FunToken.test.js
, podemos executar os testes usando:
npx hardhat test
Se tudo der certo, você deve ver os resultados do teste impressos no seu terminal:
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:
- goerlifaucet.com: Requer conta na Alchemy
- goerli-faucet.pk910.de: Usa o poder da computação para reduzir spam
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]
}
}
};
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.
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.
Com isso, estamos agora prontos para implantar o contrato, utilizando o seguinte comando:
npx hardhat run scripts/deploy.js --network goerli
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:
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.
Uma vez importado, você poderá ver 1.000.000 FUN tokens em sua carteira!
A partir daqui, você pode enviar e receber token. Por exemplo, eu enviei metade dos tokens para minha outra carteira dev:
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:
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.
Latest comments (0)