Tokens ERC-20: qué son y cómo crearlos desde cero

Bienvenidos a un nuevo artículo. Hoy vamos a cambiar la temática de los últimos artículos y adentrarnos en el que quiero que sea otro de los grandes temas de este blog: la tecnología blockchain.

En este artículo veremos una de las funcionalidades más conocidas de esta tecnología: los tokens ERC-20. En futuras publicaciones veremos en detalle en el funcionamiento de blockchain, analizando los algoritmos criptográficos, las estructuras de datos y los conceptos matemáticos que la sustentan, ya que, en mi opinión, son precisamente estos elementos los que hacen que esta tecnología sea preciosa y apasionante.

Empezaremos explicando qué es un token dentro del mundo cripto y qué tipos de tokens existen. A continuación, desarrollaremos e implementaremos nuestro propio token ERC-20 desde cero, siguiendo las especificaciones del estándar. Finalmente, veremos cómo en la práctica este proceso se simplifica bastante, utilizando herramientas y librerías que nos permiten crear tokens ERC-20 con apenas unas líneas de código.

Espero que os guste.

¿Que es un token en blockchain?

La llegada de Internet supuso una revolución en la forma en que accedemos y compartimos información, democratizando su acceso y haciéndola accesible para todo tipo de públicos. Sin embargo, la irrupción de la tecnología blockchain ha permitido dar un paso más, pasando de lo que conocemos como el Internet de la información al denominado Internet del valor.

Este cambio de paradigma se debe a que blockchain permite transferir valor de manera transparente y descentralizada, sin necesidad de depender de una autoridad central que supervise y valide cada operación. Este salto introduce una serie de conceptos nuevos, entre los que encontramos los tokens.

Un token no es más que una unidad digital que puede representar un determinado valor, activo o derecho y que se emite sobre una blockchain ya existente. A diferencia de las criptomonedas nativas, como BTC (Bitcoin) o ETH (Ether), un token no necesita disponer de su propia red ni implementar un mecanismo de consenso, sino que aprovecha la infraestructura y la seguridad de una blockchain existente.

En la práctica, en redes como Ethereum, un token suele implementarse mediante un contrato inteligente (smart contract) desplegado sobre una blockchain, en el que se define una lógica concreta que permite gestionar los balances de los usuarios, realizar transferencias y, dependiendo de su implementación, emitir o destruir tokens.

Al tratarse de un contrato inteligente desplegado sobre una blockchain, sus operaciones quedan registradas en ella, aprovechando las características de transparencia, trazabilidad y seguridad que ofrece esta tecnología. De esta manera, los usuarios pueden intercambiar tokens sin necesidad de recurrir a un intermediario que valide cada operación.

Otra cuestión distinta, y en la que no entraremos en este artículo, es el valor que puede tener ese token, que dependerá de muchos factores, como su utilidad, la oferta y la demanda o los activos y derechos que represente. Por tanto, el hecho de que un token exista en una blockchain no implica necesariamente que tenga un valor económico.

Los principales tipos de tokens que podemos encontrar, clasificados según su fungibilidad, son los siguientes:

  • Tokens fungibles: son aquellos donde todos los tokens son equivalentes e intercambiables entre sí. Por ejemplo, un billete de 5€ tiene el mismo valor que cualquier otro billete de 5€ que se haya emitido, pues los tokens fungibles se aplica el mismo principio.. En Ethereum, el estándar más utilizado para este tipo de tokens es ERC-20.
  • Tokens no fungibles (NFT): a diferencia de los anteriores, representan activos únicos que no son directamente intercambiables entre sí. Por ejemplo, los famosos NFT, donde cada uno tenía unos atributos propios y por tanto un valor concreto, no se pueden dividir ni son igualmente intercambiables. En Ethereum, uno de los estándares más conocidos es ERC-721.
  • Tokens semifungibles: combinan características de los dos anteriores. Un ejemplo serían las entradas para un evento, que pueden ser equivalentes entre sí antes de utilizarse, pero adquirir características individuales una vez canjeadas. En Ethereum encontramos el estándar ERC-1155, que permite gestionar tokens fungibles y no fungibles dentro de un mismo contrato inteligente.

