Tutoriel·8 min de lecture·Par Solingo

Qu'est-ce que l'EVM ? La Machine Virtuelle Ethereum Expliquée

Comprenez comment l'EVM exécute les smart contracts. Machine à pile, opcodes, gas, storage — tout ce que vous devez savoir.

# 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 :

  • Stack : 1024 items max, valeurs de 256 bits. Rapide, gratuit (dans les limites).
  • Memory : RAM adressable par byte. Expansible, coûte du gas à l'expansion.
  • Storage : Stockage clé-valeur persistant. Coûteux (20 000 gas pour un nouveau slot).
  • 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 :

  • Transaction soumise à un nœud
  • Transaction ajoutée au mempool (pool d'attente)
  • Mineur/validateur l'inclut dans un bloc
  • L'EVM s'initialise :
  • - 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écution du bytecode :
  • - Exécute les opcodes séquentiellement

    - Déduit le gas pour chaque opération

    - Modifie l'état (storage, balance)

  • Fin de l'exécution :
  • - 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

  • Écrire du code efficace : Comprendre les coûts gas = contrats moins chers
  • Débugger plus intelligemment : Tracer les reverts en analysant les opcodes
  • Auditer mieux : Lire le bytecode pour vérifier les contrats
  • Optimiser stratégiquement : Savoir ce qui est cher (storage) vs pas cher (mémoire)
  • Construire cross-chain : La connaissance de l'EVM se transfère à toutes les chaînes EVM
  • 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

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement