Deep EVM #10: Gestión del Stack en Huff — takes(), returns() y el Arte del dup/swap
Engineering Team
La disciplina del stack
En Huff, no hay variables — todo vive en el stack. Cada macro declara su contrato de stack con takes(N) returns(M): consume N elementos y produce M. Esta anotación no es ejecutada por la EVM — es documentación que el compilador Huff verifica estáticamente.
El stack de la EVM tiene 1024 posiciones, pero solo las 16 superiores son accesibles vía DUP y SWAP. Esto significa que la planificación del layout del stack es una habilidad crítica.
Patrones fundamentales de dup/swap
DUP: copiar un elemento del stack
// DUP1: duplicar el tope
// Stack: [a] -> [a, a]
// DUP2: copiar el segundo elemento
// Stack: [a, b] -> [b, a, b]
// DUP3: copiar el tercero
// Stack: [a, b, c] -> [c, a, b, c]
Regla: DUPn copia el elemento en posición n (contando desde 1 en el tope) y lo empuja al tope.
SWAP: intercambiar elementos
// SWAP1: intercambiar los dos superiores
// Stack: [a, b] -> [b, a]
// SWAP2: intercambiar tope con el tercero
// Stack: [a, b, c] -> [c, b, a]
Rotación del stack
Uno de los patrones más comunes es la rotación — reorganizar elementos sin perder ninguno:
// Rotar 3 elementos: [a, b, c] -> [c, a, b]
#define macro ROT3() = takes(3) returns(3) {
swap2 // [c, b, a]
swap1 // [c, a, b]
}
// Rotar inverso: [a, b, c] -> [b, c, a]
#define macro RROT3() = takes(3) returns(3) {
swap1 // [b, a, c]
swap2 // [b, c, a]
}
Ejemplo práctico: implementación de transferencia
Veamos cómo gestionar el stack para una función transfer ERC-20:
#define macro TRANSFER() = takes(0) returns(0) {
// Leer parámetros del calldata
0x04 calldataload // [to]
0x24 calldataload // [amount, to]
// Verificar amount > 0
dup1 iszero // [amount==0, amount, to]
err jumpi // [amount, to]
// Leer balance del sender
caller // [sender, amount, to]
dup1 // [sender, sender, amount, to]
BALANCE_SLOT() // [slot, sender, amount, to]
sload // [senderBal, sender, amount, to]
// Verificar balance suficiente
dup1 // [senderBal, senderBal, sender, amount, to]
dup4 // [amount, senderBal, senderBal, sender, amount, to]
gt // [amount>bal, senderBal, sender, amount, to]
err jumpi // [senderBal, sender, amount, to]
// Restar del sender
dup3 // [amount, senderBal, sender, amount, to]
swap1 sub // [newSenderBal, sender, amount, to]
dup2 // [sender, newSenderBal, sender, amount, to]
BALANCE_SLOT() // [slot, newSenderBal, sender, amount, to]
sstore // [sender, amount, to]
// Sumar al receptor
swap2 // [to, amount, sender]
dup1 // [to, to, amount, sender]
BALANCE_SLOT() // [slot, to, amount, sender]
dup1 sload // [toBal, slot, to, amount, sender]
dup4 add // [newToBal, slot, to, amount, sender]
swap1 sstore // [to, amount, sender]
// Emitir evento Transfer
swap1 // [amount, to, sender]
0x00 mstore // [to, sender]
swap1 // [sender, to]
[TRANSFER_SIG] // [sig, sender, to]
0x20 0x00 // [0, 32, sig, sender, to]
log3 // []
// Retornar true
0x01 0x00 mstore
0x20 0x00 return
err:
0x00 0x00 revert
}
Herramientas de visualización
Debug del stack en Huff requiere herramientas:
- Comentarios de stack — Documenta el estado del stack después de cada opcode con
// [elem1, elem2, ...] - Foundry traces —
forge test -vvvvmuestra trazas de opcodes con el stack - evm.codes playground — Visualizador interactivo del stack paso a paso
- huffc –print-stack — Algunas versiones del compilador pueden analizar el flujo del stack
Patrones anti-stack-overflow
Con funciones complejas, el stack puede crecer peligrosamente:
// Patrón: almacenar temporalmente en memoria
#define macro COMPLEX_CALC() = takes(5) returns(1) {
// Demasiados valores en el stack? Almacena en memoria
0x00 mstore // Guardar primer valor en mem[0x00]
0x20 mstore // Guardar segundo en mem[0x20]
// ... calcular con los 3 restantes ...
0x00 mload // Recuperar cuando sea necesario
}
Cada MSTORE cuesta 3 gas + posible expansión de memoria. Pero si la alternativa es DUP16+ (que no existe), no hay otra opción.
Errores comunes
- Stack underflow — Intentar POP de un stack vacío causa revert
- Stack imbalance — Una macro que deja elementos extra causa bugs en el caller
- Olvidar POP — Valores huérfanos en el stack acumulan basura
- DUP/SWAP incorrecto — El off-by-one más costoso: DUP2 vs DUP3 puede significar leer el valor equivocado
Conclusión
La gestión del stack es el 80% del trabajo en Huff. Cada macro debe tener un contrato de stack claro, comentarios de estado en cada opcode, y pruebas exhaustivas. Dominar dup/swap/rot es la diferencia entre un contrato Huff funcional y uno con bugs sutiles e impredecibles.