Deep EVM #18 : Débogage du bytecode EVM — Traces, dumps de pile et cast run
Engineering Team
Le défi du débogage de bas niveau
Quand une transaction Solidity reverte, vous obtenez typiquement un message d’erreur descriptif. Quand une transaction Huff ou Yul reverte, vous obtenez un payload de revert vide avec zéro contexte. Le débogage au niveau du bytecode nécessite des outils et des modèles mentaux différents.
cast run : rejeu de transactions
cast run de Foundry rejoue une transaction historique et affiche la trace complète :
cast run 0xTRANSACTION_HASH --rpc-url https://eth-mainnet.g.alchemy.com/v2/KEY
La sortie montre chaque appel, sous-appel, et le résultat :
Traces:
[85432] 0xTarget::swap()
├─ [2541] 0xPool::getReserves() [staticcall]
│ └─ ← (1000000000, 2000000000, 1699000000)
├─ [24521] 0xToken::transfer(0xRecipient, 1000)
│ └─ ← true
└─ ← ()
forge debug : analyse pas à pas
forge debug lance un débogueur interactif TUI qui affiche le bytecode, la pile, la mémoire et le stockage à chaque opcode :
forge debug --debug test/Contract.t.sol --sig "test_swap()"
Commandes clés :
n— Opcode suivantp— Opcode précédents— Entrer dans le sous-appelo— Sortir du sous-appelb— Définir un breakpointm— Afficher la mémoiret— Afficher le stockage
Lecture de traces d’opcodes bruts
Pour les contrats Huff, la trace au niveau des opcodes est souvent nécessaire :
cast run 0xHASH --rpc-url URL -t # trace flag
Chaque ligne montre : le compteur de programme, l’opcode, le coût en gas et l’état de la pile.
[0000] PUSH1 0x00 gas=29978993 stack=[]
[0002] CALLDATALOAD gas=29978990 stack=[0x00]
[0003] PUSH1 0xe0 gas=29978987 stack=[0x70a08231...]
[0005] SHR gas=29978984 stack=[0xe0, 0x70a08231...]
[0006] DUP1 gas=29978981 stack=[0x70a08231]
Techniques de débogage courantes
1. Injection de logs temporaires
En Huff, vous ne pouvez pas utiliser console.log. Injectez temporairement des événements LOG0 pour tracer les valeurs :
#define macro DEBUG_LOG() = takes(1) returns(1) {
// takes: [value]
dup1 // [value, value]
0x00 mstore // [value]
0x20 0x00 log0 // [value] — émet un log avec la valeur
}
2. Revert avec données
Pour identifier où un contrat reverte, faites reverter avec des données différentes à chaque point :
// Checkpoint 1
0x01 0x00 mstore
0x20 0x00 revert
// Checkpoint 2
0x02 0x00 mstore
0x20 0x00 revert
La valeur de retour du revert vous indique quel checkpoint a été atteint.
3. Comparaison de traces
Comparez la trace de votre contrat Huff avec celle de l’équivalent Solidity pour identifier les divergences :
# Trace du contrat Huff
forge test --match-test test_huffSwap -vvvvv > huff_trace.txt
# Trace du contrat Solidity
forge test --match-test test_solSwap -vvvvv > sol_trace.txt
# Comparer
diff huff_trace.txt sol_trace.txt
4. Vérification de l’état de la pile
Ajoutez des assertions de pile dans vos tests :
function test_stackState() public {
// Appelez une fonction qui devrait retourner une valeur spécifique
(bool success, bytes memory data) = huffContract.call(
abi.encodeWithSignature("compute(uint256)", 42)
);
assertTrue(success);
// Si le retour est incorrect, la pile est déséquilibrée
assertEq(abi.decode(data, (uint256)), expectedValue);
}
Erreurs courantes et diagnostic
| Symptôme | Cause probable | Diagnostic |
|---|---|---|
| Revert sans données | Sous-débordement de pile | Vérifier takes/returns |
| Valeur de retour incorrecte | Mauvais offset mstore | Tracer la mémoire |
| Gas out | Boucle infinie | Ajouter une limite de gas au test |
| Succès mais données vides | return avec mauvais offset/longueur | Vérifier les arguments de return |
Conclusion
Le débogage du bytecode EVM est un art qui s’apprend par la pratique. Les outils Foundry — cast run et forge debug — combinés avec l’injection de logs et la comparaison de traces, constituent un arsenal efficace pour identifier et corriger les bugs dans les contrats Huff et Yul.