WEB3DEV Español

Cover image for Testear en Hardhat sin saber Javascript
Hector
Hector

Posted on

Testear en Hardhat sin saber Javascript

Image description

¿Quieres ser un auditor de contratos inteligentes? Contáctame para que aprendas las formas, obtengas consejos y retos del mundo real enviando un correo a [email protected] o en X (Twitter) como @azdraft.

Sobrellevar la dificultad de dejar la web2 para la última frontera descubierta, la web3, es una tarea titánica. Pero esto es más pronunciado si vienes con un trasfondo de ciberseguridad en vez de haber sido un desarrollador full stack. Los prerrequisitos que las instituciones como Alchemy University, Moralis Academy o Encode Club, por nombrar a varios, indican repetidamente “tener conocimiento previo de JavaScript”.

Y este es mi caso. Después de aprender Solidity, trabajar en un número de proyectos y resolver múltiples CTF (Capture The Flag), hace unas semanas empecé el Curso de Hackeo de Contrato Inteligente, sin lugar a dudas es el mayor esfuerzo de entrenamiento para auditorías pero de nuevo, parece que hay una pared frente a mí. “Usaremos Hardhat porque es más fácil que foundry”.

Estuve en desacuerdo porque sé que la mayoría de ellos vienen, precisamente, de ser desarrolladores JavaScript en el pasado pero, para nosotros, significa aprender un lenguaje sólo para que los usemos para pruebas.

Pero, ahora, he cambiado de opinión. Después de esas semanas puedo decir esto alto y claro: no necesitas aprender JavaScript para ser un desarrollador de contrato inteligente o un auditor. Esos días terminaron. Primero porque, obviamente, puedes usar Foundry. En segundo lugar porque sólo necesitas un scaffolding y un par de instrucciones para crear tus pruebas con Hardhat.

El Código a Probar

Vamos a crear una prueba para un contrato ERC20 muy sencillo, hecho con Open Zeppelin

// 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

La idea es crear las pruebas necesarias para asegurar el correcto funcionamiento del código.

Para esto, crearemos 3 pruebas:

  1. Revisa que el desplegador y el dueño tengan la misma dirección.
  2. Revisa que el dueño sea capaz de acuñar tokens.
  3. Transferencias de token.

La estructura principal de una prueba

Mira esta estructura simple:

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

describe("Contract Name", function () {

 // Definición de los Usuarios
 let deployer, user1, user2, user3;

 // Constantes

 before(async function () {

   // Despliegue del Usuario

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

   // Despliegue del contrato


 });

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


 });

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

 });

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

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

Este siempre será nuestro punto de partida, el scaffolding de todas nuestras pruebas. No te preocupes sobre aprender la asincronía/espera de JavaScript o las bibliotecas chai or etherjs. Sólo empieza aquí:

  1. Una área para definir constantes: número de tokens, ETH a transferir…
  2. Una área para desplegar el contrato
  3. Una área para crear las pruebas (test01, test02,...)

Incluso podemos lanzar la prueba para verificar el scaffolding:

npx hardhat test test/tests.js


 Contract Name
   ✔ TEST1 Name
   ✔ TEST2 Name
   ✔ TEST3 Name


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

Por ahora, todo bien;

Constantes y Despliegue del Contrato

Los número pequeños no necesitan ser definidos como constantes, sólo úsalos, pero para números largos y ethers, si necesitas usarlos, crea una constante como:

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

Vamos al despliegue. Hardhat desplegará nuestro contrato en una red local durante la prueba. De nuevo, siempre es el mismo, sólo dos líneas de códigos:

const ErcContractFactory = await ethers.getContractFactory("contracts/ErcContract.sol:ErcContract", deployer);
this.ercContract = await ErcContractFactory.deploy();
Enter fullscreen mode Exit fullscreen mode
  • ErcContractFactory es un nombre aleatorio, sólo asegúrate que ambas entradas coincidan.
  • ercContract también es un nombre aleatorio, sólo añade ”this” antes.
  • El deployer es la cuenta que despliega el contrato. Tiene que ser un usuario definido: deployer, user1, user2 o user3.
  • En caso que necesites desplegar el contrato con 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

Nuestra tests.js ahora se verá así:

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

describe("Contract Name", function () {

 // Definición del usuario
 let deployer, user1, user2, user3;

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

 before(async function () {

   // Despliegue del usuario

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

   // Despliegue del 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, aún no estamos probando nada pero, hemos sido capaces de desplegar el contrato exitosamente.

Pruebas

De nuevo, sin preocuparte del por qué, usaremos la fórmula await/expect:

  • await: donde llamamos la función a probar.
  • expect: donde comparamos el estado del contrato luego de la espera y del resultado esperado, usualmente un número.

Prueba 1: revisa el dueño

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 puedes ver, es muy intuitivo. En la espera await, llamamos a la función, aunque en este caso es un poco diferente ya que estamos llamando a la “función del dueño”, no la que hemos codeado.

La segunda parte es donde revisamos el resultado await con el 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

Prueba 2: acuñar

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 es el mejor ejemplo de la fórmula await/expect.

  • await para llamar la función, en este caso para acuñar 5000 tokens usamos la función mint() del contrato OZ.
  • expect para comparar si el resultado de la espera es igual a 5000 tokens. Usamos la función balanceOf() desde el 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

Prueba 3: Transferencia

Podemos usar tantos await como necesitamos y revisarlos con expect cuántas veces sea necesario. Por ejemplo: el usuario deployer permitirá al user1 gastar sus tokens. Luego user1 enviará 100 tokens al 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 una nueva constante TRANSFER_TOKEN
  • El usuario por defecto es deployer, así que tenemos que conectarlos con user1 (usando connect(user1)) para ejecutar la función como user1.
  • Para revisar el balance de despliegue, (USERS_MINT - TRANSFER_TOKEN), usaremos el 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

El script final

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

describe("Contract Name", function () {
 // La definición usuarios
 let deployer, user1, user2, user3;

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

 before(async function () {
   // Despliegue del usuario

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

   // Despliegue del 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

Hay una razón JavaScript para todos los comandos, antes, asincronía, espera… pero no necesitamos saber eso. No somos desarrolladores JavaScript. Podemos ejecutar nuestra prueba pensando sobre estos comandos como ayudantes.

Hay otros que necesitamos saber como mayor (greater, gt), añadir valores (add)... esto es algo que tienes que encontrar durante tu viaje.

Notas desde el Curso de Hackeo del Contrato Inteligente

  • Recientemente terminé la tercera semana del curso y estimé que necesitaré 2 a 2.5 meses para terminar, trabajando cerca de 4/5 horas diarias. Probablemente habrán ejercicios donde me quedo atascado, así que 2.5 meses será la meta más realista.
  • De hecho, el tema más interesante en esta primera parte del curso es un ejercicio en el protocolo AAVE. Sin embargo, preferí no escribir sobre eso y hacer esto en cambio, porque ya hay otros artículos interesantes escritos por colegas. Revisa, por ejemplo, este Aave V3 Explained Simply with Diagrams por @yongtaufoo123, es totalmente genial, un artículo que debes leer.
  • Otro gran recurso es USSD Smart Contract Auditing Contest del canal de youtube de @RealJohnnyTime. Es una auditoría en vivo que hicimos en discord, donde él explicó sus conclusiones y las cosas que encontró en un concurso sherlock y tuvimos la oportunidad de participar e intercambiar nuestras opiniones y preguntas.

Este artículo es una traducción de Aitor Zaldua, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.

Discussion (0)