ERC-20: Tokens Fungibles
ERC-20 es el estándar más utilizado en Ethereum para crear tokens fungibles. Desde stablecoins como USDT y USDC hasta governance tokens como UNI y AAVE, la mayoría de los tokens siguen este estándar.
¿Qué es un Token Fungible?
Un token fungible es un activo digital donde cada unidad es idéntica e intercambiable con cualquier otra:
Fungible (ERC-20): No Fungible (ERC-721):
- 1 USDT = 1 USDT - NFT #1 ≠ NFT #2
- 1 UNI = 1 UNI - Cada uno es único
- Divisible - Indivisible
- Intercambiable - Coleccionable
La Interfaz ERC-20
El estándar define las funciones y eventos mínimos que un token debe implementar:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
// Consultas
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
// Transferencias
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
// Eventos
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
Implementación Completa
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract MiToken is IERC20 {
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 private _totalSupply;
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
constructor(string memory _name, string memory _symbol, uint256 _supply) {
name = _name;
symbol = _symbol;
_totalSupply = _supply * 10 ** decimals;
_balances[msg.sender] = _totalSupply;
emit Transfer(address(0), msg.sender, _totalSupply);
}
function totalSupply() external view returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) external view returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) external returns (bool) {
require(to != address(0), "Transfer to zero address");
require(_balances[msg.sender] >= amount, "Insufficient balance");
_balances[msg.sender] -= amount;
_balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function allowance(address owner, address spender) external view returns (uint256) {
return _allowances[owner][spender];
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
require(to != address(0), "Transfer to zero address");
require(_balances[from] >= amount, "Insufficient balance");
require(_allowances[from][msg.sender] >= amount, "Insufficient allowance");
_allowances[from][msg.sender] -= amount;
_balances[from] -= amount;
_balances[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}
El Flujo Approve + TransferFrom
Este mecanismo permite que contratos terceros muevan tokens en tu nombre:
Escenario: Alice quiere intercambiar tokens en Uniswap
1. Alice llama: token.approve(uniswapRouter, 1000)
→ "Uniswap puede mover hasta 1000 de mis tokens"
2. Alice llama: uniswap.swap(token, 1000)
→ Uniswap internamente: token.transferFrom(alice, pool, 1000)
→ Uniswap mueve los tokens de Alice al pool
Sin approve + transferFrom, los contratos no podrían
interactuar con tus tokens de forma segura.
Decimales
Los tokens ERC-20 no soportan decimales nativamente. Se usa un multiplicador:
Si decimals = 18:
- 1 token = 1000000000000000000 (10^18)
- 0.5 token = 500000000000000000
Si decimals = 6 (como USDC):
- 1 USDC = 1000000
- 0.01 USDC = 10000
En el código siempre trabajamos con enteros (Wei-like).
El frontend convierte para mostrar al usuario.
Extensiones Comunes
Token con Mint y Burn
contract TokenConMintBurn is MiToken {
address public owner;
constructor() MiToken("Mi Token", "MTK", 1000000) {
owner = msg.sender;
}
function mint(address to, uint256 amount) external {
require(msg.sender == owner, "Solo owner");
_totalSupply += amount;
_balances[to] += amount;
emit Transfer(address(0), to, amount);
}
function burn(uint256 amount) external {
require(_balances[msg.sender] >= amount, "Insufficient balance");
_balances[msg.sender] -= amount;
_totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
mapping(address => uint256) internal _balances;
uint256 internal _totalSupply;
}
Token con Pausa
contract TokenPausable {
bool public paused;
address public owner;
modifier whenNotPaused() {
require(!paused, "Token pausado");
_;
}
function pause() external {
require(msg.sender == owner);
paused = true;
}
function unpause() external {
require(msg.sender == owner);
paused = false;
}
// Agregar whenNotPaused a transfer y transferFrom
}
Usando OpenZeppelin
En la práctica, siempre usamos OpenZeppelin para tokens ERC-20:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MiTokenPro is ERC20, ERC20Burnable, Ownable {
constructor()
ERC20("Mi Token Pro", "MTPRO")
Ownable(msg.sender)
{
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
Tokens Famosos ERC-20
Token Símbolo Decimales Uso
──────────────────────────────────────────────
USDT USDT 6 Stablecoin (Tether)
USDC USDC 6 Stablecoin (Circle)
DAI DAI 18 Stablecoin descentralizada
UNI UNI 18 Governance de Uniswap
LINK LINK 18 Oracles de Chainlink
AAVE AAVE 18 Governance de Aave
WETH WETH 18 ETH wrapeado
Resumen
ERC-20 define la interfaz estándar para tokens fungibles en Ethereum: balanceOf, transfer, approve, transferFrom y los eventos Transfer y Approval. El patrón approve-transferFrom permite que contratos muevan tokens con autorización. En producción, siempre usar OpenZeppelin para implementaciones probadas y auditadas.