# 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.*

By [rabuawad](https://paragraph.com/@rabuawad) · 2026-03-05

vyper, python, dev, evm, erc20

---

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](https://eips.ethereum.org/EIPS/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ón

Descripción

`totalSupply`

El número total de monedas en circulación.

`balanceOf`

Muestra el balance de una dirección específica.

`transfer`

Envía monedas desde el propietario hacia un destino.

`allowance`

Cantidad que un tercero tiene permitido mover en nombre del dueño.

`approve`

Autoriza a una dirección externa a mover monedas por parte del dueño.

`transferFrom`

Permite a una dirección autorizada ejecutar el movimiento de fondos.

  
Además, implementaremos 3 funciones opcionales que mejoran la experiencia de usuario:

Función

Descripción

`name`

Nombre de la criptomoneda (ej. Bitcoin).

`symbol`

Símbolo de la criptomoneda (ej. BTC).

`decimals`

Cantidad de decimales (usualmente 18).

* * *

Configuración
-------------

Usaremos una configuración mínima con [uv](https://docs.astral.sh/uv/getting-started/installation/) como manejador de dependencias y [Titanoboa](https://titanoboa.readthedocs.io/en/latest/) para compilar Vyper.

1.  **Instalar UV.**
    
2.  **Crear el proyecto:** `uv init <nombre_del_proyecto>`
    
3.  **Crear entorno virtual:** `uv venv`
    
4.  **Activar el entorno:**
    

*   **Linux/macOS:** `source .venv/bin/activate`
    
*   **Windows:** `.venv\Scripts\activate`
    

5.  **Instalar Titanoboa:** `uv add titanoboa`
    

### Estructura del proyecto

*   📂 **contracts/**: Donde almacenaremos el contrato inteligente.
    
*   📂 **scripts/**: Scripts de Python para el despliegue.
    
*   📂 **tests/**: Pruebas unitarias para nuestro ERC20.
    

Desarrollo de nuestra criptomoneda
----------------------------------

Crea el archivo `contracts/ERC20.vy`.

### Pragma

El _pragma_ indica qué versión del compilador se usó. Usaremos la **0.4.3**.

    # pragma version ==0.4.3

### Implementando `totalSupply` e inmutables

Definiremos 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 = 18

Al usar `public()`, Vyper crea automáticamente la función de lectura para nosotros.

### Implementando `balanceOf`

Usaremos 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 `transfer`
---------------------------------------

La 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í?

1.  `@external`: Permite que cualquier persona o contrato llame a esta función.
    
2.  `msg.sender`: Es una variable global que representa la dirección de quien está ejecutando la transacción.
    
3.  **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 `transferFrom`
-------------------------------------------

El 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 Allowance

Primero 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 `approve`

Con 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 True

### 3\. Función `transferFrom`

Esta 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 True

Eventos: El toque final
-----------------------

Para 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: uint256

> **Importante:** 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 True

Con 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 `immutable`
---------------------------------------------

Si 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 optimizado

Así 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 Mint

En 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 Blockchain
--------------------------------------------

Para 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 script

Crea una carpeta llamada `scripts/` y dentro un archivo llamado `deploy.py`.

Instalaremos `eth-account` para manejar nuestras llaves de forma segura: `uv add eth-account`

### 2\. Seguridad ante todo: El Keystore

**Regla 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 despliegue

Aquí 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 Token

Para 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](https://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 Arbitrum
---------------------------------------------------------------------------------------------------------------------------------------------------------------------

Basic ERC20 token in Vyper with standard transfer/approve/transferFrom, owner-only mint, and a Python deploy script for Arbitrum - rafael-abuawad/basic-erc20

https://github.com

![GitHub - rafael-abuawad/basic-erc20: Basic ERC20 token in Vyper with standard transfer/approve/transferFrom, owner-only mint, and a Python deploy script for Arbitrum](https://storage.googleapis.com/papyrus_images/eb08d9a159cbe4aade8780e52525f64dac1c55b1be1bab9fa891c19e78f5f1d3.png)

](https://github.com/rafael-abuawad/basic-erc20)

---

*Originally published on [rabuawad](https://paragraph.com/@rabuawad/como-crear-una-criptomoneda-con-vyper-y-python)*
