Deep EVM #18: Depuración de Bytecode EVM — Trazas, Volcados de Stack y cast run
Engineering Team
El reto de depurar bytecode
Cuando un contrato Huff o Yul falla, no hay mensajes de error legibles — solo un REVERT silencioso. La depuración de bytecode requiere herramientas especializadas que muestren el estado del stack, memoria y storage en cada paso.
Trazas de ejecución con Foundry
Foundry proporciona trazas detalladas con el flag -vvvv:
forge test --match-test testTransfer -vvvv
La salida muestra:
[PASS] testTransfer()
Traces:
[65432] HuffToken::transfer(alice, 100)
PUSH1 0x00
CALLDATALOAD
PUSH1 0xe0
SHR
...
SSTORE [slot: 0x..., value: 900]
SSTORE [slot: 0x..., value: 100]
LOG3 (Transfer event)
RETURN
cast run: análisis de transacciones on-chain
cast run de Foundry reproduce una transacción con traza completa:
# Reproducir una transacción fallida
cast run 0xABC123...DEF --rpc-url https://eth.llamarpc.com -vvvv
# Con traza de opcodes
cast run 0xABC123...DEF --debug
Esto es invaluable para entender por qué una transacción de un bot MEV falló en producción.
Depuración paso a paso
Foundry incluye un debugger interactivo:
forge debug --debug src/test/MiTest.t.sol --sig "testTransfer()"
El debugger muestra:
- Estado del stack en cada opcode
- Contenido de la memoria
- Slots de storage modificados
- Gas restante
- Programa counter y opcode actual
Análisis de reverts
Cuando un contrato revierte, el proceso de diagnóstico es:
- Identificar el opcode REVERT — ¿En qué posición del bytecode ocurre?
- Trazar hacia atrás — ¿Qué condición falló justo antes del REVERT?
- Inspeccionar el stack — ¿Los valores son los esperados?
- Verificar calldata — ¿Se decodificaron correctamente los parámetros?
// En tu test, captura el error:
function testShouldRevert() public {
(bool ok, bytes memory returnData) = token.call(
abi.encodeWithSignature("transfer(address,uint256)", address(0), 100)
);
assertFalse(ok, "Debe revertir para address(0)");
// Decodificar datos de revert si los hay
if (returnData.length >= 4) {
bytes4 selector = bytes4(returnData);
emit log_named_bytes4("Error selector", selector);
}
}
Herramientas complementarias
Tenderly
Plataforma web para simular y depurar transacciones con visualización del stack:
https://dashboard.tenderly.co/tx/mainnet/0xABC...
evm.codes
Referencia interactiva de opcodes con playground para probar bytecode:
https://www.evm.codes/playground
Bytecode descompiladores
Herramientas como Heimdall pueden descompilar bytecode a pseudo-Solidity, ayudando a entender contratos de terceros.
Técnicas de depuración para Huff
Logging temporal con eventos
Durante el desarrollo, inserta eventos para trazar valores:
#define macro DEBUG_LOG_UINT() = takes(1) returns(1) {
// Stack: [value]
dup1 // [value, value]
0x00 mstore // [value]
0x00 // topic0 = 0 (debug event)
0x20 0x00 log1 // [value]
}
Verificación de invariantes en runtime
#define macro ASSERT_EQ() = takes(2) returns(0) {
// Stack: [a, b]
eq assert jumpi
0x00 0x00 revert
assert:
}
Patrones comunes de bugs en Huff
- Stack underflow — POP de stack vacío. Causa: macro con takes() incorrecto
- Off-by-one en DUP/SWAP — DUP2 en vez de DUP3. Causa: contar mal la profundidad del stack
- Calldata offset erróneo — Leer del offset equivocado. Causa: no contar el selector de 4 bytes
- Memory overlap — Escribir sobre datos necesarios. Causa: no gestionar el free memory pointer
- Storage slot collision — Dos variables mapeadas al mismo slot. Causa: hash incorrecto del mapping
Conclusión
La depuración de bytecode EVM es un arte que requiere paciencia y herramientas adecuadas. Foundry, cast run, y el debugger interactivo son el toolkit esencial. Para contratos Huff, los comentarios de stack diligentes y el testing diferencial contra implementaciones de referencia son la primera línea de defensa contra bugs silenciosos.