WEB3DEV

Cover image for Como Criar um Token ERC20 e um Contrato Vendor Solidity para Vender/Comprar seu Próprio Token
Panegali
Panegali

Posted on

Como Criar um Token ERC20 e um Contrato Vendor Solidity para Vender/Comprar seu Próprio Token

No desafio Scaffold-eth anterior, criamos um Staker dApp. Neste desafio, vamos criar um contrato Token Vendor (Fornecedor).

O objetivo do dApp

O objetivo deste desafio é criar seu próprio token ERC20 e um contrato Token Vendor que cuidará do processo de venda/compra de seu token trocando-o por ETH enviado pelo usuário.

O que você vai aprender?

  • O que é um Token ERC20.
  • Como cunhar um token ERC20.
  • Implementação do OpenZeppelin ERC20.
  • Propriedade de um contrato.
  • Como criar um contrato Token Vendor para vender/comprar seu token.

Além do conteúdo acima, aprenderemos muitos conceitos novos do Solidity e da Web3 e como escrever testes bem desenvolvidos para seu código Solidity. Vou pular algumas partes básicas, então, se você se sentir perdido, volte à primeira postagem do desafio e leia todas as explicações.

Alguns links muito úteis que você deve ter sempre em mente:

O que é um token ERC20?

Antes de começar, darei apenas uma visão geral do que é um token ERC20 citando diretamente a documentação da Ethereum.

Os tokens podem representar praticamente qualquer coisa na Ethereum:

  • Pontos de reputação numa plataforma online.
  • Habilidades de um personagem em um jogo.
  • Bilhete de loteria.
  • Ativos financeiros como uma ação em uma empresa.
  • Uma moeda fiduciária como o USD.
  • Uma onça de ouro.
  • E mais…

Um recurso tão poderoso da Ethereum deve ser tratado por um padrão robusto, certo? É exatamente aí que o ERC-20 desempenha seu papel! Esse padrão permite que os desenvolvedores criem aplicativos de token interoperáveis ​​com outros produtos e serviços.

O ERC-20 introduz um padrão para Tokens Fungíveis, ou seja, eles possuem uma propriedade que faz com que cada token seja exatamente igual (em tipo e valor) a outro token. Por exemplo, um token ERC-20 funciona exatamente como o ETH, ou seja, 1 token é e sempre será igual a todos os outros tokens.

Se você quiser saber mais sobre o token ERC-20, consulte estes links:

Configure o projeto

Primeiro de tudo, precisamos configurá-lo. Clone o repositório Scaffold-eth, mude para a ramificação do desafio (challenge 1) e instale todas as dependências necessárias.

git clone https://github.com/austintgriffith/scaffold-eth.git challenge-2-token-vendor
cd challenge-2-token-vendor
git checkout challenge-2-token-vendor
yarn install
Enter fullscreen mode Exit fullscreen mode

Para testar localmente seu aplicativo

  • yarn chain para iniciar sua cadeia local do Hardhat.
  • yarn start para iniciar seu aplicativo React local.
  • yarn deploy para implantar/reimplantar seu contrato e atualizar o aplicativo React.

Implementação do OpenZeppelin e do ERC20

O OpenZeppelin fornece produtos de segurança para construir, automatizar e operar aplicativos descentralizados.

Vamos usar o framework de contratos do OpenZeppelin para construir nosso próprio token ERC20.

O framework é uma biblioteca para o desenvolvimento seguro de contratos inteligentes. Construa sobre uma base sólida de código aprovado pela comunidade.

Se você quiser saber mais sobre a implementação do OpenZeppelin, pode seguir estes links:

Exercício Parte 1: Crie seu próprio token ERC20 e implante-o!

Na primeira parte do exercício, você precisa criar um contrato de token herdando o contrato ERC20 do OpenZepllein.

No construtor, você precisa cunhar 1000 tokens (lembre-se que no Solidity um token ERC20 tem 18 casas decimais) e enviar para o msg.sender (aquele que implantou o contrato).

Lembre-se de atualizar o arquivo deploy.js para enviar esses tokens para o endereço correto. Você pode encontrar seu endereço atual no canto superior direito do seu aplicativo web, basta clicar no ícone de copiar!

