# Cómo crear una criptomoneda con Vyper y Python > Guía práctica para construir, optimizar y desplegar un token ERC20 profesional utilizando Vyper 0.4.3 y Python. **Published by:** [rabuawad](https://paragraph.com/@rabuawad/) **Published on:** 2026-03-05 **Categories:** vyper, python, dev, evm, erc20 **URL:** https://paragraph.com/@rabuawad/como-crear-una-criptomoneda-con-vyper-y-python ## Content Bienvenidos. En este artículo cubriremos algo muy simple: cómo crear una criptomoneda usando Vyper y Python. Esta guía está diseñada para principiantes o para entusiastas que ya conocen un poco de blockchain y quieren profundizar. No es una introducción a la programación (no veremos variables o funciones básicas) ni a conceptos fundamentales de blockchain. Se asume que ya entiendes qué es una criptomoneda y una dirección (wallet address). Al final, tendremos una criptomoneda escrita en Vyper lista para la red de Arbitrum.A lo largo de este artículo, usaré los términos "criptomoneda" y "ERC20" como sinónimos.¿Qué es una criptomoneda? ¿Qué es un ERC20?Un ERC20 es el estándar técnico utilizado para contratos inteligentes en la Ethereum Virtual Machine (EVM). El EIP-20 define las reglas que un contrato debe seguir para ser considerado una criptomoneda y ser compatible con exchanges y billeteras. Esencialmente, necesitamos implementar 6 funciones básicas:FunciónDescripcióntotalSupplyEl número total de monedas en circulación.balanceOfMuestra el balance de una dirección específica.transferEnvía monedas desde el propietario hacia un destino.allowanceCantidad que un tercero tiene permitido mover en nombre del dueño.approveAutoriza a una dirección externa a mover monedas por parte del dueño.transferFromPermite a una dirección autorizada ejecutar el movimiento de fondos. Además, implementaremos 3 funciones opcionales que mejoran la experiencia de usuario:FunciónDescripciónnameNombre de la criptomoneda (ej. Bitcoin).symbolSímbolo de la criptomoneda (ej. BTC).decimalsCantidad de decimales (usualmente 18).ConfiguraciónUsaremos una configuración mínima con uv como manejador de dependencias y Titanoboa para compilar Vyper.Instalar UV.Crear el proyecto: uv init Crear entorno virtual: uv venvActivar el entorno:Linux/macOS: source .venv/bin/activateWindows: .venv\Scripts\activateInstalar Titanoboa: uv add titanoboaEstructura del proyecto📂 contracts/: Donde almacenaremos el contrato inteligente.📂 scripts/: Scripts de Python para el despliegue.📂 tests/: Pruebas unitarias para nuestro ERC20.Desarrollo de nuestra criptomonedaCrea el archivo contracts/ERC20.vy.PragmaEl pragma indica qué versión del compilador se usó. Usaremos la 0.4.3.# pragma version ==0.4.3Implementando totalSupply e inmutablesDefiniremos el nombre y símbolo como variables de estado. Para el suministro total, usaremos una variable pública.name: public(String[32]) symbol: public(String[10]) decimals: public(uint8) totalSupply: public(uint256) @deploy def __init__(_name: String[32], _symbol: String[10]): self.name = _name self.symbol = _symbol self.decimals = 18Al usar public(), Vyper crea automáticamente la función de lectura para nosotros.Implementando balanceOfUsaremos un HashMap para rastrear cuánto tiene cada billetera.balanceOf: public(HashMap[address, uint256])Dato: Es buena idea marcar variables internas con _ (guion bajo), pero al usar public(), Vyper maneja la visibilidad externa por nosotros de forma eficiente.Nueva Sección: Implementando transferLa función transfer es el corazón de nuestra moneda. Es la que permite mover valor de un punto A a un punto B. En Vyper, la lógica es directa: restamos del emisor y sumamos al receptor.@external def transfer(_to: address, _value: uint256) -> bool: """ @dev Transfiere tokens a una dirección especificada. @param _to La dirección a la que se transfiere. @param _value La cantidad a transferir. """ self.balanceOf[msg.sender] -= _value self.balanceOf[_to] += _value return True¿Qué está pasando aquí?@external: Permite que cualquier persona o contrato llame a esta función.msg.sender: Es una variable global que representa la dirección de quien está ejecutando la transacción.Seguridad: A diferencia de versiones antiguas de otros lenguajes, Vyper 0.4.3 maneja nativamente el desbordamiento (overflow). Si intentas enviar más de lo que tienes, la transacción fallará automáticamente sin necesidad de código extra.Perfecto, vamos a cerrar el estándar. Estas funciones son las que permiten que tu moneda interactúe con el ecosistema DeFi (como Uniswap), permitiendo que otros contratos muevan fondos por ti bajo tu permiso.Delegando poder: approve y transferFromEl concepto de Allowance (concesión) es lo que hace a los ERC20 tan potentes. Permite que autorices a una dirección (por ejemplo, un Exchange Descentralizado) a retirar una cantidad específica de tokens de tu billetera.1. El Mapeo de AllowancePrimero necesitamos una estructura para guardar quién tiene permiso de quién. Usaremos un HashMap anidado:# Propietario -> (Gasta por mí -> Cantidad) allowance: public(HashMap[address, HashMap[address, uint256]])2. Función approveCon esta función, el usuario dice: "Autorizo a esta aplicación a gastar X cantidad de mis tokens".@external def approve(_spender: address, _value: uint256) -> bool: """ @dev Autoriza a `_spender` a transferir hasta `_value` tokens. """ self.allowance[msg.sender][_spender] = _value return True3. Función transferFromEsta es la función que llama la aplicación autorizada. Nota que aquí el remitente no es el dueño de los tokens, sino el "spender".@external def transferFrom(_from: address, _to: address, _value: uint256) -> bool: """ @dev Mueve tokens de `_from` a `_to` usando el mecanismo de allowance. """ # Si no hay suficiente permiso, Vyper lanzará error por overflow self.allowance[_from][msg.sender] -= _value # 2. Mover los balances self.balanceOf[_from] -= _value self.balanceOf[_to] += _value return TrueEventos: El toque finalPara que las interfaces como MetaMask o Etherscan muestren tus transacciones en tiempo real, necesitas Eventos. Sin ellos, el contrato funciona, pero será "invisible" para muchas aplicaciones. Añade esto al inicio de tu archivo, debajo del pragma:event Transfer: sender: indexed(address) receiver: indexed(address) value: uint256 event Approval: owner: indexed(address) spender: indexed(address) value: uint256Importante: Asegúrate de llamar a log Transfer(...) y log Approval(...) dentro de sus respectivas funciones para que el estándar sea 100% oficial.@external def transfer(...) -> bool: # ... log Transfer(_from=msg.sender, _to=_to, _value=_value) return True @external def approve(...) -> bool: # ... log Approval(_owner=msg.sender, _spender=_spender, _value=_value) return True @external def transferFrom(...) -> bool: # ... log Transfer(_from=_from, _to=_to, _value=_value) return TrueCon esto ya tienes un contrato ERC20 completo y funcional. Es minimalista, seguro y sigue las mejores prácticas de Vyper 0.4.3.Optimizaciones: Ahorrando Gas con immutableSi vas a lanzar tu moneda en una red como ** **, cada unidad de gas cuenta. En la versión que escribimos arriba, el name, symbol y decimals se guardan en el almacenamiento de la blockchain. Leer del almacenamiento (o Storage) es una de las operaciones más caras en términos de gas. Para valores que nunca cambian después de desplegar el contrato, Vyper nos ofrece el envoltorio immutable.¿Por qué usar immutable?Al marcar una variable como inmutable, Vyper no la guarda en un espacio de almacenamiento costoso. En su lugar, el valor se "incrusta" directamente en el código (bytecode) del contrato durante el despliegue. Esto hace que leer estos datos sea extremadamente barato (casi gratuito).El código optimizadoAsí es como se ve la implementación usando inmutables en Vyper 0.4.3:# Declaramos las variables inmutables (se recomienda usar MAYÚSCULAS) NAME: immutable(String[10]) SYMBOL: immutable(String[5]) DECIMALS: immutable(uint8) @deploy def __init__(_name: String[10], _symbol: String[5]): # Se asignan una sola vez en el constructor NAME = _name SYMBOL = _symbol DECIMALS = 18 # Creamos funciones externas para cumplir con el estándar ERC20 @external @view def name() -> String[10]: return NAME @external @view def symbol() -> String[5]: return SYMBOL @external @view def decimals() -> uint8: return DECIMALS ¿Cuándo usarlo?SÍ: Para el nombre, símbolo, decimales o una dirección de un dueño inicial que no cambiará.NO: Para balances o cualquier valor que necesites actualizar en el futuro.Con este pequeño cambio, tu contrato no solo es más profesional, sino que tus usuarios te lo agradecerán al pagar menos comisiones por interactuar con tu moneda.Implementación del MintEn el código, implementamos esta lógica dividiéndola en partes: variable inmutable para saber quien es el creador del smart contract, una función externa para el control de acceso y una interna para la lógica contable.OWNER: immutable(address) @deploy def __init__(...): # ... OWNER = msg.sender @external def mint(_to: address, _value: uint256) -> bool: assert msg.sender == OWNER, "Solo el OWNER puede acuñar" self._mint(_to, _value) return True @internal def _mint(_to: address, _value: uint256): self.balanceOf[_to] += _value self.totalSupply += _value log Transfer(_from=empty(address), _to=_to, _value=_value)Puntos clave de esta implementación:Control de Acceso: Utilizamos assert msg.sender == OWNER para asegurar que nadie más pueda inflar el suministro de tokens de manera arbitraria.Gestión del Total Supply: A diferencia de una transferencia común, el mint aumenta la variable self.totalSupply, manteniendo la integridad del balance global del contrato.Convención de Emisión: Siguiendo las buenas prácticas, emitimos el evento Transfer utilizando empty(address) (la dirección 0x00...) como origen. Esto indica a los exploradores de bloques que los tokens no provienen de otro usuario, sino que acaban de ser creados.Separación de Lógica: Definir _mint como una función @internal es una práctica recomendada. Esto nos permite, en versiones futuras, reutilizar la lógica de acuñación desde otras funciones del contrato sin repetir código ni exponer el acceso públicamente.Dato clave: Añadir funciones como mint no rompe la compatibilidad con el estándar. El ERC20 define una interfaz mínima obligatoria; mientras esas funciones base existan y se comporten como se espera, puedes añadir toda la lógica extra que necesites. Tu contrato seguirá siendo un ERC20 legítimo ante cualquier billetera o exchange.Código Completo (Vyper 0.4.3)Aquí tienes el contrato final optimizado y listo para desplegar:# pragma version ==0.4.3 event Transfer: _from: indexed(address) _to: indexed(address) _value: uint256 event Approval: _owner: indexed(address) _spender: indexed(address) _value: uint256 # Inmutables NAME: immutable(String[10]) SYMBOL: immutable(String[5]) DECIMALS: immutable(uint8) OWNER: immutable(address) # Variables de almacenamiento balanceOf: public(HashMap[address, uint256]) allowance: public(HashMap[address, HashMap[address, uint256]]) totalSupply: public(uint256) @deploy def __init__(_name: String[10], _symbol: String[5]): NAME = _name SYMBOL = _symbol DECIMALS = 18 OWNER = msg.sender @external def transfer(_to: address, _value: uint256) -> bool: """ @dev Transfiere tokens a una dirección especificada. @param _to La dirección a la que se transfiere. @param _value La cantidad a transferir. """ self.balanceOf[msg.sender] -= _value self.balanceOf[_to] += _value log Transfer(_from=msg.sender, _to=_to, _value=_value) return True @external def approve(_spender: address, _value: uint256) -> bool: """ @dev Autoriza a `_spender` a transferir hasta `_value` tokens. """ self.allowance[msg.sender][_spender] = _value log Approval(_owner=msg.sender, _spender=_spender, _value=_value) return True @external def transferFrom(_from: address, _to: address, _value: uint256) -> bool: """ @dev Mueve tokens de `_from` a `_to` usando el mecanismo de allowance. """ self.allowance[_from][msg.sender] -= _value self.balanceOf[_from] -= _value self.balanceOf[_to] += _value log Transfer(_from=_from, _to=_to, _value=_value) return True @external def mint(_to: address, _value: uint256) -> bool: """ @dev Acuña `_value` cantidad de monedas a `_to` """ assert msg.sender == OWNER, "Solo el OWNER puede acuñar" self._mint(_to, _value) return True @external @view def name() -> String[10]: return NAME @external @view def symbol() -> String[5]: return SYMBOL @external @view def decimals() -> uint8: return DECIMALS @internal def _mint(_to: address, _value: uint256): self.balanceOf[_to] += _value self.totalSupply += _value log Transfer(_from=empty(address), _to=_to, _value=_value)Esta es la revisión final de la sección de despliegue. He corregido los errores de dedo, unificado la red a Base (para mantener la coherencia con el inicio del artículo) y optimizado el código para que sea un script profesional y seguro.Script en Python: Despliegue a la BlockchainPara el toque final, usaremos Titanoboa (o boa), un framework extremadamente ligero y potente para desplegar y testear contratos de Vyper usando Python puro.1. Configuración del scriptCrea una carpeta llamada scripts/ y dentro un archivo llamado deploy.py. Instalaremos eth-account para manejar nuestras llaves de forma segura: uv add eth-account2. Seguridad ante todo: El KeystoreRegla de oro: Nunca dejes tu llave privada en texto plano en el código o en archivos .env. Es la forma más fácil de que te roben tus fondos. Usaremos un archivo Keystore JSON (un archivo cifrado con contraseña). Si usas herramientas como Ape o Foundry, ya los tienes; si no, asegúrate de generar uno con eth-account siguiendo esta pequeña guia (el codigo para generar un keystore son menos de 35 lineas de Python!).3. El código de despliegueAquí tienes el script completo. He configurado el RPC de Arbitrum Mainnet, pero puedes cambiarlo a Sepia si prefieres testear primero.import getpass import boa from eth_account import Account def load_keystore_account(): """Carga una cuenta desde un archivo keystore de forma segura.""" with open("account.keystore.json", "r") as acc: password = getpass.getpass("\t Ingrese su contraseña del Keystore: ") encrypted_account = acc.read() account_pk = Account.decrypt(encrypted_account, password) return Account.from_key(account_pk) def main(): # 1. Definimos la red (Arbitrum en este caso) # Si quieres puedes un provider como Alchemy/Infura rpc_url = "https://arb1.arbitrum.io/rpc" with boa.set_network_env(rpc_url): # 2. Cargamos la cuenta y la añadimos al entorno de boa account = load_keystore_account() boa.env.add_account(account) print(f"Desplegando contrato con la cuenta: {account.address}...") # 3. Desplegamos el contrato # boa.load compila y envía la transacción de despliegue erc20_contract = boa.load( "contracts/ERC20.vy", "Mi Token", # _name "MT" # _symbol ) print(f"¡Éxito! Contrato desplegado en: {erc20_contract.address}") return erc20_contract if __name__ == "__main__": main()4. Lanzando el TokenPara ejecutarlo, simplemente corre en tu terminal: uv run scripts/deploy.py Titanoboa se encargará de compilar tu código Vyper al vuelo, firmar la transacción con tu cuenta cifrada y enviarla a la red de Arbitrum.Conclusión¡Felicidades! Has pasado de una carpeta vacía a tener una criptomoneda optimizada y desplegada en una Layer 2 usando el stack más moderno de Python y Vyper. Puedes buscar el address de tu contrato en arbiscan.io.GitHub - rafael-abuawad/basic-erc20: Basic ERC20 token in Vyper with standard transfer/approve/transferFrom, owner-only mint, and a Python deploy script for ArbitrumBasic ERC20 token in Vyper with standard transfer/approve/transferFrom, owner-only mint, and a Python deploy script for Arbitrum - rafael-abuawad/basic-erc20https://github.com ## Publication Information - [rabuawad](https://paragraph.com/@rabuawad/): Publication homepage - [All Posts](https://paragraph.com/@rabuawad/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@rabuawad): Subscribe to updates