Deep EVM #1 : Comment l'EVM exécute votre code — Opcodes, pile et gas
Engineering Team
L’EVM est une machine à pile
La machine virtuelle Ethereum (EVM) n’est pas comme le processeur x86 de votre ordinateur portable. Elle n’a pas de registres. C’est une machine à pile — chaque calcul empile ou dépile des éléments d’une pile de 1024 éléments où chacun est un mot de 256 bits (32 octets).
Quand vous appelez un smart contract, l’EVM reçoit le bytecode du contrat — une séquence plate d’opcodes d’un octet — et commence l’exécution à l’octet 0. Il n’y a pas de table de fonctions, pas d’en-tête ELF, pas d’étape d’édition de liens. Le bytecode est le programme.
// Solidity :
// uint256 result = 2 + 3;
// Compile en bytecode :
// PUSH1 0x02 PUSH1 0x03 ADD
// Trace de pile :
// [] -> PUSH1 0x02 -> [2]
// [2] -> PUSH1 0x03 -> [2, 3]
// [2, 3] -> ADD -> [5]
Chaque opcode consomme ses opérandes du sommet de la pile et repousse son résultat. L’opcode ADD dépile deux valeurs, les additionne et empile la somme. C’est fondamentalement différent des architectures à registres où vous spécifiez des registres source et destination.
Catégories d’opcodes
L’EVM définit environ 140 opcodes, regroupés par catégories fonctionnelles :
Arithmétique et comparaison
- ADD, SUB, MUL, DIV, MOD — Arithmétique entière 256 bits de base. Tous coûtent 3 gas (niveau G_verylow).
- SDIV, SMOD — Division et modulo signés en complément à deux.
- ADDMOD, MULMOD — Arithmétique modulaire :
(a + b) % Net(a * b) % Nen un seul opcode. Essentiels pour les opérations sur courbes elliptiques, coût de 8 gas. - EXP — Exponentiation. Coûte 10 gas + 50 par octet de l’exposant, ce qui en fait l’un des opcodes arithmétiques les plus chers.
- LT, GT, SLT, SGT, EQ, ISZERO — Opcodes de comparaison qui empilent 1 (vrai) ou 0 (faux).
Opérations bit à bit
- AND, OR, XOR, NOT — Logique bit à bit, 3 gas chacune.
- SHL, SHR, SAR — Décalage à gauche, décalage logique à droite, décalage arithmétique à droite (ajoutés dans Constantinople, EIP-145). Avant leur existence, les décalages nécessitaient MUL/DIV par des puissances de 2.
- BYTE — Extrait un seul octet d’un mot de 32 octets.
BYTE(0, x)retourne l’octet le plus significatif.
Manipulation de la pile
- POP — Supprime l’élément du sommet.
- PUSH1 à PUSH32 — Empile de 1 à 32 octets de données immédiates. PUSH1 est l’opcode le plus fréquent dans le bytecode déployé.
- DUP1 à DUP16 — Duplique le N-ième élément de la pile au sommet.
- SWAP1 à SWAP16 — Échange l’élément du sommet avec le N-ième élément en dessous.
Environnement et informations de bloc
- CALLER (msg.sender), CALLVALUE (msg.value), CALLDATALOAD, CALLDATASIZE, CALLDATACOPY — Accès au contexte de la transaction.
- NUMBER, TIMESTAMP, BASEFEE, CHAINID — Informations au niveau du bloc.
- BALANCE, EXTCODESIZE, EXTCODECOPY — Interrogation d’autres comptes.
Le barème du gas
Chaque opcode a un coût en gas. Le gas sert deux objectifs : il empêche les boucles infinies (problème de l’arrêt) et il tarife équitablement les ressources de calcul.
Les coûts de gas se répartissent en niveaux :
| Niveau | Gas | Exemples |
|---|---|---|
| Zéro | 0 | STOP, RETURN, REVERT |
| Base | 2 | ADDRESS, ORIGIN, CALLER |
| Très bas | 3 | ADD, SUB, LT, GT, AND, OR, POP |
| Bas | 5 | MUL, DIV, MOD |
| Moyen | 8 | ADDMOD, MULMOD, JUMP |
| Élevé | 10 | JUMPI |
| Spécial | variable | SLOAD, SSTORE, CALL, CREATE |
Les opcodes les plus chers sont ceux qui touchent à l’état :
// Coûts de gas pour l'accès à l'état (après EIP-2929) :
// SLOAD (froid) : 2100 gas
// SLOAD (chaud) : 100 gas
// SSTORE (froid, 0->non-zéro) : 22100 gas
// SSTORE (chaud) : 100 gas (+ 20000 si 0->non-zéro)
// CALL (froid) : 2600 gas
// CALL (chaud) : 100 gas
// BALANCE (froid) : 2600 gas
// BALANCE (chaud) : 100 gas
Accès froid vs chaud (EIP-2929)
L’EIP-2929 (mise à jour Berlin, avril 2021) a introduit le concept de liste d’accès — un ensemble par transaction d’adresses et de slots de stockage déjà consultés.
La première fois que vous accédez à un slot de stockage ou à une adresse externe dans une transaction, il est « froid » et coûte du gas supplémentaire. Les accès suivants sont « chauds » et bon marché. C’est pourquoi l’ordre dans lequel vous lisez les slots de stockage est important pour l’optimisation du gas.
// En Solidity, ce pattern est coûteux :
function bad() external view returns (uint256) {
// Première lecture du slot : 2100 gas (froid)
uint256 a = myStorage;
// ... logique ...
// Deuxième lecture : 100 gas (chaud)
uint256 b = myStorage;
return a + b;
}
// Mise en cache mémoire :
function good() external view returns (uint256) {
uint256 cached = myStorage; // 2100 gas (froid), une seule fois
return cached + cached; // 6 gas (ADD + DUP)
}
Flux d’exécution : que se passe-t-il dans une transaction
Quand vous envoyez une transaction qui appelle un contrat, voici la séquence complète :
- Validation de la transaction — Vérification du nonce, solde >= valeur + gas * gasPrice, vérification de la signature.
- Déduction du gas intrinsèque — 21 000 gas pour la transaction elle-même, plus 16 gas par octet de calldata non nul et 4 par octet nul.
- Configuration du contexte — L’EVM crée un contexte d’exécution : code, calldata, appelant, valeur, gas restant.
- Le compteur de programme commence à 0 — L’EVM lit l’opcode à la position 0 et l’exécute.
- Exécution séquentielle — Chaque opcode est exécuté, le gas est déduit. JUMP et JUMPI permettent un flux de contrôle non linéaire, mais uniquement vers des positions marquées par JUMPDEST.
- Terminaison — L’exécution se termine par STOP (succès, pas de données de retour), RETURN (succès, avec données de retour), REVERT (échec, état annulé) ou épuisement du gas.
- Validation ou annulation de l’état — En cas de succès, tous les changements d’état sont validés. En cas d’annulation, tous les changements dans ce contexte d’appel sont annulés.
Le compteur de programme et JUMP
Le compteur de programme (PC) est un registre implicite qui suit la position actuelle dans le bytecode. La plupart des opcodes avancent le PC de 1 (ou de 1 + N pour les opcodes PUSH). Deux opcodes modifient directement le PC :
- JUMP — Dépile une destination de la pile, définit le PC à cette valeur. La destination doit contenir un opcode JUMPDEST sinon la transaction échoue.
- JUMPI — Saut conditionnel. Dépile la destination et la condition. Si la condition est non nulle, saut ; sinon continuation séquentielle.
C’est ainsi que l’EVM implémente if/else, les boucles et la dispatch de fonctions. Le compilateur Solidity génère un sélecteur de fonction qui charge les 4 premiers octets du calldata, compare avec les signatures connues et exécute JUMPI vers le bloc de code correspondant.
Sous-appels : CALL, STATICCALL, DELEGATECALL
Les contrats peuvent invoquer d’autres contrats via trois opcodes d’appel :
- CALL — Appel standard. Crée un nouveau contexte d’exécution avec sa propre pile et mémoire. L’appelé s’exécute indépendamment ; s’il reverte, seuls ses changements sont annulés.
- STATICCALL — Appel en lecture seule (EIP-214). Tout opcode modifiant l’état (SSTORE, CREATE, LOG, SELFDESTRUCT) dans l’appelé provoque un revert immédiat.
- DELEGATECALL — Exécute le code de l’appelé dans le contexte de stockage de l’appelant. msg.sender et msg.value sont préservés de l’appel original. C’est ainsi que fonctionnent les patterns proxy et les bibliothèques.
Implications pratiques pour le MEV
Si vous construisez des bots MEV, comprendre l’EVM au niveau des opcodes n’est pas optionnel — c’est une exigence compétitive. Chaque unité de gas économisée dans l’exécution de votre bot est une marge bénéficiaire. Points clés :
- Simulez avant de soumettre — Utilisez
eth_callou un EVM local (revm, EVMONE) pour tracer l’exécution et connaître le coût exact en gas. - Minimisez les accès froids — Préchauffez les slots de stockage via les listes d’accès (EIP-2930).
- Utilisez STATICCALL pour les lectures — Légèrement moins cher et garantit l’absence de mutation d’état.
- Connaissez vos coûts d’opcodes — Un seul SLOAD mal placé peut coûter 2100 gas ; dans un environnement MEV compétitif, c’est la différence entre profit et perte.
Conclusion
L’EVM est élégante dans sa simplicité : une machine à pile avec des mots de 256 bits, un format de bytecode plat et un système de comptage de gas qui tarife chaque opération. Comprendre ce fondement — opcodes, pile et gas — est le prérequis pour tout ce qui suit dans cette série : disposition mémoire, optimisation du stockage, primitives de sécurité, et enfin l’écriture de Yul et Huff bruts.
Dans le prochain article, nous explorerons les quatre emplacements de données de l’EVM : pile, mémoire, stockage et calldata — et pourquoi choisir le bon détermine si votre contrat coûte 0,50 $ ou 50 $ à exécuter.