Para transferir tokens para sua conta, adicione esta linha ao seu deploy.js:

const result = await yourToken.transfer("**SEU ENDEREÇO DE FRONT-END**", utils.parseEther("1000"));
Enter fullscreen mode Exit fullscreen mode

Não se assuste, explicarei mais tarde, depois de revisar o código.

  • Você pode ver no front-end que o saldo (balanceOf) de sua carteira tem esses 1000 tokens?
  • Você pode transferir (transfer()) alguns desses tokens para outro endereço de carteira? Basta abrir uma nova janela anônima no Chrome, digitar seu endereço localhost e você terá uma nova conta de gravação para enviar esses tokens!

Conceitos importantes para dominar

YourToken.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

// Saiba mais sobre a implementação do ERC20 
na documentação do OpenZeppelin:
https://docs.openzeppelin.com/contracts/4.x/erc20
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract YourToken is ERC20 {
    constructor() ERC20("Scaffold ETH Token", "SET") {
        _mint(msg.sender, 1000 * 10 ** 18);
    }
}
Enter fullscreen mode Exit fullscreen mode

Como você pode ver, estamos importando o Contrato ERC20.sol da biblioteca do OpenZeppelin. Esse contrato é a implementação OpenZeppelin do padrão ERC20 e eles fizeram um trabalho incrível em termos de segurança e otimização!

Quando você tem is ERC20 em seu código, esse código faz seu contrato YourContract herdar todas as variáveis ​​de função/estado implementadas no Contrato ERC20 do OpenZeppelin.

O mais incrível é que tudo é de código aberto. Tente CMD+click na palavra-chave ERC20 ou na função _mint.

Como você pode ver quando o constructor do nosso contrato é chamado, também estamos chamando o construtor ERC20 passando dois argumentos. O primeiro é o name do nosso Token e o segundo é o symbol.

A segunda parte importante é a função _mint, vamos dar uma olhada nela.

O primeiro require que você vê é apenas a verificação de que o cunhador (minter, aquele que receberá todo o token cunhado) não é o endereço nulo.

_beforeTokenTransfer e _afterTokenTransfer são ganchos de função que são chamados após qualquer transferência de tokens. Isso inclui cunhagem e queima.

No restante do código, estamos atualizando o _totalSupply do token (no nosso caso seriam 1000 tokens com 18 casas decimais), atualizando o saldo (balance) do minter com a quantidade e estamos emitindo um evento Transfer.

Não é legal isso? E em nosso TokenContract chamamos apenas uma função.

Lembra que eu disse para atualizar o arquivo deploy.js para transferir todos esses tokens para nossa carteira no aplicativo Web? O código era este:

await yourToken.transfer('0xafDD110869ee36b7F2Af508ff4cEB2663f068c6A', utils.parseEther('1000'));
Enter fullscreen mode Exit fullscreen mode

transfer é outra função oferecida pela implementação do Contrato ERC20.

Não vou entrar muito em detalhes mas após verificar que ambos o sendere recipient não são o null address, a função irá verificar se o remetente tem saldo suficiente para transferir o valor solicitado, fará a transferência e também emitirá um evento Transfer.

Exercício Parte 2: Criar um Contrato Vendor

Nesta parte do exercício, vamos criar nosso Contrato Vendor.

O Vendor será responsável por permitir que os usuários troquem ETH por nosso Token. Para fazer isso precisamos:

  • Definir um preço para nosso token (1 ETH = 100 Tokens).
  • Implementar uma função buyToken() pagável. Para transferir tokens, observe a função transfer() exposta pela implementação do OpenZeppelin ERC20.
  • Emitir um evento BuyTokens que registrará quem é o comprador, a quantidade de ETH enviada e a quantidade de Token comprado.
  • Transferir todos os tokens para o contrato Vendor no momento da implantação.
  • Transferir a propriedade (ownership) do contrato Vendor (no momento da implantação) para nosso endereço de front-end (você pode vê-lo no canto superior direito do seu aplicativo web) para retirar o ETH do saldo

Conceitos importantes para dominar