Además de esta clasificación, los tokens se pueden clasificar por su finalidad como los utility tokens, que permiten acceder a determinados servicios, o los governance tokens, que otorgan derechos de participación en la gobernanza de un proyecto.

En este artículo nos centraremos en los tokens fungibles, concretamente en el estándar ERC-20 de Ethereum, para entender cómo funcionan y cómo podemos implementar nuestro propio token desde cero.

¿Que es el estándar ERC-20?

Como hemos visto, la forma más habitual de crear un token es mediante un contrato inteligente que se despliega sobre una blockchain. Este contrato implementa las estructuras de datos y los métodos necesarios para gestionar los balances de los usuarios, realizar transferencias y, dependiendo de su implementación, emitir o destruir tokens.

Ahora bien, imaginad que cada desarrollador utilizase su propia nomenclatura y sus propios métodos para implementar estas funcionalidades. Sería un auténtico lío, ya que cada vez que quisiésemos desarrollar una aplicación que tuviese que interactuar con diferentes tokens, tendríamos que conocer cómo funciona internamente cada uno de sus contratos y adaptar nuestro código.

Precisamente para evitar este problema surgió el estándar ERC-20 (Ethereum Request for Comments 20), que define una serie de funciones y eventos comunes que deben implementar los tokens fungibles en Ethereum.

De esta manera, independientemente de quién haya desarrollado el token o de cómo esté implementado internamente, cualquier aplicación que siga el estándar ERC-20 sabrá cómo interactuar con él. Esto facilita enormemente la integración e interoperabilidad entre tokens, wallets y aplicaciones descentralizadas.

Es importante entender que ERC-20 no nos dice exactamente cómo debemos programar nuestro token, sino qué funciones y eventos debe ofrecer para que el resto de aplicaciones puedan interactuar con él.

Token ERC-20 desde zero

Para entender realmente cómo funciona un token ERC-20, vamos a desarrollar nuestra propia implementación desde cero, siguiendo las especificaciones del estándar ERC-20

A medida que avancemos, iremos viendo las diferentes funciones y estructuras de datos que necesitamos implementar y explicaremos qué papel desempeña cada una de ellas.

El primer paso será definir una interfaz que recoja las funciones y eventos establecidos por el estándar ERC-20.

En Solidity, una interfaz nos permite definir qué funciones debe ofrecer un contrato inteligente, indicando sus nombres, parámetros y valores de retorno, pero sin implementar su lógica interna.

En nuestro caso, definiremos la interfaz IERC20, siguiendo las especificaciones del estándar. Posteriormente, nuestro contrato implementará esta interfaz, obligándonos a desarrollar las funciones necesarias para cumplir con ERC-20.

// DEFINIMOS UNA INTERFAZ QUE SIGA EL ESTÁNDAR ERC20

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient,uint256 amount) external returns (bool);
    function allowance(address owner,address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender,address recipient,uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner,address indexed spender,uint256 value);
}

Una vez definida la interfaz, vamos a crear nuestro contrato ERC20, que heredará de IERC20 y en el que iremos implementando todas las funciones que hemos definido anteriormente.

Lo primero será declarar las variables de estado que utilizaremos para almacenar la información de nuestro token:

  • _totalSupply: almacenará la cantidad total de tokens existentes.
  • _name: almacenará el nombre de nuestro token.
  • _symbol: almacenará su símbolo, por ejemplo, ETH o USDC.

A continuación, definimos dos estructuras de datos de tipo mapping, que nos permitirán relacionar diferentes claves con sus respectivos valores:

  • _balances: relaciona cada dirección de Ethereum (address) con la cantidad de tokens que posee (uint256). De esta manera, podemos consultar y actualizar el balance de cualquier usuario.
  • _allowances: es un mapping anidado que nos permitirá gestionar las autorizaciones. Es decir, almacenar cuántos tokens ha autorizado un usuario a gastar a otra dirección en su nombre. Más adelante veremos cómo funciona este mecanismo cuando implementemos las funciones approve() y transferFrom().

