Desarrollo de un Token ERC20 con Hardhat y OpenZeppellin para el Backend y EthersJS para el “frontend”

Dentro de las posibilidades que ofrece la Blockchain, los Tokens ERC20 es una de las más generalizadas a la hora de desplegar una DApp, bien sea como Token para un DAO o bien como base para un nuevo proyecto Blockchain. En esta entrada daremos los detalles del desarrollo de un token ERC20 usando Hardhat como base y OpenZeppellin como Framework de desarrollo y EthersJS para los despliegues y las consultas y llamadas. ¿Te lo vas a perder?

Antes de empezar con el artículo te recuerdo que ya tenemos una guía sobre la instalación de Hardhat, por si no sabes cómo montar el entorno de desarrollo. Si ya lo tienes instalado ya puedes continuar con este artículo sin problemas.

OpenZeppellin

Si no lo conoces OpenZeppellin es uno de los principales frameworks de desarrollo sobre la EVM de Ehtherium, y que implementan muchos otras blockchain:

Entre las diferentes bibliotecas que incluye OpenZeppelin destacan las siguientes:

  • Contracts: Biblioteca de contratos inteligentes
  • Upgrades: Biblioteca para la creación de contratos inteligentes actualizables
  • Defender: biblioteca de SecOps para la EVM

En este artículo nos centraremos en el uso de Contracts para contratos inteligentes.

OpenZeppellin Contracts

Dentro de esta biblioteca Solidity disponemos de ayudas para la implementación de los estándares ERC20 o Tokens fungibles y ERC721 o NFT’s (Non Fungible Tokens ó Tokens no Fungibles).

La diferencia principal entre ellos es que los ERC20 son token que son repetidos, o indistinguibles entre unos y otros, mientras que los ERC721 o NFT’s son únicos e distinguibles unos de otros. En posteriores artículos nos centraremos en los NFT’s pero en este será enteramente dedicado a los Tokens estándar ERC20 o tokens fungibles.

ERC20

Si nos basamos en los contratos de OpenZeppellin tenemos algunas ventajas ya que disponemos de implementaciones específicas para los diferentes tipo de Tokens que suele haber por las diferentes Blockchains:

  • ERC20: una implementación del estándar ERC20
  • IERC20: interfaz de ERC20
  • IERC20Metadata: Interfaz de los datos de un ERC20
  • Extensiones:
    • Burnable: para tokens que permitan realizar la quema de tokens
    • Capped: para limitar el supply de tokens
    • Pausable: para poder pausar el minado, quema o transferencia de tokens
    • Snapshot: para guardar el totalsupply y los balances del token un determinados momentos
    • Votes: para el sistema de votación directa o delegada similar a la de Compound pero con un soporte de un supply hasta 2^224
    • VotesComp: igual al sistema de votación de compound con un soporte de 2^96
    • Wrapper: soporte de tokens envueltos (wrapped en inglés)
    • FlashMint: implementación del estándar ERC3156 de préstamos rápidos

El Estándar ERC20 tiene una serie de funcionalidades, pero será mejor explicarlas cuando demos uso a ellas desde las pruebas del Token.

Creación del Token

Para crear los Tokens ERC20 deberemos en la carpeta del proyecto incluir la dependencia de OpenZeppelin:

$ npm install @openzeppelin/contracts

Con la dependencia ya instalada ya podremos crear el Token.sol dentro de la carpeta contracts:

// contracts/Token.sol
// SPDX-License-Identifier: Business Source License 1.1
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
        _mint(msg.sender, initialSupply);
    }
}

Como podemos ver tenemos lo siguiente:

  • Nombre y ruta del fichero
  • Licencia a aplicar. En este caso estamos usando la Business Source License 1.1 similar a la MariaDB
  • Versión a usar de Solidity, en este caso la 0.8.0 o superior.
  • La importación del contrato ERC20 de OpenZeppellin
  • La definición del contrato llamado Token que hereda de ERC20 de OpenZeppellin
  • La definición del constructor que recibe como parámetro el supply inicial del token o número de monedas iniciales en el momento del lanzamiento del token así como la llamada al constructor del contrato ERC20 heredado para darle el nombre al Token y su símbolo, “MyToken” y “MTK” respectivamente.