Vendor.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "./YourToken.sol";

// Saiba mais sobre a implementação do ERC20 
na documentação do OpenZeppelin: https://docs.openzeppelin.com/contracts/4.x/api/access#Ownable
import "@openzeppelin/contracts/access/Ownable.sol";

contract Vendor is Ownable {

  // Nosso contrato de token
  YourToken yourToken;

  // preço do token para ETH
  uint256 public tokensPerEth = 100;

  // Evento que registra a operação de compra
  event BuyTokens(address buyer, uint256 amountOfETH, uint256 amountOfTokens);

  constructor(address tokenAddress) {
    yourToken = YourToken(tokenAddress);
  }

  /**
  * @notice Permitir que os usuários comprem tokens por ETH
  */
  function buyTokens() public payable returns (uint256 tokenAmount) {
    require(msg.value > 0, "Enviar ETH para comprar alguns tokens");

    uint256 amountToBuy = msg.value * tokensPerEth;

    // verificar se o contrato do fornecedor tem quantidade suficiente de tokens para a transação
    uint256 vendorBalance = yourToken.balanceOf(address(this));
    require(vendorBalance >= amountToBuy, "O contrato Vendor não tem tokens suficientes em seu saldo");

    // Transferir token para o remetente da msg.sender
    (bool sent) = yourToken.transfer(msg.sender, amountToBuy);
    require(sent, "Falha ao transferir o token para o usuário");

    // emite o evento
    emit BuyTokens(msg.sender, msg.value, amountToBuy);

    return amountToBuy;
  }

  /**
  * @notice Permitir que o proprietário do contrato retire ETH
  */
  function withdraw() public onlyOwner {
    uint256 ownerBalance = address(this).balance;
    require(ownerBalance > 0, "O proprietário não tem saldo para retirar");

    (bool sent,) = msg.sender.call{value: address(this).balance}("");
    require(sent, "Falha ao enviar o saldo do usuário de volta ao proprietário");
  }
}
Enter fullscreen mode Exit fullscreen mode

Vamos revisar a parte importante do código.

Em buyTokens() estamos verificando se o usuário nos enviou pelo menos algum ETH, caso contrário iremos reverter a transação (não seja pão-duro!). Lembre-se que para receber ETH nossa função deve ter a palavra-chave payable.

Depois disso, calculamos, com base no preço do token, quantos tokens ele receberá com a quantidade de ETH enviada.

Também estamos verificando se o contrato Vendor possui saldo de tokens suficiente para atender a solicitação de compra do usuário, caso contrário, revertemos a transação.

Se todas as verificações correrem bem, acionamos a função transfer do nosso Contrato de Token implementado dentro do contrato ERC20 que é herdado pelo Contrato de Token (veja a imagem acima para visualizar o código). Essa função está retornando um boolean que nos notificará se a operação foi bem-sucedida.

A última coisa a fazer é emitir o evento BuyTokens para notificar a blockchain que fechamos o negócio!

A função withdraw() é bem simples. Como você pode ver, depende do onlyOwner function modifier que herdamos do contrato Owner. Esse modificador está verificando se o msg.sender é o proprietário do contrato. Não queremos que outro usuário retire o ETH que coletamos. Dentro da função, estamos transferindo o ETH para o proprietário e verificando se a operação foi bem-sucedida. Outra maneira de fazer isso, como eu disse anteriormente, é usar o sendValue do utilitário Address do OpenZeppelin.

Exercício Parte 3: Permita que o Vendor compre de volta!

Esta é a última parte do exercício e é a mais difícil, não do ponto de vista da tecnologia, mas mais do ponto de vista conceitual e de UX (experiência do usuário).

Queremos permitir que o usuário venda seu token para nosso contrato Vendor. Como você sabe, o contrato pode aceitar ETH quando sua função é declarada como payable, mas eles só podem receber ETH.

Portanto, o que precisamos implementar é permitir que nosso Vendor pegue tokens diretamente de nosso saldo de tokens e confie nele para nos devolver o mesmo valor de ETH. Isso é chamado de “abordagem de aprovação”.

