Tokenización de Activos Fungibles en Web3: Creando un Token ERC-20 con OpenZeppelin y Hardhat 💰
Este tutorial te guiará paso a paso en la creación de un token fungible (ERC-20) en la blockchain. Utilizaremos las robustas librerías de OpenZeppelin para la implementación del contrato inteligente y Hardhat como nuestro entorno de desarrollo para probar y desplegar el token.
La tokenización de activos fungibles es uno de los pilares de la economía descentralizada y una de las aplicaciones más comunes de la tecnología blockchain. Un token fungible es, por definición, intercambiable por otro de su mismo tipo, como el dinero fiduciario o las acciones de una empresa. En el mundo de Web3, estos tokens se materializan a menudo a través del estándar ERC-20 en la red Ethereum, o estándares compatibles en otras cadenas de bloques.
Este tutorial te proporcionará los conocimientos y las herramientas necesarias para diseñar, implementar, probar y desplegar tu propio token ERC-20. Al final, tendrás una comprensión clara de cómo funcionan estos tokens y cómo interactuar con ellos.
¿Qué es un Token ERC-20? 📖
El estándar ERC-20 (Ethereum Request for Comments 20) es una especificación técnica utilizada en la blockchain de Ethereum para la implementación de tokens fungibles. Fue propuesto en 2015 por Fabian Vogelsteller y se ha convertido en el estándar de facto para la creación de la gran mayoría de tokens en Ethereum y redes compatibles (BNB Chain, Polygon, Avalanche, etc.).
Un token ERC-20 define un conjunto de reglas básicas que un contrato inteligente debe seguir, permitiendo que los tokens sean interoperables con cualquier aplicación o cartera que soporte el estándar. Esto incluye:
- Total Supply: La cantidad total de tokens en existencia.
- Balance Of: Consulta el balance de tokens de una dirección específica.
- Transfer: Mueve tokens de una dirección a otra.
- Approve: Permite a una dirección gastar tokens en nombre de otra.
- Allowance: Consulta cuántos tokens se ha permitido gastar a una dirección.
Estos métodos, junto con dos eventos (Transfer y Approval), son la base de cómo los tokens ERC-20 funcionan y se mueven en la blockchain.
Herramientas Necesarias 🛠️
Antes de sumergirnos en el código, necesitamos configurar nuestro entorno de desarrollo. Aquí están las herramientas que utilizaremos:
- Node.js y npm (o Yarn): Para gestionar nuestros paquetes de JavaScript.
- Hardhat: Un entorno de desarrollo flexible y extensible para Ethereum.
- Solidity: El lenguaje de programación para escribir contratos inteligentes.
- OpenZeppelin Contracts: Una biblioteca de contratos inteligentes seguros y probados para implementar los estándares ERC.
- MetaMask: Una cartera de Ethereum para interactuar con la red.
Instalación de Node.js y npm
Si aún no tienes Node.js y npm instalados, descárgalos desde el sitio web oficial: nodejs.org. Verifícalos con:
node -v
npm -v
Configuración del Proyecto Hardhat
- Crea un nuevo directorio para tu proyecto:
mkdir MiTokenERC20
cd MiTokenERC20
- Inicializa un nuevo proyecto npm:
npm init -y
- Instala Hardhat:
npm install --save-dev hardhat
- Inicializa un proyecto Hardhat:
npx hardhat
Selecciona `Create a JavaScript project` cuando te lo pregunten. Esto creará la estructura básica de carpetas (`contracts`, `scripts`, `test`) y archivos (`hardhat.config.js`).
5. Instala OpenZeppelin Contracts:
npm install @openzeppelin/contracts
Esta librería es fundamental para implementar estándares ERC de forma segura.
6. Instala las dependencias adicionales para testing y despliegue (opcional pero recomendado):
npm install --save-dev @nomicfoundation/hardhat-toolbox dotenv
`hardhat-toolbox` incluye herramientas para testing, verificación y un `ether.js` más moderno. `dotenv` nos servirá para gestionar variables de entorno (claves privadas, IDs de API).
Diseñando Nuestro Token 🎯
Para este tutorial, crearemos un token llamado 'MiToken' con el símbolo 'MTK' y una oferta inicial de 1,000,000 de tokens. También haremos que sea mintable (se pueden crear más tokens después del despliegue) y burnable (se pueden destruir tokens). Estas funcionalidades son comunes y se implementan fácilmente con OpenZeppelin.
Estructura del Contrato
Nuestro contrato MiToken.sol será relativamente sencillo gracias a OpenZeppelin:
- Importará
ERC20.solpara la funcionalidad básica ERC-20. - Importará
ERC20Capped.solsi queremos un suministro máximo. - Importará
Ownable.solpara tener un propietario que pueda realizar acciones privilegiadas (como acuñar o quemar). - Importará
ERC20Mintable.solyERC20Burnable.solpara las funciones de acuñado y quemado.
Implementación del Contrato Inteligente (Solidity) ✍️
Crea un nuevo archivo MiToken.sol dentro de la carpeta contracts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Mintable.sol"; // Para versiones más antiguas, pero para ^0.8.20 se suele integrar la lógica de acuñamiento manualmente o usar roles.
// En versiones recientes de OpenZeppelin (>=4.x), la funcionalidad de acuñado no viene con un contrato 'ERC20Mintable' separado,
// sino que se espera que el desarrollador añada la lógica de acuñado y los permisos (por ejemplo, con Ownable o AccessControl).
// Para este tutorial, implementaremos una función 'mint' simple restringida al propietario usando 'Ownable'.
contract MiToken is ERC20, Ownable, ERC20Burnable {
// Constructor: Se ejecuta una vez al desplegar el contrato.
// Inicializa el nombre y el símbolo del token.
// El propietario es quien despliega el contrato.
constructor()
ERC20("MiToken", "MTK")
Ownable(msg.sender)
{
// Acuñamos el suministro inicial al propietario que despliega el contrato.
// Los tokens ERC-20 tienen 18 decimales por defecto. Para 1,000,000 tokens, necesitamos 1,000,000 * (10 ** 18).
_mint(msg.sender, 1_000_000 * (10 ** 18));
}
/**
* @dev Función para acuñar nuevos tokens.
* Solo el propietario del contrato puede llamar a esta función.
* @param to Dirección a la que se acuñarán los tokens.
* @param amount Cantidad de tokens a acuñar (expresada en unidades base, por ejemplo, 1 token = 1e18).
*/
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
// La función `burn` ya está disponible a través de la herencia de `ERC20Burnable`.
// Simplemente llama a `super._burn(account, amount)` o `_burn(account, amount)`.
}
Explicación del Código
// SPDX-License-Identifier: MITypragma solidity ^0.8.20;: Especifica la licencia y la versión del compilador Solidity.import "@openzeppelin/contracts/...": Importamos los contratos base de OpenZeppelin.ERC20.sol: Contiene la implementación básica del estándar ERC-20.Ownable.sol: Un patrón de control de acceso que asigna unowneral contrato y permite restringir funciones solo a eseowner.ERC20Burnable.sol: Añade la funcionalidad para que los poseedores de tokens puedan quemar sus propios tokens (burn).
contract MiToken is ERC20, Ownable, ERC20Burnable: Nuestro contratoMiTokenhereda de estos tres contratos, obteniendo sus funcionalidades.constructor() ERC20("MiToken", "MTK") Ownable(msg.sender):- El constructor se ejecuta solo una vez cuando el contrato se despliega.
- Llama a los constructores de
ERC20para establecer el nombre (MiToken) y el símbolo (MTK). - Llama al constructor de
Ownablepara establecer elmsg.sender(la dirección que despliega el contrato) como el propietario inicial. _mint(msg.sender, 1_000_000 * (10 ** 18));: Acuñamos 1,000,000 de tokens al propietario. Los tokens ERC-20 generalmente tienen 18 decimales, por lo que multiplicamos por10^18para obtener la cantidad correcta en unidades base.
function mint(address to, uint256 amount) public onlyOwner: Esta función personalizada permite al propietario acuñar nuevos tokens y enviarlos a una dirección específica. El modificadoronlyOwnergarantiza que solo el propietario pueda llamar a esta función.- Importante: Para versiones de OpenZeppelin más recientes (4.x y posteriores), no existe un contrato
ERC20Mintable.solindependiente de la misma forma queERC20Burnable.sol. La estrategia común es añadir una funciónmintpersonalizada y protegerla conOwnableoAccessControl, como hemos hecho aquí.
Configuración de Hardhat para Despliegue ⚙️
Necesitaremos configurar hardhat.config.js para poder desplegar nuestro contrato en una red de prueba (como Sepolia o Polygon Mumbai). Para esto, usaremos dotenv para cargar variables de entorno de un archivo .env.
- Crea un archivo
.enven la raíz de tu proyecto:
ALCHEMY_API_KEY=tu_clave_api_de_alchemy_o_infura
PRIVATE_KEY=tu_clave_privada_de_metamask
<div class="callout warning">⚠️ <strong>Advertencia:</strong> NUNCA publiques tu clave privada en un repositorio público. Usa `.gitignore` para excluir `.env`. Tu clave privada es como la clave de tu bóveda bancaria.</div>
2. Añade la siguiente configuración a hardhat.config.js:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY;
const SEPOLIA_PRIVATE_KEY = process.env.PRIVATE_KEY;
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
accounts: [SEPOLIA_PRIVATE_KEY],
},
// Puedes añadir otras redes como Polygon Mumbai aquí si lo deseas
// mumbai: {
// url: `https://polygon-mumbai.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
// accounts: [SEPOLIA_PRIVATE_KEY],
// },
},
};
* **ALCHEMY_API_KEY:** Obtén una clave API gratuita de [Alchemy](https://www.alchemy.com/) o [Infura](https://infura.io/).
* **PRIVATE_KEY:** Es la clave privada de tu cuenta de MetaMask (la que usarás para desplegar y pagar las tarifas de gas). Para obtenerla, abre MetaMask, haz clic en los tres puntos de tu cuenta, selecciona "Detalles de la cuenta" y luego "Exportar clave privada".
Creación del Script de Despliegue 🚀
Crearemos un script en la carpeta scripts para desplegar nuestro contrato MiToken. Crea un archivo deploy.js dentro de la carpeta scripts.
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Desplegando contratos con la cuenta:", deployer.address);
const initialBalance = await deployer.getBalance();
console.log("Balance de la cuenta antes del despliegue:", hre.ethers.utils.formatEther(initialBalance), "ETH");
const MiToken = await hre.ethers.getContractFactory("MiToken");
const miToken = await MiToken.deploy();
await miToken.deployed();
console.log("MiToken desplegado en la dirección:", miToken.address);
const finalBalance = await deployer.getBalance();
console.log("Balance de la cuenta después del despliegue:", hre.ethers.utils.formatEther(finalBalance), "ETH");
console.log("Costo total del despliegue:", hre.ethers.utils.formatEther(initialBalance.sub(finalBalance)), "ETH");
// Opcional: Verificar el suministro total y el balance del propietario
const totalSupply = await miToken.totalSupply();
console.log("Suministro total de MiToken:", hre.ethers.utils.formatUnits(totalSupply, 18), "MTK");
const ownerBalance = await miToken.balanceOf(deployer.address);
console.log("Balance del propietario (deployer) de MiToken:", hre.ethers.utils.formatUnits(ownerBalance, 18), "MTK");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Compilación y Despliegue del Contrato ✨
¡Es hora de compilar y desplegar nuestro token!
1. Compilar el Contrato
Desde la raíz de tu proyecto, ejecuta:
npx hardhat compile
Si todo está correcto, verás un mensaje de éxito. Esto creará los artefactos del contrato (.json) en la carpeta artifacts.
2. Desplegar en la Red de Prueba (Sepolia)
Para desplegar en Sepolia, usa el script que creamos:
npx hardhat run scripts/deploy.js --network sepolia
Si el despliegue es exitoso, verás la dirección del contrato de MiToken en la consola. ¡Felicidades, has desplegado tu propio token ERC-20!
Interactuando con el Token Desplegado 🌐
Ahora que tu token está en la red de prueba, puedes interactuar con él de varias maneras.
Usando la Consola de Hardhat
Hardhat tiene una consola integrada que te permite interactuar con tus contratos desplegados.
- Inicia la consola de Hardhat con tu red:
npx hardhat console --network sepolia
- Dentro de la consola, obtén una instancia de tu contrato:
const MiToken = await ethers.getContractFactory("MiToken");
const miToken = await MiToken.attach("DIRECCIÓN_DE_TU_TOKEN_DESPLEGADO");
const [deployer, addr1, addr2] = await ethers.getSigners();
// Consulta el nombre y símbolo
console.log("Nombre:", await miToken.name());
console.log("Símbolo:", await miToken.symbol());
// Consulta el balance del propietario
const ownerBalance = await miToken.balanceOf(deployer.address);
console.log("Balance del propietario:", ethers.utils.formatUnits(ownerBalance, 18));
// Transfiere tokens a otra dirección
await miToken.transfer(addr1.address, ethers.utils.parseUnits("100", 18));
console.log("Balance de addr1 después de la transferencia:", ethers.utils.formatUnits(await miToken.balanceOf(addr1.address), 18));
// Quema tokens (solo si la cuenta tiene balance suficiente)
await miToken.connect(deployer).burn(ethers.utils.parseUnits("50", 18));
console.log("Balance del propietario después de quemar:", ethers.utils.formatUnits(await miToken.balanceOf(deployer.address), 18));
// Acuña nuevos tokens (solo el propietario puede hacerlo)
await miToken.connect(deployer).mint(addr2.address, ethers.utils.parseUnits("200", 18));
console.log("Balance de addr2 después de acuñar:", ethers.utils.formatUnits(await miToken.balanceOf(addr2.address), 18));
// Salir de la consola
.exit
Verificación en Etherscan
Es una buena práctica verificar tu contrato en Etherscan (o el explorador de bloques equivalente para tu red). Esto permite a otros ver el código fuente verificado del contrato y asegura la transparencia.
- Asegúrate de tener instalado el plugin de verificación de Hardhat:
npm install --save-dev @nomicfoundation/hardhat-verify
- Configura tu clave API de Etherscan en
.envyhardhat.config.js:
// .env
ETHERSCAN_API_KEY=tu_clave_api_de_etherscan
// hardhat.config.js
// ... otras configuraciones ...
require("@nomicfoundation/hardhat-verify");
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY;
module.exports = {
// ...
etherscan: {
apiKey: ETHERSCAN_API_KEY,
},
};
- Verifica tu contrato después del despliegue:
npx hardhat verify --network sepolia DIRECCION_DE_TU_TOKEN_DESPLEGADO
Una vez verificado, puedes ir a Sepolia Etherscan (sepolia.etherscan.io) y buscar la dirección de tu contrato para ver su código fuente, transacciones y más.
Pruebas de Contratos Inteligentes (Opcional pero Recomendado) ✅
La escritura de pruebas es crucial para asegurar la funcionalidad y seguridad de tu contrato. Hardhat facilita esto con su integración de Mocha y Chai. La plantilla de Hardhat ya crea una carpeta test.
Crea un archivo MiToken.test.js dentro de la carpeta test.
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MiToken", function () {
let MiToken;
let miToken;
let deployer;
let addr1;
let addr2;
let addrs;
const initialSupply = ethers.utils.parseUnits("1000000", 18); // 1,000,000 tokens
beforeEach(async function () {
// Obtiene las cuentas de prueba proporcionadas por Hardhat
[deployer, addr1, addr2, ...addrs] = await ethers.getSigners();
// Despliega el contrato antes de cada prueba
MiToken = await ethers.getContractFactory("MiToken");
miToken = await MiToken.deploy();
await miToken.deployed();
});
describe("Deployment", function () {
it("Debería establecer el nombre y el símbolo correctos", async function () {
expect(await miToken.name()).to.equal("MiToken");
expect(await miToken.symbol()).to.equal("MTK");
});
it("Debería asignar el suministro total al desplegador", async function () {
const deployerBalance = await miToken.balanceOf(deployer.address);
expect(deployerBalance).to.equal(initialSupply);
expect(await miToken.totalSupply()).to.equal(initialSupply);
});
it("Debería establecer el desplegador como el propietario", async function () {
expect(await miToken.owner()).to.equal(deployer.address);
});
});
describe("Transactions", function () {
it("Debería transferir tokens entre cuentas", async function () {
// Transferir 50 tokens del desplegador a addr1
await miToken.transfer(addr1.address, ethers.utils.parseUnits("50", 18));
const addr1Balance = await miToken.balanceOf(addr1.address);
expect(addr1Balance).to.equal(ethers.utils.parseUnits("50", 18));
const deployerBalance = await miToken.balanceOf(deployer.address);
expect(deployerBalance).to.equal(initialSupply.sub(ethers.utils.parseUnits("50", 18)));
});
it("Debería fallar si el remitente no tiene suficientes tokens", async function () {
const initialDeployerBalance = await miToken.balanceOf(deployer.address);
// Intentar transferir más tokens de los que tiene el desplegador a addr1
await expect(
miToken.connect(addr1).transfer(deployer.address, ethers.utils.parseUnits("1", 18))
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
});
it("Debería actualizar los balances después de la aprobación y la transferencia from", async function () {
// El desplegador aprueba a addr1 para gastar 100 tokens
await miToken.approve(addr1.address, ethers.utils.parseUnits("100", 18));
const allowance = await miToken.allowance(deployer.address, addr1.address);
expect(allowance).to.equal(ethers.utils.parseUnits("100", 18));
// addr1 gasta 50 tokens del desplegador y los envía a addr2
await miToken.connect(addr1).transferFrom(deployer.address, addr2.address, ethers.utils.parseUnits("50", 18));
expect(await miToken.balanceOf(addr2.address)).to.equal(ethers.utils.parseUnits("50", 18));
expect(await miToken.balanceOf(deployer.address)).to.equal(initialSupply.sub(ethers.utils.parseUnits("50", 18)));
expect(await miToken.allowance(deployer.address, addr1.address)).to.equal(ethers.utils.parseUnits("50", 18));
});
});
describe("Minting", function () {
it("Solo el propietario debería poder acuñar tokens", async function () {
const mintAmount = ethers.utils.parseUnits("500", 18);
const initialTotalSupply = await miToken.totalSupply();
await miToken.connect(deployer).mint(addr1.address, mintAmount);
expect(await miToken.balanceOf(addr1.address)).to.equal(mintAmount);
expect(await miToken.totalSupply()).to.equal(initialTotalSupply.add(mintAmount));
// Intentar acuñar con una cuenta que no es el propietario debería revertir
await expect(
miToken.connect(addr1).mint(addr2.address, mintAmount)
).to.be.revertedWith("Ownable: caller is not the owner");
});
});
describe("Burning", function () {
it("Los usuarios deberían poder quemar sus propios tokens", async function () {
const burnAmount = ethers.utils.parseUnits("10", 18);
const initialDeployerBalance = await miToken.balanceOf(deployer.address);
const initialTotalSupply = await miToken.totalSupply();
await miToken.connect(deployer).burn(burnAmount);
expect(await miToken.balanceOf(deployer.address)).to.equal(initialDeployerBalance.sub(burnAmount));
expect(await miToken.totalSupply()).to.equal(initialTotalSupply.sub(burnAmount));
});
it("Debería fallar si se intenta quemar más tokens de los que se poseen", async function () {
const burnAmount = ethers.utils.parseUnits("10000000", 18); // Más de lo que tiene el desplegador
await expect(
miToken.connect(deployer).burn(burnAmount)
).to.be.revertedWith("ERC20: burn amount exceeds balance");
});
});
});
Para ejecutar las pruebas:
npx hardhat test
Si todas las pruebas pasan, verás un resultado similar a:
MiToken
Deployment
✅ Debería establecer el nombre y el símbolo correctos
✅ Debería asignar el suministro total al desplegador
✅ Debería establecer el desplegador como el propietario
Transactions
✅ Debería transferir tokens entre cuentas
✅ Debería fallar si el remitente no tiene suficientes tokens
✅ Debería actualizar los balances después de la aprobación y la transferencia from
Minting
✅ Solo el propietario debería poder acuñar tokens
Burning
✅ Los usuarios deberían poder quemar sus propios tokens
✅ Debería fallar si se intenta quemar más tokens de los que se poseen
10 passing (XXXms)
Próximos Pasos y Consideraciones Avanzadas 🚀
Has creado y desplegado un token ERC-20 funcional. Este es solo el comienzo. Aquí hay algunas ideas para continuar:
- ERC20Capped: Añade un límite máximo al suministro total de tu token.
- ERC20Pausable: Permite al propietario pausar todas las transferencias de tokens en caso de emergencia.
- ERC20Snapshot: Permite tomar 'instantáneas' de los balances de los tokens en un momento dado, útil para airdrops o sistemas de votación.
- Implementar un Token de Utilidad: Diseña un caso de uso real para tu token (por ejemplo, para pagar tarifas en una dApp, votar en una DAO, o como recompensa).
- Auditorías y Seguridad: Para proyectos en producción, es esencial realizar auditorías de seguridad profesionales.
- Integración con Front-end: Crea una interfaz de usuario simple (usando React, Vue, o Svelte) para interactuar con tu token usando bibliotecas como Ethers.js o Web3.js.
Preguntas Frecuentes (FAQ) sobre Tokens ERC-20
¿Cuál es la diferencia entre un token y una moneda (coin)?
Una 'moneda' (coin) es la criptomoneda nativa de una blockchain (ej. ETH de Ethereum, BTC de Bitcoin), utilizada para pagar tarifas de transacción y asegurar la red. Un 'token' es un activo que reside en una blockchain existente y está construido sobre un estándar (ej. ERC-20 en Ethereum). Los tokens pueden representar una amplia gama de activos o utilidades.
¿Puedo cambiar los parámetros de mi token después de desplegarlo?
No directamente. Los contratos inteligentes son inmutables una vez desplegados. Si necesitas cambiar la lógica de tu token, generalmente tendrás que desplegar un nuevo contrato y migrar los usuarios (o utilizar un patrón de contrato actualizable, como los proxies, que son más avanzados).
¿Por qué se usan 18 decimales por defecto en ERC-20?
Históricamente, Ethereum usaba 18 decimales para ETH (1 wei = 10^-18 ETH). Este número se adoptó para los tokens ERC-20 para mantener la compatibilidad y permitir una alta granularidad, lo que es útil para representar valores pequeños sin perder precisión. Sin embargo, puedes configurar un número diferente de decimales si lo deseas, pero 18 es el más común.
Completado Desarrollo Web3 Solidity
Tutoriales relacionados
- Decodificando el Futuro: Explorando y Utilizando Identidades Descentralizadas (DID) en Web3 🔑intermediate15 min
- Creando y Utilizando un Mercado NFT Descentralizado con IPFS y Smart Contracts 🌐intermediate20 min
- Desplegando tu Primer Contrato Inteligente en Ethereum con Hardhat y Solidity 🚀intermediate20 min
- Explorando y Usando Oráculos Descentralizados en dApps: Conectando Web3 con el Mundo Real 🔗intermediate20 min
- Tokenización de Activos del Mundo Real (RWA) en Blockchain: Una Guía Práctica 🌎intermediate15 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!