Deep EVM #9: Introducción a Huff — Macros, Etiquetas y Opcodes Crudos
Engineering Team
¿Qué es Huff?
Huff es un lenguaje de ensamblador de bajo nivel para la EVM que te da control total sobre el bytecode generado. A diferencia de Yul, que proporciona variables con nombre y funciones, Huff opera directamente sobre el stack — escribes opcodes crudos organizados en macros reutilizables.
Huff fue creado por Aztec Protocol para escribir contratos extremadamente optimizados en gas. Hoy es la herramienta preferida para desarrolladores de MEV, puentes cross-chain y cualquier aplicación donde cada unidad de gas cuenta.
¿Por qué Huff en lugar de Yul?
Yul es excelente, pero tiene limitaciones:
- El compilador de Yul inserta su propia gestión de memoria
- Las variables locales de Yul a veces generan SWAP/DUP innecesarios
- No puedes controlar el layout exacto del bytecode
- No puedes crear jump tables O(1)
Huff elimina todas estas capas de abstracción. Lo que escribes es (casi) exactamente lo que se despliega.
Estructura de un contrato Huff
// Definir la interfaz
#define function balanceOf(address) view returns (uint256)
#define function transfer(address, uint256) nonpayable returns (bool)
// Constantes
#define constant OWNER_SLOT = FREE_STORAGE_POINTER()
// Macro principal: punto de entrada
#define macro MAIN() = takes(0) returns(0) {
// Leer el selector de función (primeros 4 bytes)
0x00 calldataload 0xe0 shr
// Despacho de funciones
dup1 __FUNC_SIG(balanceOf) eq balanceOf jumpi
dup1 __FUNC_SIG(transfer) eq transfer jumpi
// Fallback: revert
0x00 0x00 revert
balanceOf:
BALANCE_OF()
transfer:
TRANSFER()
}
// Macro para balanceOf
#define macro BALANCE_OF() = takes(0) returns(0) {
0x04 calldataload // [account]
0x00 mstore // [] (account en mem[0x00])
[OWNER_SLOT] 0x20 mstore // [] (slot en mem[0x20])
0x40 0x00 sha3 // [hash]
sload // [balance]
0x00 mstore // []
0x20 0x00 return // retorna 32 bytes
}
Macros: la unidad fundamental
Las macros en Huff son bloques de código reutilizables. A diferencia de las funciones, las macros se expanden inline — no hay overhead de JUMP.
// takes(N) returns(M) documenta el efecto en el stack
#define macro REQUIRE_NOT_ZERO() = takes(1) returns(1) {
// Stack de entrada: [value]
dup1 // [value, value]
continue jumpi // [value] (salta si value != 0)
0x00 0x00 revert // nunca alcanzado si value != 0
continue:
}
// Uso:
#define macro SAFE_TRANSFER() = takes(3) returns(0) {
// Stack: [to, amount, from]
dup2 // [amount, to, amount, from]
REQUIRE_NOT_ZERO() // [amount, to, amount, from]
// ... lógica de transferencia ...
}
Etiquetas y saltos
En Huff, las etiquetas definen destinos de salto dentro de una macro:
#define macro MAX() = takes(2) returns(1) {
// Stack: [a, b]
dup2 dup2 // [a, b, a, b]
gt // [a>b, a, b]
max_a jumpi // [a, b]
// b >= a: retornar b
swap1 pop // [b]
fin jump
max_a:
// a > b: retornar a
pop // [a]
fin:
}
Cada etiqueta compila a un JUMPDEST en el bytecode. Los saltos son directos — no hay resolución en tiempo de ejecución como en lenguajes de alto nivel.
Constantes y almacenamiento
// FREE_STORAGE_POINTER() asigna slots secuencialmente
#define constant TOTAL_SUPPLY = FREE_STORAGE_POINTER() // slot 0
#define constant BALANCES = FREE_STORAGE_POINTER() // slot 1
#define constant ALLOWANCES = FREE_STORAGE_POINTER() // slot 2
// Constantes literales
#define constant MAX_UINT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
#define constant TRANSFER_SIG = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Compilación y testing
Huff se compila con el compilador huffc:
huffc src/MiContrato.huff -b # Obtener bytecode
huffc src/MiContrato.huff -r # Obtener bytecode de runtime
Para testing, Huff se integra con Foundry:
import {HuffDeployer} from "foundry-huff/HuffDeployer.sol";
contract MiContratoTest is Test {
address contrato;
function setUp() public {
contrato = HuffDeployer.deploy("MiContrato");
}
function testBalanceOf() public {
// Interactuar con el contrato desplegado
(bool ok, bytes memory data) = contrato.staticcall(
abi.encodeWithSignature("balanceOf(address)", address(this))
);
assertTrue(ok);
}
}
Comparación de tamaño de bytecode
Para un token ERC-20 básico:
| Lenguaje | Bytecode runtime | Gas de despliegue |
|---|---|---|
| Solidity (optimizado) | ~2,400 bytes | ~480,000 gas |
| Yul | ~800 bytes | ~160,000 gas |
| Huff | ~350 bytes | ~70,000 gas |
Huff produce bytecode 7x más compacto que Solidity. En despliegue, esto se traduce en ahorros masivos.
Conclusión
Huff es el lenguaje más cercano al metal que puedes usar para la EVM. Las macros proporcionan reutilización sin overhead, las etiquetas dan control preciso sobre el flujo, y la ausencia de abstracciones garantiza que lo que escribes es lo que se despliega. En los próximos artículos exploraremos gestión avanzada del stack, jump tables O(1) y patrones de ejecución adaptativa.