Este é o fluxo que acontecerá:

  • O usuário solicita “aprovar” o contrato Vendor para transferir os tokens do saldo do usuário para a carteira do Vendor (isso acontecerá no contrato do Token). Ao invocar a função approve, você especificará o número máximo de tokens que deseja permitir que o outro contrato seja capaz de transferir.
  • O usuário invocará uma função sellTokens no contrato Vendor que transferirá o saldo do usuário para o saldo do Vendor.
  • O contrato Vendor transferirá para a carteira do usuário uma quantidade igual de ETH.

Conceitos importantes para dominar

  • Função ERC20 approve — Define amount como a permissão de spender sobre os tokens do chamador. Retorna um valor booleano indicando se a operação foi bem-sucedida. Emite um evento Approval.
  • Função ERC20 transferFrom — Move amount de tokens do sender para o recipientusando o mecanismo de permissão. amount é então deduzido da permissão do chamador. Retorna um valor booleano indicando se a operação foi bem-sucedida. Emite um evento Transfer.

Uma observação importante que gostaria de explicar: UX acima da segurança

Este mecanismo de aprovação não é algo novo. Se você já usou uma DEX como a Uniswap, você já fez isso.

A função approve permite que outra carteira/contrato transfira no máximo o número de tokens que você especificar nos argumentos da função. O que isso significa? E se eu quiser negociar 200 tokens? Devo aprovar o contrato Vendor para transferir apenas 200 tokens para si mesmo. Se eu quiser vender mais 100, devo aprová-lo novamente. É uma boa UX? Talvez não, mas é a mais segura.

As DEXs usam outra abordagem. Para evitar pedir sempre ao usuário para aprovar cada vez que você deseja trocar TokenA por TokenB, eles simplesmente pedem para aprovar o número MAX possível de tokens diretamente. O que isso significa? Que todo contrato de DEX poderia potencialmente roubar todos os seus tokens sem que você soubesse. Você sempre deve estar ciente do que está acontecendo nos bastidores!

Vendor.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "hardhat/console.sol";
import "./YourToken.sol";

// Saiba mais sobre a implementação do ERC20 na documentação do OpenZeppelin: https://docs.openzeppelin.com/contracts/4.x/api/access#Ownable
import "@openzeppelin/contracts/access/Ownable.sol";

contract Vendor is Ownable {

  // Nosso contrato de token
  YourToken yourToken;

  // preço do token para ETH
  uint256 public tokensPerEth = 100;

  // Evento que registra a operação de compra
  event BuyTokens(address buyer, uint256 amountOfETH, uint256 amountOfTokens);
  event SellTokens(address seller, uint256 amountOfTokens, uint256 amountOfETH);

  constructor(address tokenAddress) {
    yourToken = YourToken(tokenAddress);
  }

  /**
  * @notice Permitir que os usuários comprem tokens por ETH
  */
  function buyTokens() public payable returns (uint256 tokenAmount) {
    require(msg.value > 0, "Enviar ETH para comprar alguns tokens");

    uint256 amountToBuy = msg.value * tokensPerEth;

    // verificar se o contrato Vendor tem quantidade suficiente de tokens para a transação
    uint256 vendorBalance = yourToken.balanceOf(address(this));
    require(vendorBalance >= amountToBuy, "O contrato Vendor não tem tokens suficientes em seu saldo");

    // Transferir token para o remetente msg.sender
    (bool sent) = yourToken.transfer(msg.sender, amountToBuy);
    require(sent, "Falha ao transferir o token para o usuário");

    // emite o evento
    emit BuyTokens(msg.sender, msg.value, amountToBuy);

    return amountToBuy;
  }

  /**
  * @notice Permite que os usuários vendam tokens por ETH
  */
  function sellTokens(uint256 tokenAmountToSell) public {
    // Verifica se a quantidade solicitada de tokens a serem vendidos é maior que 0
    require(tokenAmountToSell > 0, "Specify an amount of token greater than zero");

    // Verifica se o saldo de tokens do usuário é suficiente para fazer a troca
    uint256 userBalance = yourToken.balanceOf(msg.sender);
    require(userBalance >= tokenAmountToSell, "Your balance is lower than the amount of tokens you want to sell");

    // Verifica se o saldo do Vendor é suficiente para fazer a troca
    uint256 amountOfETHToTransfer = tokenAmountToSell / tokensPerEth;
    uint256 ownerETHBalance = address(this).balance;
    require(ownerETHBalance >= amountOfETHToTransfer, "O Vendor não tem fundos suficientes para aceitar a solicitação de venda");

    (bool sent) = yourToken.transferFrom(msg.sender, address(this), tokenAmountToSell);
    require(sent, "Falha ao transferir tokens do usuário para o vendor");


    (sent,) = msg.sender.call{value: amountOfETHToTransfer}("");
    require(sent, "Falha ao enviar ETH para o usuário");
  }

  /**
  * @notice Permite que o proprietário do contrato retire ETH
  */
  function withdraw() public onlyOwner {
    uint256 ownerBalance = address(this).balance;
    require(ownerBalance > 0, "O proprietário não tem saldo para retirar");

    (bool sent,) = msg.sender.call{value: address(this).balance}("");
    require(sent, "Falha ao enviar o saldo do usuário de volta ao proprietário");
  }
}
Enter fullscreen mode Exit fullscreen mode