Por último, definimos el constructor, que se ejecutará una única vez cuando despleguemos el contrato inteligente. Este recibirá el nombre, el símbolo y nos permitirá inicializar sus valores.

Fijaos en que todavía no estamos emitiendo ningún token. Esto se debe a que estamos desarrollando una implementación genérica de ERC-20, que posteriormente podremos reutilizar para crear diferentes tokens.

Cuando creemos nuestro propio token, heredaremos de este contrato y utilizaremos la función interna _mint() para emitir el suministro inicial y asignarlo a la dirección correspondiente.

contract ERC20 is IERC20 {
    // Variables de estado
    uint256 private _totalSupply;
    string private _name;
    string private _symbol;

    // Balances y autorizaciones
    mapping(address => uint256) private _balances;
    mapping(address => mapping(address => uint256)) private _allowances;

    // Constructor
    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }
}

Una vez tenemos definidas las variables de estado, podemos empezar implementando las funciones más sencillas del estándar, aquellas que permiten consultar información del token.

En primer lugar, tenemos totalSupply() y balanceOf(), que nos permiten conocer, respectivamente, el suministro total de tokens y el balance de una dirección determinada.

    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }
    function balanceOf(address account) public view virtual override returns (uint256) {
        return _balances[account];
    }

Estas funciones son bastante simples, consultan las variables de estado y devuelven el valor que tengan en ese momento. También incorporaremos tres funciones adicionales para consultar el nombre, el símbolo y el número de decimales del token. Estas funciones son adicionales , y no son obligatorias según el estándar, pero se suelen implementar ya que son de utilidad para poder interactuar con otros contratos del ecosistema.

    function name() public view virtual returns (string memory) {
        return _name;
    }

    function symbol() public view virtual returns (string memory) {
        return _symbol;
    }

    function decimals() public view virtual returns (uint8) {
        return 18;
    }

La función decimals() nos dice cuántas posiciones decimales utiliza el token para representar sus cantidades. En nuestro caso utilizaremos 18, que es el valor habitual en Ethereum. Esto significa que, para representar un token completo, tendremos que trabajar internamente con 10^18 unidades mínimas. Es importante que tengamos en cuenta que los balances se almacenan como números enteros, no como números decimales!!!.

Transferencia de tokens

¿Que sería del token, si no existiese una función para poder transferirlos? Para ello, ERC-20 define la función transfer(), que recibe la dirección del destinatario y la cantidad de tokens que queremos enviar.

    function transfer(address to,uint256 amount) public virtual override returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

Fijaos en que esta función apenas contiene lógica. Simplemente llama a _transfer(), indicando como origen de la transferencia la dirección que está ejecutando la transacción (msg.sender).

Esto nos permite separar la función pública de la lógica interna que se encarga de realizar las transferencias.

    function _transfer(address from, address to, uint256 amount) internal {
        require(from != address(0), "ERC20: invalid sender");
        require(to != address(0), "ERC20: invalid receiver");

        _update(from, to, amount);
    }

Aquí realizamos dos comprobaciones para asegurarnos de que ni la dirección de origen ni la de destino sean la dirección cero (address(0)).

Pero ¿dónde estamos descontando los tokens del emisor e incrementando el balance del destinatario? En la función interna _update(), que es un compontente central del contrato.

   function _update(address from,address to,uint256 amount) internal virtual {
        if (from == address(0)) {
            // Emisión de tokens
            _totalSupply += amount;
        } else {
            // Descontar balance del emisor
            uint256 fromBalance = _balances[from];

            require(fromBalance >= amount, "ERC20: insufficient balance");

            unchecked {
                _balances[from] = fromBalance - amount;
            }
        }

        if (to == address(0)) {
            // Destrucción de tokens
            unchecked {
                _totalSupply -= amount;
            }
        } else {
            // Incrementar balance del receptor
            unchecked {
                _balances[to] += amount;
            }
        }

        emit Transfer(from, to, amount);
    }

