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:
- Solidity by Example.
- Documentação do Solidity.
- Documentação do Hardhat.
- Documentação do Ethers-js.
- Documentação do OpenZeppelin.
- Tutorial do OpenZeppelin Ethernaut.
- Tutorial do CryptoZombies.
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
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.
- Implementações de padrões como ERC20 e ERC721.
- Esquema flexível de permissões baseadas em funções.
- Componentes reutilizáveis do Solidity para construir contratos personalizados e sistemas descentralizados complexos.
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"));
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
- Contrato OpenZeppelin ERC20.
- Padrão Ethereum ERC-20.
-
Herança — Os contratos podem herdar de outros contratos usando a palavra-chave
is
. - Sombreamento de variáveis de estado herdadas — Conforme explicado pelo SolidityByCode, ao contrário das funções, as variáveis de estado não podem ser substituídas declarando-as novamentente no contrato filho
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);
}
}
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'));
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 sender
e 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çãotransfer()
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
- Eventos.
- Funções pagáveis (payable).
-
Contratos Próprietário (ownable) e de Propriedade (ownership) do Open Zeppelin — Módulo do OpenZeppelin usado por meio de herança. Ele disponibilizará o modificador
onlyOwner
, que pode ser aplicado às suas funções para restringir seu uso ao proprietário. - Utilitário de endereço (Address) do OpenZeppelin - (não necessário, mas útil para conhecido) — Coleção de funções relacionadas ao tipo de endereço. Você pode usá-lo para transferir fundos ETH com segurança do Vendor para o proprietário
-
Função de transferência do contrato OpenZeppelin ERC20 —
transfer(address recipient, uint256 amount)
move a quantidade (amount
) dos tokens da conta do chamador para orecipient
e retorna um valor booleano indicando se a operação foi bem-sucedida. -
Enviando ether — Como vimos no desafio anterior, sempre use a função
call
para fazer isso!
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");
}
}
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 despender
sobre os tokens do chamador. Retorna um valor booleano indicando se a operação foi bem-sucedida. Emite um eventoApproval
. -
Função ERC20 transferFrom — Move
amount
de tokens dosender
para orecipient
usando 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");
}
}
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 Approve
e 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);
});
});
});
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:
- Mude o
defaultNetwork
empackages/hardhat/hardhat.config.js
para a rede de testes que você gostaria de usar (no meu caso a Rinkeby). - Atualizado
infuriaProjectId
c om um criado no Infura. - 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. - Execute
yarn account
para ver os detalhes da conta, como saldos eth em diferentes redes. - 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!
- 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.
- 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 comandoyarn 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:
- 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.
- 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
, encontreconst DEBUG = true;
e altere-o parafalse
. - 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! - 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 substitualocalhost
pela rede do seu contrato. No nosso caso, serárinkeby
- 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
- 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!
- GitHub: https://github.com/StErMi
- Twitter: https://twitter.com/StErMi
- Medium: https://medium.com/@stermi
- Dev.to: https://dev.to/stermi
Artigo escrito por StErMi. Traduzido por Marcelo Panegali.
Top comments (0)