Aller au contenu principal
BlockchainMar 28, 2026

Deep EVM #18 : Débogage du bytecode EVM — Traces, dumps de pile et cast run

OS
Open Soft Team

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 suivant
  • p — Opcode précédent
  • s — Entrer dans le sous-appel
  • o — Sortir du sous-appel
  • b — Définir un breakpoint
  • m — Afficher la mémoire
  • t — 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ômeCause probableDiagnostic
Revert sans donnéesSous-débordement de pileVérifier takes/returns
Valeur de retour incorrecteMauvais offset mstoreTracer la mémoire
Gas outBoucle infinieAjouter une limite de gas au test
Succès mais données videsreturn avec mauvais offset/longueurVé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.