En primer lugar, la función comprueba si la dirección de origen es address(0). Si es la dirección 0, entonces se trata de una llamada que viene de la función _mint ( creación de nuevos tokens), y por tanto debemos incrementar el suministro total mediante:

_totalSupply += amount

En caso contrario, estamos descontando tokens de una dirección existente. Para ello, consultamos su balance y comprobamos mediante require() nos aseguramos de que dispone de suficientes tokens para realizar la operación (no puedes mandar mas tokens de los que tienes).

Si el balance es suficiente, descontamos la cantidad correspondiente mediante:

_balances[from] = fromBalance - amount;

A continuación, realizamos una comprobación similar con la dirección de destino.

Si el destinatario es address(0), significa que estamos destruyendo tokens, por lo que reduciremos el suministro total. En caso contrario, incrementaremos el balance del destinatario:

_balances[to] += amount;

De esta manera, podemos gestionar tres operaciones diferentes utilizando una misma función:

  • Transferencia: descontamos tokens del emisor y los añadimos al destinatario, sin modificar el suministro total.
  • Emisión: incrementamos el suministro total y añadimos los nuevos tokens al destinatario.
  • Destrucción: descontamos tokens de una dirección y reducimos el suministro total.

Por último, emitimos el evento Transfer, informando de las direcciones involucradas y de la cantidad transferida. Este evento posteriormente puede ser capturado por una DAPP ( aplicación descentralizada) para mostrar la información a los usuarios.

Autorizaciones: Approve y Allowance

Hasta ahora hemos visto cómo un usuario puede transferir sus propios tokens a otra dirección. Sin embargo, ERC-20 también permite autorizar a un tercero para que pueda transferir tokens en nuestro nombre. Imaginad, por ejemplo, que queremos interactuar con una aplicación descentralizada que necesita utilizar una determinada cantidad de nuestros tokens. En lugar de transferirlos directamente a esa aplicación, podemos autorizarla para que pueda gastar una cantidad concreta.

Para ello, utilizaremos dos funciones: approve() y allowance().

function allowance(address owner,address spender) public view virtual override returns (uint256) {
    return _allowances[owner][spender];
}

function approve(address spender, uint256 amount) public virtual override returns (bool) {
    _approve(msg.sender, spender, amount);
    return true;
}

La función allowance() nos dice cuántos tokens una dirección determina (owner) a autorizado a otra dirección(spender) para gastar.

Por su parte, approve() establece permite definir esta autorización mediante una llamada a la función interna _approve().

    // Establecer una autorización
    function _approve(address owner, address spender, uint256 amount) internal {
        _approve(owner, spender, amount, true);
    }

    function _approve(address owner,address spender,uint256 amount,bool emitEvent) internal virtual {
        require(owner != address(0), "ERC20: invalid approver");
        require(spender != address(0), "ERC20: invalid spender");

        _allowances[owner][spender] = amount;

        if (emitEvent) {
            emit Approval(owner, spender, amount);
        }
    }

Fijaos en cómo utilizamos el mapping _allowances[owner][spender] para almacenar la cantidad de tokens autorizados. Por ejemplo, si el usuario A autoriza al usuario B a gastar 100 tokens, almacenaremos esa autorización en _allowances[A][B]= 100.

Transferencias delegadas

Hemos visto que podemos autorizar a una dirección (spender) a gastar una cantidad determinada de nuestros tokens. Ahora bien, necesitamos una función que permita gastar esos tokens.

Recordemos que la función transfer() utiliza msg.sender como dirección de origen de la transferencia. Por tanto, imaginad que el usuario A autoriza al usuario B a gastar 100 de sus tokens. Si B ejecuta la función transfer(), estaría transfiriendo sus propios tokens y no los del usuario A, que es precisamente lo que queremos evitar.