Vamos revisar sellTokens.

Em primeiro lugar, verificamos se o tokenAmountToSell é maior do que 0, caso contrário, revertemos a transação. Você precisa vender pelo menos um de seus tokens!

Em seguida, verificamos se o saldo do token do usuário é pelo menos maior que a quantidade de token que ele está tentando vender. Você não pode vender mais do que possui!

Depois disso, calculamos o amountOfETHToTransfer para o usuário após a operação de venda. Precisamos ter certeza de que o Vendor pode pagar esse valor, então estamos verificando se o saldo do Vendor (em ETH) é maior que o valor a ser transferido para o usuário.

Se tudo estiver certo, prosseguimos com a operação (bool sent) = yourToken.transferFrom(msg.sender, address(this), tokenAmountToSell);. Estamos informando ao contrato YourToken para transferir tokenAmountToSell do saldo do usuário msg.sender para o saldo do Vendor address(this). Esta operação só pode ser bem-sucedida se o usuário já tiver aprovado pelo menos esse valor específico com a função approve que já analisamos.

A última coisa que fazemos é transferir o valor de ETH da operação de venda de volta para o endereço do usuário. E acabamos!

Atualize seu App.jsx

Para testar isso em seu aplicativo React, você pode atualizar seu App.jsx adicionando dois Card para Approvee Sell tokens (consulte o repositório de código do GitHub no final da postagem) ou pode fazer tudo na guia Contrato de depuração que oferece todos os características necessárias.

Exercício Parte 4: Crie um conjunto de testes

Você já sabe do artigo anterior, que os testes são uma ótima base para a segurança e otimização do seu aplicativo. Você nunca deve ignorá-los e eles são uma forma de entender o fluxo das operações que estão envolvidas na lógica do aplicativo como um todo.

Os testes no ambiente Solidity se baseiam em quatro bibliotecas:

Vamos revisar um teste e depois apresentarei todo o código

Testando a função sellTokens()

Este é o teste que verificará se nossas funções sellTokens funcionam conforme o esperado.

Vamos rever a lógica:

  • Em primeiro lugar, addr1 compra alguns tokens do contrato Vendor.
  • Antes de vender, como dissemos antes, precisamos aprovar o contrato Vendor para poder transferir para ele a quantidade de token que queremos vender.
  • Após a aprovação, verificamos novamente se a permissão de tokens do addr1 para o Vendor é pelo menos a quantidade de tokens que addr1 deseja vender (e transferir para o Vendor). Essa verificação pode ser ignorada porque sabemos que o OpenZeppelin já testou seu código, mas eu só queria adicioná-la para fins de aprendizado.
  • Estamos prontos para vender a quantidade de tokens que acabamos de comprar usando a função sellTokens do contrato Vendor.

Neste ponto, precisamos verificar três coisas:

  • O saldo de tokens do usuário é 0 (vendemos todos os nossos tokens).
  • A carteira do usuário aumentou em 1 ETH com essa transação.
  • O saldo de tokens do Vendor é 1000 (compramos 100 tokens).

