Ir al contenido principal
BlockchainMar 28, 2026

Deep EVM #8: Construyendo un Token Swap en Yul Puro

OS
Open Soft Team

Engineering Team

El reto: un AMM en Yul puro

En este artículo construiremos un Market Maker Automatizado (AMM) simplificado completamente en Yul. Este es el proyecto culminante de nuestra serie sobre Yul — aplicaremos todo lo aprendido: gestión de memoria, acceso a storage, bucles, y llamadas externas.

Un AMM de producto constante (como Uniswap V2) mantiene la invariante x * y = k, donde x e y son las reservas de dos tokens.

Layout de storage

Primero definimos dónde almacenamos el estado:

// Slot 0: token0 (address)
// Slot 1: token1 (address)
// Slot 2: reserve0 (uint256)
// Slot 3: reserve1 (uint256)
// Slot 4: totalSupply de LP tokens
// Slot 5+: mapping balances LP -> keccak256(account, 5)

Despacho de funciones

object "SimpleAMM" {
    code {
        // Constructor: almacenar token0 y token1
        sstore(0, calldataload(0))  // token0
        sstore(1, calldataload(32)) // token1
        
        datacopy(0, dataoffset("runtime"), datasize("runtime"))
        return(0, datasize("runtime"))
    }
    
    object "runtime" {
        code {
            switch shr(224, calldataload(0))
            
            case 0x022c0d9f /* swap(uint256,uint256,address) */ {
                swap(calldataload(4), calldataload(36), calldataload(68))
            }
            
            case 0x0902f1ac /* getReserves() */ {
                mstore(0x00, sload(2))
                mstore(0x20, sload(3))
                return(0x00, 0x40)
            }
            
            default { revert(0, 0) }
            
            // ... funciones helper ...
        }
    }
}

Interacción con tokens ERC-20

Para transferir tokens, necesitamos codificar manualmente las llamadas ERC-20:

function transferFrom(token, from, to, amount) {
    let ptr := mload(0x40)
    
    // transferFrom(address,address,uint256) = 0x23b872dd
    mstore(ptr, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
    mstore(add(ptr, 4), from)
    mstore(add(ptr, 36), to)
    mstore(add(ptr, 68), amount)
    
    let success := call(gas(), token, 0, ptr, 100, 0, 32)
    
    if iszero(success) {
        revert(0, 0)
    }
    
    // Verificar retorno (algunos tokens no retornan bool)
    if returndatasize() {
        if iszero(mload(0)) {
            revert(0, 0)
        }
    }
}

function transfer(token, to, amount) {
    let ptr := mload(0x40)
    
    // transfer(address,uint256) = 0xa9059cbb
    mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)
    mstore(add(ptr, 4), to)
    mstore(add(ptr, 36), amount)
    
    let success := call(gas(), token, 0, ptr, 68, 0, 32)
    
    if iszero(success) {
        revert(0, 0)
    }
}

Lógica de swap con producto constante

La función de swap calcula la cantidad de salida manteniendo la invariante k:

function swap(amountIn, minAmountOut, to) {
    let token0 := sload(0)
    let token1 := sload(1)
    let reserve0 := sload(2)
    let reserve1 := sload(3)
    
    // Determinar dirección del swap basándose en qué token se deposita
    // (simplificado: asumimos token0 -> token1)
    
    // amountOut = (amountIn * 997 * reserve1) / (reserve0 * 1000 + amountIn * 997)
    // El factor 997/1000 = 0.3% de comisión
    let numerador := mul(mul(amountIn, 997), reserve1)
    let denominador := add(mul(reserve0, 1000), mul(amountIn, 997))
    let amountOut := div(numerador, denominador)
    
    // Protección contra deslizamiento
    if lt(amountOut, minAmountOut) {
        mstore(0x00, 0x08c379a0) // Error selector
        revert(0, 4)
    }
    
    // Transferir token0 del usuario a este contrato
    transferFrom(token0, caller(), address(), amountIn)
    
    // Transferir token1 al usuario
    transfer(token1, to, amountOut)
    
    // Actualizar reservas
    sstore(2, add(reserve0, amountIn))
    sstore(3, sub(reserve1, amountOut))
}

Consideraciones de seguridad

Incluso en Yul puro, debemos considerar la seguridad:

  1. Overflow — En Yul no hay checks automáticos. Para multiplicaciones grandes, verificar que mul(a, b) / a == b
  2. Reentrancia — Las llamadas a tokens ERC-20 son interacciones externas. Actualizar reservas ANTES de transferir tokens de salida
  3. Deslizamiento — Siempre verificar amountOut >= minAmountOut
  4. Manipulación de precio — En producción, usar oráculos TWAP en vez de reservas spot

Comparación de gas: Yul vs Solidity

Un swap idéntico implementado en ambos lenguajes muestra diferencias significativas:

OperaciónSolidityYul puroAhorro
Swap simple~85,000 gas~62,000 gas27%
AddLiquidity~120,000 gas~88,000 gas27%
Deployment~1,200,000 gas~450,000 gas62%

El mayor ahorro está en el despliegue — el bytecode de Yul es mucho más compacto porque no incluye el overhead del runtime de Solidity.

Conclusión

Construir un token swap en Yul puro demuestra el poder y la complejidad de programar directamente contra la EVM. Los ahorros de gas son significativos (20-60%), pero el coste es código más complejo y difícil de auditar. En la práctica, este enfoque tiene sentido para protocolos de alto volumen donde el ahorro acumulado justifica la inversión en auditorías adicionales.