Deep EVM #8: Construyendo un Token Swap en Yul Puro
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:
- Overflow — En Yul no hay checks automáticos. Para multiplicaciones grandes, verificar que
mul(a, b) / a == b - Reentrancia — Las llamadas a tokens ERC-20 son interacciones externas. Actualizar reservas ANTES de transferir tokens de salida
- Deslizamiento — Siempre verificar
amountOut >= minAmountOut - 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ón | Solidity | Yul puro | Ahorro |
|---|---|---|---|
| Swap simple | ~85,000 gas | ~62,000 gas | 27% |
| AddLiquidity | ~120,000 gas | ~88,000 gas | 27% |
| Deployment | ~1,200,000 gas | ~450,000 gas | 62% |
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.