O Waffle oferece alguns utilitários interessantes para verificar alterações no saldo de ether e alterações nos saldos dos tokens, mas, infelizmente, parece que há um problema no último (confira o problema do GitHub que acabei de criar).

Código completo da cobertura de teste

const {ethers} = require('hardhat');
const {use, expect} = require('chai');
const {solidity} = require('ethereum-waffle');

use(solidity);

describe('Staker dApp', () => {
  let owner;
  let addr1;
  let addr2;
  let addrs;

  let vendorContract;
  let tokenContract;
  let YourTokenFactory;

  let vendorTokensSupply;
  let tokensPerEth;

  beforeEach(async () => {
    // eslint-disable-next-line no-unused-vars
    [owner, addr1, addr2, ...addrs] = await ethers.getSigners();

    // Implantar o contrato ExampleExternalContract
    YourTokenFactory = await ethers.getContractFactory('YourToken');
    tokenContract = await YourTokenFactory.deploy();

    // Implantar o Contrato Staker
    const VendorContract = await ethers.getContractFactory('Vendor');
    vendorContract = await VendorContract.deploy(tokenContract.address);

    await tokenContract.transfer(vendorContract.address, ethers.utils.parseEther('1000'));
    await vendorContract.transferOwnership(owner.address);

    vendorTokensSupply = await tokenContract.balanceOf(vendorContract.address);
    tokensPerEth = await vendorContract.tokensPerEth();
  });

  describe('Testar o método buyTokens()', () => {
    it('buyTokens revertido sem envio de eth', async () => {
      const amount = ethers.utils.parseEther('0');
      await expect(
        vendorContract.connect(addr1).buyTokens({
          value: amount,
        }),
      ).to.be.revertedWith('Enviar ETH para comprar alguns tokens');
    });

    it('buyTokens revertido, o vendor não tem tokens suficientes', async () => {
      const amount = ethers.utils.parseEther('101');
      await expect(
        vendorContract.connect(addr1).buyTokens({
          value: amount,
        }),
      ).to.be.revertedWith('O contrato vendor não tem tokens suficientes em seu saldo');
    });

    it('sucesso do buyTokens!', async () => {
      const amount = ethers.utils.parseEther('1');

      // Verifique se o processo buyTokens foi bem-sucedido e se o evento foi emitido
      await expect(
        vendorContract.connect(addr1).buyTokens({
          value: amount,
        }),
      )
        .to.emit(vendorContract, 'BuyTokens')
        .withArgs(addr1.address, amount, amount.mul(tokensPerEth));

      // Verifique se o saldo de token do usuário é 100
      const userTokenBalance = await tokenContract.balanceOf(addr1.address);
      const userTokenAmount = ethers.utils.parseEther('100');
      expect(userTokenBalance).to.equal(userTokenAmount);

      // Verifique se o saldo de token do usuário é 100
      const vendorTokenBalance = await tokenContract.balanceOf(vendorContract.address);
      expect(vendorTokenBalance).to.equal(vendorTokensSupply.sub(userTokenAmount));

      // Verifique se o saldo de ETH do vendor é 1
      const vendorBalance = await ethers.provider.getBalance(vendorContract.address);
      expect(vendorBalance).to.equal(amount);
    });
  });

  describe('Testar o método withdraw()', () => {
    it('withdraw revertida porque não foi chamada pelo proprietário´, async () => {
      await expect(vendorContract.connect(addr1).withdraw()).to.be.revertedWith('Ownable: quem está chamando não é o proprietário');
    });

    it('withdraw revertida porque não foi chamada pelo proprietário', async () => {
      await expect(vendorContract.connect(owner).withdraw()).to.be.revertedWith('O proprietário não tem saldo para sacar');
    });

    it('withdraw bem sucedido', async () => {
      const ethOfTokenToBuy = ethers.utils.parseEther('1');

      // operação buyTokens
      await vendorContract.connect(addr1).buyTokens({
        value: ethOfTokenToBuy,
      });

      // operação de saque
      const txWithdraw = await vendorContract.connect(owner).withdraw();

      // Verifique se o saldo do Vendor tem 0 eth
      const vendorBalance = await ethers.provider.getBalance(vendorContract.address);
      expect(vendorBalance).to.equal(0);

      // Verifique se o saldo do proprietário foi alterado de 1 eth
      await expect(txWithdraw).to.changeEtherBalance(owner, ethOfTokenToBuy);
    });
  });

  describe('Testar o método sellTokens()', () => {
    it('sellTokens revertido porque tokenAmountToSell é 0', async () => {
      const amountToSell = ethers.utils.parseEther('0');
      await expect(vendorContract.connect(addr1).sellTokens(amountToSell)).to.be.revertedWith(
        'Especifique uma quantidade de token maior que zero',
      );
    });

    it('sellTokens revertido porque o usuário não tem tokens suficientes', async () => {
      const amountToSell = ethers.utils.parseEther('1');
      await expect(vendorContract.connect(addr1).sellTokens(amountToSell)).to.be.revertedWith(
        Seu saldo é menor do que a quantidade de tokens que você deseja vender,
      );
    });

    it('sellTokens revertido porque o fornecedor não tem tokens suficientes', async () => {
      // Usuário 1 compra
      const ethOfTokenToBuy = ethers.utils.parseEther('1');

      // operação buyTokens
      await vendorContract.connect(addr1).buyTokens({
        value: ethOfTokenToBuy,
      });

      await vendorContract.connect(owner).withdraw();

      const amountToSell = ethers.utils.parseEther('100');
      await expect(vendorContract.connect(addr1).sellTokens(amountToSell)).to.be.revertedWith(
        'O Vendor não tem fundos suficientes para aceitar a solicitação de venda',
      );
    });

    it('sellTokens revertido porque o usuário já aprovou a transferência', async () => {
      // Usuário 1 compra
      const ethOfTokenToBuy = ethers.utils.parseEther('1');

      // operação buyTokens
      await vendorContract.connect(addr1).buyTokens({
        value: ethOfTokenToBuy,
      });

      const amountToSell = ethers.utils.parseEther('100');
      await expect(vendorContract.connect(addr1).sellTokens(amountToSell)).to.be.revertedWith(
        'ERC20: o valor da transferência excede o permitido',
      );
    });

    it('sellTokens bem sucedido', async () => {
      // addr1 comprar 1 ETH de tokens
      const ethOfTokenToBuy = ethers.utils.parseEther('1');

      // operação buyTokens
      await vendorContract.connect(addr1).buyTokens({
        value: ethOfTokenToBuy,
      });

      const amountToSell = ethers.utils.parseEther('100');
      await tokenContract.connect(addr1).approve(vendorContract.address, amountToSell);

      // verificar se o Vendor pode transferir a quantidade de tokens que queremos vender
      const vendorAllowance = await tokenContract.allowance(addr1.address, vendorContract.address);
      expect(vendorAllowance).to.equal(amountToSell);

      const sellTx = await vendorContract.connect(addr1).sellTokens(amountToSell);

      // Verifique se o saldo de tokens do Vendor é 1000
      const vendorTokenBalance = await tokenContract.balanceOf(vendorContract.address);
      expect(vendorTokenBalance).to.equal(ethers.utils.parseEther('1000'));

      // Verifique se o saldo de tokens do usuário é 0
      const userTokenBalance = await tokenContract.balanceOf(addr1.address);
      expect(userTokenBalance).to.equal(0);

      // Verifique se o saldo de ETH do usuário é 1
      const userEthBalance = ethers.utils.parseEther('1');
      await expect(sellTx).to.changeEtherBalance(addr1, userEthBalance);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Etapa final: implantar seu contrato na lua (rede de testes)

Ok, agora é a hora. Implementamos nosso contrato inteligente, testamos a interface do usuário do front-end, cobrimos todos os casos extremos com nossos testes. Estamos prontos para implantá-lo na rede de testes.

Seguindo a documentação do Scaffold-eth, estes são os passos que precisamos seguir:

  1. Mude o defaultNetwork em packages/hardhat/hardhat.config.js para a rede de testes que você gostaria de usar (no meu caso a Rinkeby).
  2. Atualizado infuriaProjectIdc om um criado no Infura.
  3. Gere uma conta de implantador com yarn generate. Este comando deve gerar dois arquivos .txt. Um que representará o endereço da conta e outro com a frase inicial da conta gerada.
  4. Execute yarn account para ver os detalhes da conta, como saldos eth em diferentes redes.
  5. Certifique-se de que o arquivos mnemonic.txt e de contas relativas não sejam enviados com seu repositório git, caso contrário, qualquer pessoa pode obter a propriedade do seu contrato!
  6. Financie sua conta de implantador com alguns créditos. Você pode usar uma carteira instantânea para enviar fundos para o código QR que acabou de ver em seu console.
  7. Implante seu contrato com yarn deploy!

Nota do tradutor: A rede de testes Rinkeby foi descontinuada. Utilize a rede de testes Sepolia.

Se tudo correr bem, você verá algo como isto no seu console:

Os metadados de implantação são armazenados na pasta /deployments e copiados automaticamente para /packages/react-app/src/contracts/hardhat_contracts.json por meio do sinalizador --export-all no comando yarn deploy (consulte/packages/hardhat/packagen.json).

Se você deseja verificar o contrato implantado, pode procurá-los no site do Etherscan Rinkeby:

Atualize seu aplicativo front-end e implante-o no Surge!

Vamos usar o método Surge, mas você também pode implantar seu aplicativo no AWS S3 ou no IPFS, você decide!

As documentações do Scaffold-eth sempre são úteis, mas vou resumir o que você deve fazer:

  1. Se você estiver implantando na rede principal, verifique seu contrato no Etherscan. Este procedimento agregará credibilidade e confiança à sua inscrição. Se você estiver interessado em fazê-lo, basta seguir este guia para o Scaffold-eth.
  2. Desative o modo de depuração (ele imprime uma quantidade enorme de console.log, algo que você não deseja ver no Chrome Developer Console, acredite em mim!). Abra App.jsx, encontre const DEBUG = true; e altere-o para false.
  3. Dê uma olhada em App.jsx e remova todo o código não utilizado. Certifique-se de enviar apenas o que você realmente precisa!
  4. Certifique-se de que seu aplicativo React esteja apontando para a rede correta (aquela que você acabou de usar para implantar seu contrato). Procure por const targetNetwork = NETWORKS["localhost"]; e substitua localhost pela rede do seu contrato. No nosso caso, será rinkeby
  5. Certifique-se de estar usando seus próprios nós e não os do Scaffold-eth, pois eles são públicos e não há garantia de que serão retirados ou limitados por taxa. Revise as linhas 58 e 59 do App.jsx
  6. Atualize o constants.js e troque as chaves de API do Infura, Etherscan e Blocknative se quiser usar os serviços deles.

Estamos prontos? Vamos lá!

Agora compile seu aplicativo React com o yarn build e, quando o script de compilação terminar, implante-o no Surge com o yarn surge.

Se tudo correr bem, você verá algo parecido com isto. Seu dApp agora está ativo no Surge!

Você pode conferir nosso dApp implantado aqui: https://woozy-cable.surge.sh/

Recapitulação e conclusões

Isso é o que aprendemos e fizemos até agora

  • Clonamos o repositório do desafio Scaffold-eth;
  • Aprendemos muitos conceitos da Web3/Solidity (análise profunda no contrato ERC20, padrão de aprovação e assim por diante);
  • Criamos um contrato de Token ERC20;
  • Criamos um contrato Vendor para permitir que os usuários os comprem e vendam;
  • Testamos nosso contrato localmente na rede Hardhat;
  • Implantamos nosso contrato no Rinkeby;
  • Implantamos nosso dApp no Surge.

Se tudo funcionar como esperado, você está pronto para dar o grande salto e implantar tudo na rede principal da Ethereum!

Repositório do GitHub para este projeto: scaffold-eth-challenge-2-token-vendor

Você gostou deste conteúdo? Siga-me para mais!


Artigo escrito por StErMi. Traduzido por Marcelo Panegali.

Top comments (0)