Para solucionar este problema, el estándar ERC-20 define la función transferFrom(), que nos permite indicar explícitamente la dirección desde la que queremos transferir los tokens, siempre que quien ejecuta la función( msg.sender) disponga de una autorización suficiente.

function transferFrom(address from,address to,uint256 amount) public virtual override returns (bool) {
    _spendAllowance(from, msg.sender, amount);
    _transfer(from, to, amount);
    return true;
}

Fijaos en que, antes de realizar la transferencia, llamamos a _spendAllowance(), indicando la dirección propietaria de los tokens (from), la dirección que está intentando gastarlos (msg.sender) y la cantidad que queremos transferir.

Esta función se encargará de comprobar que efectivamente el spender puede gastar tokens de From y, en caso afirmativo, descontar la cantidad correspondiente del allowance disponible.

function _spendAllowance(address owner,address spender,uint256 amount) internal virtual {
    uint256 currentAllowance = allowance(owner, spender);
    if (currentAllowance != type(uint256).max) {
        require(currentAllowance >= amount, "ERC20: insufficient allowance");

        unchecked {
            _approve(owner, spender, currentAllowance - amount, false);
        }
    }
}

Si la autorización es suficiente, finalmente ejecutamos _transfer(from, to, amount), que descontará los tokens del balance del usuario A y los añadirá al destinatario que hayamos indicado.

Siguiendo nuestro ejemplo, si A ha autorizado a B a gastar 100 tokens y B ejecuta transferFrom() para enviar 40 tokens de A al usuario C, el resultado será que A tendrá 40 tokens menos, C recibirá esos 40 tokens y B seguirá teniendo exactamente el mismo balance. Además, la autorización de B sobre los tokens de A se reducirá de 100 a 60.

Emisión y destrucción de tokens.

Para terminar nuestra implementación, nos quedan dos funciones internas: _mint() y _burn().

Aunque estas funciones no forman parte de la interfaz obligatoria del estándar ERC-20, son habituales en sus implementaciones y nos permiten gestionar la creación y destrucción de tokens.

function _mint(address account, uint256 amount) internal {
    require(account != address(0), "ERC20: mint to zero address");

    _update(address(0), account, amount);
}

function _burn(address account, uint256 amount) internal {
    require(account != address(0), "ERC20: burn from zero address");

    _update(account, address(0), amount);
}

Como podéis ver, ambas funciones reutilizan _update(), que ya habíamos implementado anteriormente.

En _mint(), indicamos address(0) como origen de la operación, provocando que _update() incremente el suministro total y el balance del destinatario.

Por el contrario, en _burn() utilizamos address(0) como destinatario, provocando que se reduzcan tanto el balance de la dirección de origen como el suministro total.

Es importante entender que ambas funciones son internal, por lo que no pueden ejecutarse directamente desde una wallet. Serán los contratos que hereden de nuestro ERC-20 los encargados de decidir cuándo y cómo utilizarlas.

Con esto ya tenemos implementada la lógica principal de nuestro contrato ERC-20, siguiendo una arquitectura muy similar a la utilizada por OpenZeppelin.

Creando nuestro propio token

Ahora que ya hemos desarrollado nuestra implementación del estándar ERC-20, vamos a utilizarla para crear nuestro propio token, al que llamaremos TokenMJP (MJP).

Para ello, crearemos un nuevo contrato inteligente llamado MyToken, que heredará de nuestro contrato ERC20. Gracias a la herencia, podremos reutilizar todas las funcionalidades que hemos desarrollado anteriormente, sin necesidad de volver a implementar la lógica de transferencias, balances o autorizaciones.

En nuestro caso, además de emitir un suministro inicial de tokens, vamos a permitir que el propietario del contrato pueda emitir nuevas unidades posteriormente. Observar que dependiendo de la lógica de negocio este contrato podrá ser mas o menos complejo ya que es en este contrato donde definimos todas las reglas de negocio, que se apoyarán en la implementación del estándar ERC-20

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.34;

import {ERC20} from "./ERC20.sol";

