WEB3DEV

Cover image for Teste de Hardhat Sem Conhecer JavaScript
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Teste de Hardhat Sem Conhecer JavaScript

https://miro.medium.com/v2/resize:fit:720/format:webp/1*rwgBmJnwTOx2N1qxH5Rbxg.png

Quer ser um auditor de contratos inteligentes? Entre em contato comigo para conhecer caminhos de aprendizagem, conselhos e desafios do mundo real por e-mail em [email protected] ou em X como @azdraft_.

Enfrentar o desafio de sair da Web2 para a última fronteira a ser descoberta, a Web3, é um desafio titânico. Ainda mais quando se vem de um contexto em segurança cibernética, em vez de ter sido um desenvolvedor full stack (aquele profissional multitarefas que cobre várias frentes na área de TI, que pode trabalhar com diferentes linguagens, habilitado a oferecer um suporte completo). Os pré-requisitos que instituições como a Alchemy University, a Moralis Academy ou o Encode Club, para citar algumas, afirmam repetidamente são “conhecimento prévio de JavaScript”.

E esse é o meu caso. Depois de aprender Solidity, trabalhar em vários projetos e resolver vários desafios de CTF, há algumas semanas comecei o Curso de Hacking de Contrato Inteligente, sem dúvida o maior esforço para treinar auditores até o momento, mas parecia que a barreira estava na minha frente novamente “Usaremos o Hardhat porque é mais fácil do que o Foundry”.

Eu discordei. Porque eu sei que a maioria deles vem, justamente, de serem desenvolvedores JavaScript na sua vida antes dessa formação, mas, para nós, significa aprender uma linguagem para usá-la apenas para testes.

Agora, mudei de ideia. Depois dessas semanas, posso dizer em alto e bom som: você não precisa aprender JavaScript para ser um desenvolvedor ou auditor de contratos inteligentes. Esses dias acabaram. Primeiro, obviamente, porque pode usar o Foundry. Segundo, porque você só precisa de uma armação e de algumas instruções para criar seus testes com Hardhat.

O Código para Testar

Vamos criar um teste para um contrato ERC20 bem simples, feito com o OpenZeppelin:

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

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

contract ErcContract is ERC20, Ownable {

    constructor() ERC20("Erc201", "SCHC") {}

    function mint(address to, uint256 amount) external onlyOwner {
        _mint(to, amount);
    }
}  
Enter fullscreen mode Exit fullscreen mode

A ideia é criar os testes necessários para garantir o correto funcionamento do código.

Para isso, criaremos 3 testes:

  1. Verificar se o implantador e o proprietário têm o mesmo endereço.
  2. Verificar que o proprietário é capaz de cunhar tokens.
  3. Transferências de tokens.

A Estrutura Principal de um Teste

Veja esta estrutura muito simples:

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

describe("Contract Name", function () {

  // Definição de usuários
  let deployer, user1, user2, user3;

  // Constantes

  before(async function () {


    // Implantação do usuário

    [deployer, user1, user2, user3] = await ethers.getSigners();

    // Implantação do contrato



  });

  it("TEST1 Name", async function () {



  });

  it("TEST2 Name", async function () {

  });

  it("TEST3 Name", async function () {

  });
});
Enter fullscreen mode Exit fullscreen mode

Este será sempre o nosso ponto de partida, a armação para todos os nossos testes. Não se preocupe em aprender JavaScript async/await ou as bibliotecas etherjs ou chai. Tudo começa aqui:

  1. Uma área para definir constantes: número de tokens, ETH a ser transferido,…
  2. Uma área para implantar o contrato.
  3. Uma área para criar os testes (test01, test02,…).

Podemos até lançar o teste para verificar a armação:

npx hardhat test test/tests.js

 Contract Name
   ✔ TEST1 Name
   ✔ TEST2 Name
   ✔ TEST3 Name

 3 passing (896ms)
Enter fullscreen mode Exit fullscreen mode

Até agora tudo bem.

Constantes e Implantação do Contrato

Números pequenos não precisam ser definidos como constantes, basta usá-los, mas, para números grandes e ethers, se precisar usá-los, crie uma constante como esta:

const USERS_MINT = ethers.utils.parseEther("5000");
const ETH_TRANSFER = ethers.utils.parseEther("6");
Enter fullscreen mode Exit fullscreen mode

