# Qu'est-ce que l'EVM ? La Machine Virtuelle Ethereum Expliquée
L'Ethereum Virtual Machine (EVM) est le moteur d'exécution qui fait tourner les smart contracts sur Ethereum et toutes les blockchains EVM-compatibles (Polygon, Arbitrum, BSC, etc.). Comprendre l'EVM est crucial pour écrire du code Solidity efficace et sécurisé.
Dans ce guide approfondi, nous explorerons comment l'EVM fonctionne sous le capot, pourquoi c'est important pour les développeurs, et comment optimiser vos contrats pour de meilleures performances.
Qu'est-ce qu'une Machine Virtuelle ?
Une machine virtuelle est une émulation logicielle d'un ordinateur. Tout comme vous pouvez faire tourner Windows sur un Mac avec VMware, l'EVM exécute du bytecode dans un environnement isolé, séparé du système hôte.
L'EVM est :
- Déterministe : Même entrée = même sortie, toujours
- Isolée : Ne peut pas accéder au réseau, au système de fichiers, ou à d'autres contrats sans appels explicites
- Mesurée en gas : Chaque opération coûte du gas pour éviter les boucles infinies
- Basée sur une pile : Utilise une pile pour les calculs (vs basée sur des registres comme x86)
Architecture de l'EVM
Le Modèle de Machine à Pile
L'EVM est une machine virtuelle basée sur une pile. Toutes les opérations manipulent une pile de valeurs de 256 bits.
Exemple : Additionner Deux Nombres
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
Compilé en bytecode, cela devient :
PUSH1 0x00 // Pousse 0 sur la pile
CALLDATALOAD // Charge 'a' depuis calldata
PUSH1 0x20 // Pousse 32 (0x20) sur la pile
CALLDATALOAD // Charge 'b' depuis calldata
ADD // Dépile deux valeurs, pousse la somme
PUSH1 0x00
MSTORE // Stocke le résultat en mémoire
PUSH1 0x20
PUSH1 0x00
RETURN // Retourne 32 bytes depuis la mémoire
Évolution de la pile :
[5] // a = 5 poussé
[5, 3] // b = 3 poussé
[8] // ADD dépile les deux, pousse 8
Organisation de la Mémoire
L'EVM a trois zones de stockage de données :
Comparaison des Coûts :
Accès Stack : 3 gas
Lecture Memory : 3 gas
Expansion Memory : coût quadratique
Écriture Storage : 20 000 gas (nouveau), 5 000 gas (mise à jour)
Lecture Storage : 2 100 gas
Opcodes Clés de l'EVM
Les opcodes sont les instructions assembleur que l'EVM exécute. Solidity compile en opcodes.
Opérations de Pile
PUSH1 0x42 // Pousse 1 byte (0x42) sur la pile
PUSH2 0x1234 // Pousse 2 bytes sur la pile
POP // Retire l'item du haut
DUP1 // Duplique l'item du haut
SWAP1 // Échange les deux items du haut
Arithmétique
ADD // a + b
SUB // a - b
MUL // a * b
DIV // a / b (division entière)
MOD // a % b
EXP // a ** b (exponentiation)
Comparaison & Logique
LT // Less than (inférieur à)
GT // Greater than (supérieur à)
EQ // Equal (égal)
ISZERO // Vérifie si zéro
AND // ET bit à bit
OR // OU bit à bit
XOR // XOR bit à bit
NOT // NON bit à bit
Opérations Mémoire
MLOAD // Charge 32 bytes depuis la mémoire
MSTORE // Stocke 32 bytes en mémoire
MSTORE8 // Stocke 1 byte en mémoire
Opérations Storage
SLOAD // Charge depuis le storage persistant (2 100 gas)
SSTORE // Stocke dans le storage persistant (20 000 gas pour nouveau slot)
Opérations d'Appel
CALL // Appelle un autre contrat
DELEGATECALL // Appelle avec le contexte de l'appelant (utilisé dans les proxies)
STATICCALL // Appel en lecture seule (ne peut pas modifier l'état)
CREATE // Déploie un nouveau contrat
CREATE2 // Déploie à une adresse déterministe
Informations d'Environnement
ADDRESS // Adresse de ce contrat
CALLER // msg.sender
CALLVALUE // msg.value (ETH envoyé)
CALLDATALOAD // Lit depuis calldata
CALLDATASIZE // Taille de calldata
TIMESTAMP // block.timestamp
NUMBER // block.number
GASPRICE // tx.gasprice
Comment les Transactions s'Exécutent
Quand vous envoyez une transaction appelant un smart contract :
- Définit la limite de gas depuis la transaction
- Charge le bytecode du contrat
- Prépare le contexte d'exécution (appelant, valeur, calldata)
- Exécute les opcodes séquentiellement
- Déduit le gas pour chaque opération
- Modifie l'état (storage, balance)
- Succès : Changements validés, gas non utilisé remboursé
- Revert : Changements annulés, gas consommé
- Out of gas : Changements annulés, tout le gas consommé
Explication des Coûts en Gas
Chaque opcode a un coût en gas fixe. Le gas évite les boucles infinies et limite le calcul.
Opérations Courantes :
ADD, SUB, MUL, DIV : 3-5 gas
SLOAD (lecture storage) : 2 100 gas
SSTORE (nouveau slot) : 20 000 gas
SSTORE (mise à jour) : 5 000 gas
LOG0 : 375 gas + 8 gas par byte
CALL : 700 gas de base + coût du transfert
CREATE : 32 000 gas
Pourquoi le storage est-il si cher ?
Le storage est permanent et stocké sur chaque nœud. Écrire dans le storage fait gonfler la blockchain, donc c'est intentionnellement coûteux pour décourager la surutilisation.
Exemple d'Optimisation Gas :
// Mauvais : 3 opérations SLOAD (6 300 gas)
function badSum() public view returns (uint) {
return myValue + myValue + myValue;
}
// Bon : 1 SLOAD, utilise la mémoire (2 100 gas)
function goodSum() public view returns (uint) {
uint val = myValue; // Cache la lecture storage
return val + val + val;
}
Comprendre Calldata, Memory, Storage
Calldata
- Arguments de fonction en lecture seule
- Le moins cher à lire (4 gas par byte non-zéro, 16 gas par byte zéro)
- À utiliser pour les paramètres de fonction dans les fonctions external
function processArray(uint[] calldata data) external {
// 'data' vit dans calldata, pas copié en mémoire
}
Memory
- Stockage temporaire pendant l'exécution de la fonction
- Effacé après le retour de la fonction
- À utiliser pour les variables temporaires, tableaux dynamiques
function buildArray() public pure returns (uint[] memory) {
uint[] memory arr = new uint[](5);
arr[0] = 1;
return arr; // Existe seulement pendant l'exécution
}
Storage
- Stockage blockchain permanent
- Organisé en slots de 32 bytes
- Chaque contrat a son propre storage
uint256 public myValue; // Slot 0
mapping(address => uint) public balances; // Slot 1+
Exemple de Bytecode EVM
Voyons ce que Solidity compile :
contract SimpleStorage {
uint256 public value;
function setValue(uint256 _value) public {
value = _value;
}
}
Compilé avec solc --bin --asm SimpleStorage.sol :
Bytecode (partiel) :
PUSH1 0x80
PUSH1 0x40
MSTORE // Configure le pointeur de mémoire libre
CALLVALUE
DUP1
ISZERO
PUSH2 0x0F
JUMPI // Revert si ETH envoyé à fonction non-payable
...
CALLDATALOAD // Charge le sélecteur de fonction
PUSH4 0x3fa4f245 // Sélecteur setValue(uint256)
EQ
PUSH2 0x1E
JUMPI // Saute à la logique setValue si sélecteur correspond
...
Pourquoi est-ce important ?
- Comprendre le bytecode vous aide à optimiser l'utilisation du gas
- Vous pouvez auditer des contrats en lisant le bytecode
- Débugger des reverts nécessite de connaître le bytecode
EVM vs Autres VMs
| Fonctionnalité | EVM | WASM (Polkadot) | Move VM (Aptos) |
|---------|-----|-----------------|-----------------|
| Taille pile | 1024 items | Dynamique | Basé sur registres |
| Modèle gas | Par-opcode | Par-instruction | Par-bytecode |
| Déterminisme | Complet | Complet | Complet |
| Langage | Solidity | Rust, C++ | Move |
| Upgradabilité | Patterns proxy | Natif | Intégré |
Chaînes EVM-Compatibles
Le succès de l'EVM a conduit à de nombreuses chaînes EVM-compatibles :
- Layer 2s : Arbitrum, Optimism, zkSync, Polygon zkEVM
- Sidechains : Polygon PoS, Gnosis Chain
- L1 alternatifs : BNB Chain, Avalanche C-Chain, Fantom
Pourquoi la compatibilité EVM ?
- Réutiliser les contrats Solidity sans changements
- Mêmes outils de développement (Foundry, Hardhat, Remix)
- Profiter de l'écosystème massif d'Ethereum
Optimiser pour l'EVM
1. Packer les Variables de Storage
// Mauvais : 3 slots de storage
uint256 a; // Slot 0
uint128 b; // Slot 1
uint128 c; // Slot 2
// Bon : 2 slots de storage (b et c packés)
uint256 a; // Slot 0
uint128 b; // Slot 1 (premiers 128 bits)
uint128 c; // Slot 1 (derniers 128 bits)
2. Utiliser immutable et constant
// Mauvais : SLOAD à chaque fois (2 100 gas)
address public owner;
// Bon : Hardcodé dans le bytecode (3 gas)
address public immutable owner;
3. Minimiser les Écritures Storage
// Mauvais : Met à jour le storage dans la boucle
for (uint i = 0; i < 10; i++) {
total += i; // 10 opérations SSTORE
}
// Bon : Utilise la mémoire, écrit une fois
uint temp = total;
for (uint i = 0; i < 10; i++) {
temp += i;
}
total = temp; // 1 SSTORE
4. Utiliser les Events au Lieu du Storage
Pour les données que vous n'avez pas besoin d'interroger on-chain, utilisez des events :
// Mauvais : Stocke chaque transaction (coûteux)
Transaction[] public transactions;
// Bon : Émet un event (375 gas + données)
event TransactionMade(address indexed user, uint amount);
Pourquoi l'EVM est Important pour les Développeurs
Le Futur : Évolution de l'EVM
Améliorations à venir :
- EIP-4844 (Proto-Danksharding) : Disponibilité des données L2 moins chère
- Verkle Trees : Réduire la taille du stockage d'état
- EVM Object Format (EOF) : Meilleure structure de bytecode
- Account Abstraction (ERC-4337) : Comptes programmables
Conclusion
L'EVM est le cœur battant d'Ethereum et de l'écosystème EVM plus large. C'est une simple machine à pile avec une exécution déterministe, mesurée en gas pour éviter les abus.
En tant que développeur Solidity, comprendre l'EVM vous aide à :
- Écrire des contrats moins chers
- Débugger plus rapidement
- Architecturer mieux
- Construire avec confiance