Como siempre podemos compilar el token usando hardhat:

$ npx hardhat compile

Si todo ha ido bien y no falla al compilar ya podemos probar a desplegarlo.

Despliegue en pruebas

De cara a lanzar el token en pruebas debemos realizar un script de despliegue, esto en Hardhat se hace metiendo un script en el directorio scripts, en este caso con el nombre deploy-mtk.ts que escribiremos en Typescript.

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// When running the script with npx hardhat run  you'll find the Hardhat
// Runtime Environment's members available in the global scope.
import { ethers } from "hardhat";

async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run('compile');
  // Supply Inicial
  let initialSupply = '10000000000000000000000'; // 10000 * 1e18
  // We get the contract to deploy
  const Token = await ethers.getContractFactory("Token");
  const token = await Token.deploy(initialSupply);

  await token.deployed();

  console.log("Token deployed to:", token.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Como podemos ver tenemos una función main asíncrona con las siguientes funcionalidades:

  • Definición del supply inicial del token: como se puede ver tenemos que multiplicar el número de tokens por su número de decimales por 10 par así poder tener el número completo en wei. Los wei serían como los satoshis en bitcoin, es decir la unidad mínima indivisible del token que estemos creando. Por ejemplo los Euros o dólares normalmente sólo tienen dos decimales, por lo que si quisiéramos tener un valor inicial de 1000 euros virtuales sería 1000*100 (10e2).
  • Obtención del contrato, usando una cadena con el nombre del contrato (.sol)
  • Despliegue llamando al constructor: llamando al método deploy que llama al constructor del Token con el monto inicial de Tokens.
  • Indicación de la dirección del contrato que acabamos de desplegar por el log
  • Llamada al método main teniendo en cuenta los posibles errores al desplegar.

Con todo esto definido podemos hacer un despliegue de prueba:

$ npx hardhat run scripts/deploy-mtk.ts

El cual nos debería de dar una salida con la dirección del contrato del token, por ejemplo:

No need to generate any newer typings.
Token deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3

En posteriores artículos iremos publicando manera de desplegar estos contratos en redes de pruebas de diferentes blockchains así como en las redes principales.

Pruebas del Token

De cara a realizar la pruebas tendremos que crear un fichero en el directorio test del proyecto por ejemplo en un fichero llamado token.ts:

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

describe("MyToken", function () {
  let initialSupply = '10000000000000000000000'; // 10000 * 1e18
  let Token;
  let hardhatToken;
  let owner;
  let addr1;
  let addr2;
  let addrs;

  beforeEach(async function () {
    // Get the ContractFactory and Signers here.
    Token = await ethers.getContractFactory("Token");
    [this.owner, this.addr1, this.addr2, ...this.addrs] = await ethers.getSigners();
    this.hardhatToken = await Token.deploy(initialSupply);
    await this.hardhatToken.deployed();
  });
  it("Should return the correct Total Supply", async function () {
    const TokenAddress = this.hardhatToken.address;
    console.log("Token Address: " + TokenAddress);
    expect(initialSupply).to.equal(await this.hardhatToken.totalSupply());
  });

});

Como podemos ver tenemos las dos principales importaciones de expect desde chai y de ethers desde hardhat y la definición de las pruebas:

  • describe: define el conjunto de pruebas con un nombre, este este caso del token a probar
    • Definición de variables:
      • inicialSupply
      • Token: Contrato
      • hardhatToken: contrato a desplegar o desplegado
      • Variables para las direcciones para:
        • el propietario del token
        • primera dirección de la blockchain de pruebas
        • segunda dirección de la blockchain de pruebas
        • resto de direcciones de la blockchain de pruebas
    • beforeEach:
      • Obtención del token
      • Obtención de las direcciones o signers
      • Despliegue del token con el supply inicial
      • Esperar hasta que se se haya desplegado
    • it:
      • Coger la dirección del owner del token
      • Imprimirla por pantalla
      • Comprobar que el supply inicial es el mismo que el esperado

Comprobación de Balances

Otra de las cosas típicas cuando desplegamos un Token es la de comprobar que todos los saldos son correctos para las diferentes direcciones donde realizamos los diferentes repartos. Por ejemplo en el Token.sol definíamos que el propietario del token recibía unos el monto inicial. Para ello podemos mete runa nueva prueba en el fichero Typescript de pruebas:

describe("MyToken", function () {
  // resto de código...
  it("The deployer account must have the initial supply", async function () {
    await this.hardhatToken.deployed();
    expect(initialSupply).to.equal(await this.hardhatToken.balanceOf(this.owner.address));
  });
});

Como vemos ver llamamos al método balanceOf pasando como parámetro la dirección del propietario y comprobamos si coincide con el monto inicial.

Transferencia de Tokens desde la cuenta administradora

De cada a poder realizar una transferencia de fondos con los permisos de la cuenta administadora del token debemos hacer uso del método transfer que recibe dos parámetros:

  • La cuenta de destino de los tokens
  • El monto a transferir

Veamos el código de ejemplo en las pruebas:

import { BigNumber } from "ethers";
describe("MyToken", function () {
  // resto de código...
  it("Should checks ammounts after a transfer", async function () {
    const ownerBalanceBeforeTransfer = await this.hardhatToken.balanceOf(this.owner.address);
    // Transfer 50 tokens from owner to addr1
    await this.hardhatToken.transfer(this.addr1.address, 50);
    const addr1BalanceBeforeTransfer = await this.hardhatToken.balanceOf(this.addr1.address);
    expect(50).to.equal(addr1BalanceBeforeTransfer);

    const ownerBalanceAfterTransfer = await this.hardhatToken.balanceOf(this.owner.address);
    // se usa Bignumber porque TS no soporta por defecto cifras tan grandes
    expect(BigNumber.from("9999999999999999999950")).to.equal(ownerBalanceAfterTransfer);
  });
});

Como podemos ver vamos a necesitar el uso de otra biblioteca por parte de ethers para disponer de un tipo de dato llamado BigNumber que nos permitirá manejar números tan grandes a los que se usan en las cantidades de tokens debido al uso de los decimales como un valor global en vez de números float tipicos con decimales.

Por lo que deberemos importar Bignumber desde ethers al principio del fichero.

Después dentro de describe necesitaremos describir la prueba:

  • Obtener el balance de la cuenta administradora del Token
  • Transferencia de la cuenta principal a una secundaria (addr1) con 50 wei
  • Obtener el balance de la cuenta destino
  • Comprobación del balance de la cuenta destino
  • Obtención del balance de la cuenta administradora
  • Comprobación del saldo de la cuenta administradora

Como puede verse para comprobar el balance de la administradora lo comparamos usando BigNumber.from() metiéndole el valor como cadena e igualándola al balance de la misma.

Si todo ha ido bien habríamos movido 50 wei de una cuenta a otra.

Transferencia de fondos de una cuenta que tenemos controlada a otra

En el siguiente ejemplo deberemos gestionar una transferencia de fondos de una cuenta a otra, pero como queremos aislar unas pruebas de otras deberemos seguir metiendo en la prueba el despliegue del contrato y el traspaso de fondos de la cuenta administradora (owner) a otra cuenta (addr1) antes de intentar hacer la transferencia:

  it("Should transfer tokens between accounts", async function () {
    const ownerBalanceBeforeTransfer = await this.hardhatToken.balanceOf(this.owner.address);
    // Transfer 50 tokens from owner to addr1
    await this.hardhatToken.transfer(this.addr1.address, 50);
    const addr1BalanceBeforeTransfer = await this.hardhatToken.balanceOf(this.addr1.address);
    expect(50).to.equal(addr1BalanceBeforeTransfer);
    // Transfer 50 tokens from addr1 to addr2
    // We use .connect(signer) to send a transaction from another account
    await this.hardhatToken.connect(this.addr1).transfer(this.addr2.address, 50);
    const addr2BalanceAfterTransfer = await this.hardhatToken.balanceOf(this.addr2.address);
    const addr1BalanceAfterTransfer = await this.hardhatToken.balanceOf(this.addr1.address);
    expect(50).to.equal(addr2BalanceAfterTransfer);
    expect(0).to.equal(addr1BalanceAfterTransfer);
  });

Como vemos tenemos un código similar al anterior pero después de haber transferido los fondos a addr1 los intentamos mover a otra cuenta.

Con el método connect indicamos al token con que cuenta queremos realizar la operación, para que sea diferente a la administradora. Para después llamar al método transfer, indicando la dirección de destino y el monto a transferir.

Después comprobaremos si los balances de ambas cuentas coinciden con lo esperado.

Autorización y Transferencia de fondos entre cuentas sin permisos iniciales

Muchas veces en en los contratos de token debemos poder mover fondos desde una cuenta de un contrato que no es la administradora del token a otra cuenta en nombre de esta primera cuenta.

Es decir, debemos autorizar a una cuenta poder gestionar los tokens de otra cuenta.

Para ello existe el método approve que permite hacer uso de un determinado token de una cuenta por parte de otra.

En este ejemplo intentaremos mover tokens de la cuenta addr1 a la cuenta addr2 desde la cuenta owner. es decir: addr1—>owner–>addr2.

Veamos cómo se realizan estos pasos en la prueba:

  it("Should transfer tokens from one account to another with approve", async function () {
    const ownerBalanceBeforeTransfer = await this.hardhatToken.balanceOf(this.owner.address);
    // Transfer 50 tokens from owner to addr1
    await this.hardhatToken.transfer(this.addr1.address, 50);
    const addr1BalanceBeforeTransfer = await this.hardhatToken.balanceOf(this.addr1.address);
    // permitiendo gestionar desde la cuenta owner la cuenta 1
    await this.hardhatToken.connect(this.addr1).approve(this.owner.address, 50);
    const tokensallowed = await this.hardhatToken.allowance(this.addr1.address, this.owner.address);
    // console.log("Tokens Allowed: " + tokensallowed);
    expect(50).to.equal(tokensallowed);
    // Transfer 50 tokens from addr1 to addr2 with the Owner Account Permissions
    // We use .connect(signer) to send a transaction from another account
    await this.hardhatToken.transferFrom(this.addr1.address, this.addr2.address, 50);
    const addr2BalanceAfterTransfer = await this.hardhatToken.balanceOf(this.addr2.address);
    const addr1BalanceAfterTransfer = await this.hardhatToken.balanceOf(this.addr1.address);
    expect(50).to.equal(addr2BalanceAfterTransfer);
    expect(0).to.equal(addr1BalanceAfterTransfer);
  });

Como vemos realizamos los siguientes pasos:

  • Transferencia inicial de fondos (50 wei) a la cuenta addr1
  • Comprobación de la transferencia
  • Aprobación para el token de la cuenta owner para manejar los fondos (50 wei) de la cuenta addr1
  • Comprobación mediante el método allowance de que la cuenta addr1 permite a la cuenta owner manejar los 50 wei indicados
  • Ahora ya sí usar el método transferFrom indicando la dirección de origen (addr1) , destino (addr2) y monto (50 wei). Fíjate que en esta llamada no hay connect por lo que lo estamos haciendo con la cuenta owner.
  • Comprobaciones de saldos de las cuentas addr1 y addr2.

Conclusiones

Con esto le hemos pegado un buen repaso al estándar ERC20 de Etherium y cómo desarrollar el token con Hardhat en Solidity usando OpenZeppellin de base y realizando las pruebas en Typescript con la ayuda de EhersJS.

En el siguientes artículos hablaremos de despliegue sobre la blockchain de pruebas (testnet) , sobre otras extensiones interesantes de OpenZeppelin y sobre un contrato de una ICO.

Comments

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información. ACEPTAR

Aviso de cookies