Vamos passar para a implantação. O Hardhat implantará nosso contrato em uma rede local durante os testes. Mais uma vez, é sempre a mesma coisa, apenas duas linhas de código:

const ErcContractFactory = await ethers.getContractFactory("contracts/ErcContract.sol:ErcContract", deployer);
this.ercContract = await ErcContractFactory.deploy();

Enter fullscreen mode Exit fullscreen mode
  • ErcContractFactory é um nome aleatório, apenas certifique-se de que ambas as entradas correspondam.
  • ercContract também é um nome aleatório, basta adicionar “this.” antes.
  • O deployer é a conta que implanta o contrato. Deve ser um usuário definido: deployer, user1, user2 ou user3.
  • Caso seja necessário implantar o contrato com parâmetros:
const ErcContractFactory = await ethers.getContractFactory("contracts/ErcContract.sol:ErcContract", deployer);
this.ercContract = await ErcContractFactory.deploy(parameter1, parameter2);
Enter fullscreen mode Exit fullscreen mode

Nosso tests.js está assim agora:

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

describe("Contract Name", function () {

  // Definição de usuários
  let deployer, user1, user2, user3;

  // Constantes
  const USERS_MINT = ethers.utils.parseEther("5000");
  const ETH_TRANSFER = ethers.utils.parseEther("6");

  before(async function () {


    // Implantação de usuário

    [deployer, user1, user2, user3] = await ethers.getSigners();

    // Implantação de contrato
    const ErcContractFactory = await ethers.getContractFactory("contracts/ErcContract.sol:ErcContract", deployer);
    this.ercContract = await ErcContractFactory.deploy();

  });

  it("TEST01 Name", async function () {

  });

  it("TEST02 Name", async function () {

  });

  it("TEST03 Name", async function () {

  });
});
Enter fullscreen mode Exit fullscreen mode
npx hardhat test test/tests.js

  Contract Name
    ✔ TEST01 Name
    ✔ TEST02 Name
    ✔ TEST03 Name

  3 passing (456ms)
Enter fullscreen mode Exit fullscreen mode

Obviamente, ainda não estamos testando nada, mas conseguimos implantar o contrato com sucesso.

Testes

Novamente, sem nos preocuparmos com o motivo, usaremos a fórmula await/expect:

  • await: onde chamamos a função a ser testada.
  • expect: onde comparamos o estado do contrato após o await e o resultado esperado, geralmente um número.

Teste 1: Verificar o proprietário

it("TEST01: Check the ownership", async function() {
    const contractOwner = await this.ercContract.owner();
    expect(contractOwner).to.equal(deployer.address);
});
Enter fullscreen mode Exit fullscreen mode

Como você pode ver, é bastante intuitivo. Na parte await, chamamos a função, embora neste caso seja um pouco diferente, uma vez que estamos chamando a “função do proprietário” e não uma que nós mesmos codificamos.

A segunda parte é quando verificamos o resultado do await com o resultado esperado.

✗ npx hardhat test test/erc20-1/tests.js

  Contract Name
    ✔ TEST01: Check the ownership
    ✔ TEST2 Name
    ✔ TEST3 Name

  3 passing (460ms)
Enter fullscreen mode Exit fullscreen mode

Teste 2: Cunhar

it("TEST02: Mint 5000 tokens", async function() {
   await this.ercContract.mint(deployer.address, USERS_MINT);
   expect(await this.ercContract.balanceOf(deployer.address)).to.equal(USERS_MINT);
});
Enter fullscreen mode Exit fullscreen mode

Este é o melhor exemplo da fórmula await/expect.

  • await para chamar a função, neste caso para cunhar 5.000 tokens usamos função mint() do contrato OZ.
  • expect para comparar se o resultado de await é igual a 5.000 tokens. Usamos a função balanceOf() do contrato OZ.
px hardhat test test/erc20-1/tests.js

  Contract Name
    ✔ TEST01: Check the ownership
    ✔ TEST02: Mint 5000 tokens
    ✔ TEST3 Name

  3 passing (474ms)
Enter fullscreen mode Exit fullscreen mode

Teste 3: Transferências

Podemos usar quantos await precisarmos e verificar com expect quantas vezes forem necessárias também. Por exemplo: o deployer (implantador) de usuários permitirá que user1 gaste seus tokens. Depois, user1 enviará 100 tokens para user2.