contract MyToken is ERC20 {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not Allowed");
        _;
    }

    constructor(uint256 initialSupply) ERC20("TokenMJP", "MJP") {
        owner = msg.sender;
        _mint(owner, initialSupply * 10 ** decimals());
    }

    function mint(uint256 amount) public onlyOwner {
        _mint(owner, amount * 10 ** decimals());
    }
}

En primer lugar, mediante is ERC20 indicamos que nuestro contrato hereda de ERC20, por lo que este tendrá todas las funciones públicas que hemos desarrollado anteriormente, como transfer(), balanceOf(), approve() o transferFrom().

A continuación, definimos una variable owner, que almacenará la dirección del propietario del contrato, y un modificador onlyOwner, que nos permitirá restringir determinadas funciones para que únicamente puedan ser ejecutadas por esta dirección. Gracias a este modificador podemos hacer que determinadas funciones, únicamente puedan ser ejecutadas por el propietario del contrato.

Observad que incorporamos una función pública mint(), protegida mediante onlyOwner, que permitirá al propietario emitir nuevos tokens cuando lo considere necesario. Esta función reutiliza el método interno _mint() que hemos implementado anteriormente, por lo que no necesitamos desarrollar ninguna lógica adicional para actualizar los balances o el suministro total.

La implementación de este contrato, contendrá toda la lógica de negocio que queremos aplicar con estos tokens. En este caso se desarrolla una versión sencilla, ya que el objetivo es ver como es el estándar ERC-20 y un caso de uso sencillo de este, pero por ejemplo si queremos montar un sistema de recompensas/votaciones mediante un token ERC-20, sería en este contrato donde escribiríamos toda la lógica.

Con esto ya tenemos nuestro propio token ERC-20. Ahora solo nos queda desplegarlo sobre una blockchain y comprobar que podemos interactuar con él desde una wallet, consultar nuestros balances y realizar transferencias entre diferentes direcciones.

Despliegue y pruebas

Una vez desarrollado nuestro token ERC-20, vamos a desplegarlo sobre una blockchain para comprobar que funciona correctamente y que podemos interactuar con él desde una wallet.

Para ello, utilizaremos Hardhat como entorno de desarrollo y Ganache para levantar una blockchain de Ethereum local, sobre la que desplegaremos nuestro contrato inteligente. Finalmente, utilizaremos MetaMask para interactuar con el token y realizar algunas transferencias.

No vamos a entrar en detalle sobre la configuración de estas herramientas, ya que lo vamos a ver en detalle en otros artículos.

Hemos desplegado el contrato MyToken con un suministro inicial de 1.000 MJP, que se han asignado automáticamente a la dirección que ha realizado el despliegue, tal y como habíamos definido en el constructor.

Una vez desplegado, podemos importar nuestro token en MetaMask utilizando la dirección del contrato inteligente. Como podemos observar, MetaMask reconoce el símbolo MJP, sus 18 decimales y nos permite consultar el balance de nuestra dirección, que inicialmente es de 1.000 MJP.

Ahora que ya tenemos nuestro token disponible en MetaMask, vamos a comprobar una de las funcionalidades que hemos implementado anteriormente: la transferencia entre direcciones.

Para ello, realizaremos una transferencia de 500 MJP desde nuestra wallet a una segunda dirección utilizando MetaMask.

Aunque estemos realizando esta operación desde la interfaz de MetaMask, internamente estamos interactuando con nuestro contrato inteligente mediante la función transfer(), que, como hemos visto anteriormente, se encarga de actualizar los balances del emisor y del destinatario.

Además, esta operación requiere una transacción en la blockchain, por lo que tendremos que pagar las correspondientes comisiones de gas utilizando ETH de prueba de nuestra red local.

Si consultamos Ganache, podemos observar cómo la transacción ha quedado registrada en la blockchain, incluyendo las direcciones involucradas, los datos de la llamada al contrato y el gas consumido.

De esta manera hemos visto cómo funciona el estándar ERC-20, cómo podemos desarrollar nuestro propio token siguiendo sus especificaciones y cómo desplegarlo sobre una red blockchain.

Sin embargo, en la práctica, no es recomendable utilizar una implementación propia de ERC-20, salvo que exista una necesidad concreta que lo justifique. Lo habitual, y una de las buenas prácticas en el desarrollo de contratos inteligentes, es utilizar bibliotecas ampliamente utilizadas, probadas y auditadas, como las que proporciona OpenZeppelin.

Estas bibliotecas nos permiten reutilizar implementaciones consolidadas de diferentes estándares, evitando tener que desarrollar desde cero funcionalidades que ya han sido implementadas y revisadas por la comunidad.

En nuestro caso, si utilizamos la implementación ERC-20 de OpenZeppelin, el contrato MyToken prácticamente no cambiaría. La principal diferencia es que, en lugar de heredar del contrato ERC20 que hemos desarrollado nosotros, heredaremos directamente del que proporciona OpenZeppelin.

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

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

contract MyToken is ERC20 {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not Allowed");
        _;
    }

    constructor(uint256 initialSupply) ERC20("TokenMJP", "MJP") {
        owner = msg.sender;
        _mint(owner, initialSupply * 10 ** decimals());
    }

    function mint(uint256 amount) public onlyOwner {
        _mint(owner, amount * 10 ** decimals());
    }
}

Conclusiones

En este artículo hemos visto qué son los tokens y cómo funciona el estándar ERC-20. Para entenderlo en detalle, hemos implementado nuestra propia versión siguiendo las especificaciones de la EIP-20 de Ethereum, desarrollando las diferentes funciones que nos permiten gestionar los balances, realizar transferencias y autorizar a terceros a gastar nuestros tokens.

Como hemos comentado, en la práctica es recomendable utilizar implementaciones ya probadas y auditadas, como las que proporciona OpenZeppelin, en lugar de desarrollar nuestra propia versión desde cero.

También hemos visto que crear nuestro propio token ERC-20 es bastante sencillo. Básicamente, necesitamos desplegar un contrato inteligente que herede del contrato ERC-20 de OpenZeppelin y, automáticamente, ya dispondremos de todas las funcionalidades que define el estándar. Será en este nuevo contrato donde podremos implementar toda la lógica de negocio que queramos incorporar a nuestro token. El valor que pueda llegar a tener dependerá, en parte, de esta lógica de negocio, de su utilidad y de otros factores como la oferta y la demanda.

Al final, podemos ver que, técnicamente, desarrollar un token ERC-20 no tiene demasiado misterio, lo realmente interesante es el entorno en el que se ejecuta este contrato.

Todas estas funciones se ejecutan sobre una blockchain, aprovechando así las propiedades de transparencia, trazabilidad y descentralización que ofrece esta tecnología.

Por ejemplo, cuando un usuario realiza una transacción desde su wallet, esta viene firmada digitalmente, de manera que la propia red puede verificar que ha sido autorizada por la clave privada correspondiente. Gracias a ello, no necesitamos desarrollar un sistema de autenticación tradicional para identificar qué dirección está realizando una operación, ya que es la propia blockchain la que nos proporciona estos mecanismos.

Por otro lado, gracias a la arquitectura distribuida de blockchain, conseguimos que nuestro contrato inteligente y, por tanto, los balances de los usuarios, formen parte de un estado compartido entre los diferentes nodos de la red. Además, mediante los mecanismos de consenso, los nodos pueden ponerse de acuerdo sobre el estado de la blockchain, sin necesidad de depender de una autoridad central que gestione esta información.

En definitiva, creo que la programación de contratos inteligentes supone una auténtica revolución, no tanto por la complejidad de los contratos que podamos desarrollar, sino por todas las posibilidades que nos ofrece el entorno sobre el que se ejecutan. Y es precisamente esta combinación de criptografía, estructuras de datos, algoritmos de consenso y sistemas distribuidos lo que, en mi opinión, hace que la tecnología blockchain sea tan interesante.

Mateu Joan Perelló
Resumen de privacidad

Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.