it("TEST3 Transfer 100 Tokens", async function () {
    const TRANSFER_TOKEN = ethers.utils.parseEther("100");
    await this.ercContract.approve(user1.address, TRANSFER_TOKEN);
    await this.ercContract.connect(user1).transferFrom(deployer.address, user2.address, TRANSFER_TOKEN);
    expect(await this.ercContract.balanceOf(deployer.address)).to.equal(USERS_MINT.sub(TRANSFER_TOKEN));
    expect(await this.ercContract.balanceOf(user2.address)).to.equal(TRANSFER_TOKEN);
 });

Enter fullscreen mode Exit fullscreen mode
  • Declaramos uma nova constante TRANSFER_TOKEN.
  • O usuário padrão é deployer então precisamos nos conectar a user1 (usando connect(user1)) para executar a função como user1.
  • Para verificar o saldo do implantador, (USERS_MINT — TRANSFER_TOKEN), usamos o comando sub.
npx hardhat test test/erc20-1/tests.js

  Contract Name
    ✔ TEST01: Check the ownership
    ✔ TEST02: Mint 5000 tokens
    ✔ TEST03 Transfer 100 Tokens

  3 passing (505ms)
Enter fullscreen mode Exit fullscreen mode

O Script Final

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

describe("Contract Name", function () {
  // Definição de usuários
  let deployer, user1, user2, user3;

  // Constantes
  const USERS_MINT = ethers.utils.parseEther("5000");
  const ETH_TRANSFER = ethers.utils.parseEther("6");

  before(async function () {
    // Implantação de usuário

    [deployer, user1, user2, user3] = await ethers.getSigners();

    // Implantação de contrato
    const ErcContractFactory = await ethers.getContractFactory(
     "contracts/erc20-1/Erc201.sol:Erc201",
     deployer
    );
    this.ercContract = await ErcContractFactory.deploy();
  });

  it("TEST01: Check the ownership", async function () {
    const contractOwner = await this.ercContract.owner();
    expect(contractOwner).to.equal(deployer.address);
  });

  it("TEST02: Mint 5000 tokens", async function () {
    await this.ercContract.mint(deployer.address, USERS_MINT);
    expect(await this.ercContract.balanceOf(deployer.address)).to.equal(USERS_MINT);
  });

  it("TEST03 Transfer 100 Tokens", async function () {
    const TRANSFER_TOKEN = ethers.utils.parseEther("100");
    await this.ercContract.approve(user1.address, TRANSFER_TOKEN);
    await this.ercContract.connect(user1).transferFrom(deployer.address, user2.address, TRANSFER_TOKEN);
    expect(await this.ercContract.balanceOf(deployer.address)).to.equal(USERS_MINT.sub(TRANSFER_TOKEN));
    expect(await this.ercContract.balanceOf(user2.address)).to.equal(TRANSFER_TOKEN);
  });
});
Enter fullscreen mode Exit fullscreen mode

Existe uma razão JavaScript para todos os comandos, before, async, await,… mas não precisamos saber disso. Não somos desenvolvedores de JavaScript. Podemos executar nosso teste pensando nesses comandos como auxiliares.

Existem outros que precisamos saber, como greater (gt), add values (add)… isso é algo que você precisa descobrir durante sua jornada.

Notas do Curso de Hacking de Contratos Inteligentes

  • Acabei de terminar a terceira semana do curso e estimo que precisarei de cerca de 2 a 2,5 meses para terminar, trabalhando cerca de 4/5 horas por dia. Provavelmente haverá exercícios em que ficarei um pouco preso, então 2,5 meses será a meta mais realista.
  • Na verdade, o tema mais interessante desta primeira parte do curso é um exercício sobre o protocolo AAVE. Porém, preferi não escrever sobre isso, e sim fazer este, porque já existem outros artigos muito interessantes escritos por colegas. Verifique, por exemplo, este “Aave V3 Explained Simply with Diagrams” de @yongtaufoo123, é simplesmente maravilhoso, um artigo de leitura obrigatória.
  • Outro excelente recurso é o “USSD Smart Contract Auditing Contest” no canal do YouTube de @RealJohnnyTime. É uma auditoria ao vivo que fizemos no Discord onde ele explicou suas conclusões e descobertas em um concurso de Sherlock e tivemos a oportunidade de participar e compartilhar nossas opiniões e dúvidas.

Este artigo foi escrito por Aitor Zaldua e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Oldest comments (0)