BitrootBlog
Retour au site ↗
© 2026 Bitroot · Le contenu est fourni à titre informatif et ne constitue pas un conseil financier, juridique, fiscal ou d'investissement.
Charte éditorialeRetour au site
← Tous les articles
Fondamentaux EVM·2026/10/04·Environ 17 min

Comparatif des instructions d'appel : CALL, CALLCODE, DELEGATECALL et STATICCALL

Les quatre instructions d'appel ne diffèrent dans le bytecode que par quelques numéros, mais leur sémantique décide dans quel contexte le code s'exécute, quel stockage il écrit et ce que deviennent msg.sender et address(this). En comparant une à une CALL, CALLCODE, DELEGATECALL et STATICCALL, on voit pourquoi les contrats proxy dépendent de DELEGATECALL et pourquoi STATICCALL a été introduit.

Une transaction ordinaire entre dans le contrat A, qui confie l'exécution au contrat d'implémentation B via DELEGATECALL. Le bytecode exécuté provient de B, mais address(this) renvoie l'adresse de A, msg.sender est le compte à l'origine de la transaction, et les écritures de B dans le storage atterrissent dans les slots de A. Remplacer DELEGATECALL par CALL modifie toutes ces valeurs ; le remplacer par STATICCALL fait échouer toute écriture sur une exception.

Au niveau du bytecode, les quatre instructions d'appel ne diffèrent que par un numéro, mais leurs divergences sémantiques traversent les mises à niveau de proxy, les appels de bibliothèque, les requêtes en lecture seule et la protection contre la réentrance. Cet article compare une à une leur comportement en matière de contexte d'exécution, d'appartenance du stockage, de champs de message et de gas, et explique pourquoi DELEGATECALL est devenu la base des contrats proxy, pourquoi STATICCALL a nécessité une EIP dédiée, et pourquoi CALLCODE est passé du statut de membre de Frontier à celui de vestige déprécié.

Numéros et paramètres de pile des quatre instructions

Commençons par aligner les apparences. CALL est 0xf1, CALLCODE est 0xf2, DELEGATECALL est 0xf4 et STATICCALL est 0xfa. Toutes quatre empilent 0 en cas d'échec et 1 en cas de succès, décrivent les intervalles d'entrée et de sortie par un décalage et une longueur en mémoire, sont soumises à la limite de profondeur d'appel de 1024 et échouent en cas de gas insuffisant plutôt que de tronquer silencieusement.

Le nombre de paramètres de pile les répartit en deux groupes. CALL et CALLCODE prennent chacun 7 opérandes : gas, l'adresse cible, value, le décalage d'entrée, la longueur d'entrée, le décalage de sortie et la longueur de sortie. DELEGATECALL et STATICCALL en prennent 6, sans value : DELEGATECALL reprend le msg.value de la portée parente, STATICCALL le fixe à 0. La différence des tables de paramètres est en soi une porte d'entrée vers la sémantique. Des deux instructions qui peuvent définir value, l'une transfère réellement (CALL), l'autre non (CALLCODE) ; des deux qui ne peuvent pas définir value, l'une hérite (DELEGATECALL), l'autre le met à zéro (STATICCALL).

Le traitement des données de retour mérite lui aussi une comparaison. Toutes quatre écrivent les données de retour du sous-appel dans la zone mémoire indiquée par l'appelant, mais la longueur écrite est bornée par le out_size fourni par ce dernier. Depuis que la mise à niveau Byzantium a introduit RETURNDATASIZE et RETURNDATACOPY (EIP-211), l'appelant n'a plus à deviner à l'avance la taille des données de retour : il peut lire la longueur puis copier à la demande, si bien que les proxys et les contrats de transfert génériques n'ont plus besoin de réserver une zone de sortie suffisamment grande.

Comparaison détaillée : d'où vient le code, où s'écrit l'état, à qui appartiennent les champs de message

CALL crée un nouveau contexte d'exécution. Le code provient de l'adresse cible, le storage appartient lui aussi à l'adresse cible, address(this) est le contrat cible et msg.sender le contrat courant qui lance l'appel. Les ethers indiqués par value sont réellement transférés du contrat courant vers l'adresse cible : le solde du contrat courant doit donc être suffisant, faute de quoi l'appel échoue.

CALLCODE récupère lui aussi le code de l'adresse cible pour l'exécuter, mais l'exécution se déroule dans le contexte du compte courant : le storage est celui du contrat courant et address(this) l'adresse du contrat courant. Il présente par rapport à CALL un point contre-intuitif supplémentaire : value peut valoir n'importe quelle valeur fixée par l'appelant, si bien que msg.value peut être réécrit avec un nombre différent de celui de la portée parente, tandis que le prétendu transfert de valeur consiste pour le compte courant à se transférer à lui-même, sans variation réelle de solde. Le contrôle de valeur reste néanmoins nécessaire : la définition de 0xf2 dans le Yellow Paper exige, comme condition préalable à l'appel, que value ne dépasse pas le solde du compte courant, faute de quoi le code appelé n'est pas atteint et la pile reçoit 0. Pour CALLCODE, msg.sender est le contrat courant qui l'exécute : c'est un point commun avec CALL, et aussi sa divergence la plus décisive avec DELEGATECALL.

DELEGATECALL exécute de même le code cible dans le contexte du compte courant, le storage et address(this) désignant le contrat courant, mais il transporte tels quels msg.sender et msg.value de la portée parente dans la portée enfant. L'EIP-7 le formule ainsi : l'expéditeur et la valeur se propagent de la portée parente vers la portée enfant, et le comportement de CALLER et VALUE dans le code enfant est identique à celui de l'environnement parent. Il en résulte un avantage direct : un contrat d'implémentation peut référencer librement msg.sender et msg.value, sans devoir être recompilé spécifiquement pour rester compatible avec les appels de proxy. La section Motivation de l'EIP-7 cite deux autres usages : découper le code d'implémentation en plusieurs morceaux exécutés par segments, afin de contourner la limite d'appel d'environ 3 millions de gas alors en vigueur ; et conserver la source du code dans une adresse modifiable, vers laquelle les appels sont relayés.

STATICCALL crée un contexte d'exécution semblable à celui de CALL : le code et le storage appartiennent à l'adresse cible, address(this) est le contrat cible, msg.sender est l'appelant et msg.value vaut 0. La différence tient à la marque statique apposée sur la portée enfant, qui interdit toute modification d'état pendant l'exécution. Les opérations interdites énumérées par l'EIP-214 comprennent CREATE, CREATE2, LOG0 à LOG4, SSTORE, SELFDESTRUCT ainsi que CALL portant un value non nul ; y recourir déclenche directement une exception au lieu d'exécuter la modification. La spécification prévoit une exception : CALLCODE n'est pas considéré comme une modification d'état même avec un value non nul, car dans sa sémantique cette valeur n'a pas quitté le compte courant.

Un tableau comparatif

Dans le tableau ci-dessous, les champs de message désignent les valeurs observées à l'intérieur du code appelé, et address(this) le résultat empilé par ADDRESS lors de l'exécution du code cible.

InstructionNuméroParamètres de pileOrigine du codeAppartenance du storageaddress(this)msg.sendermsg.valueÉtat modifiable
CALL0xf17 (dont value)Adresse cibleAdresse cibleAdresse cibleContrat courantValeur indiquée, transfert réelAutorisé
CALLCODE0xf27 (dont value)Adresse cibleContrat courantContrat courantContrat courantValeur indiquée, soumise au contrôle de solde mais sans transfert réelAutorisé
DELEGATECALL0xf46Adresse cibleContrat courantContrat courantHérité de la portée parenteHérité de la portée parenteAutorisé
STATICCALL0xfa6Adresse cibleAdresse cibleAdresse cibleContrat courantFixée à 0Interdit

Gas : coût de base, coût d'accès et règle de rétention 63/64

Le gas des quatre instructions résulte de la somme de plusieurs composantes : le coût de base, le coût d'accès à l'adresse cible, le coût d'extension de la mémoire et la quantité effectivement transmise au sous-appel. La part transmise au sous-appel est restituée si elle n'est pas consommée : elle s'apparente donc davantage à un crédit qu'à une dépense.

Le coût d'accès à l'adresse cible est passé à une tarification froid/chaud avec la mise à niveau Berlin (EIP-2929) : le premier accès au sein d'une même transaction est un accès froid, facturé 2600 gas ; un accès ultérieur est chaud, facturé 100 gas. Auparavant, l'EIP-150 avait porté le coût de base de CALL, CALLCODE et DELEGATECALL à 700 gas ; après Berlin, la famille d'appels est passée à la tarification froid/chaud, 2600 et 100 remplaçant le forfait de 700, l'évaluation intervenant avant le calcul du gas disponible. L'ensemble d'adresses est partagé au sein d'une même transaction : si l'exécution d'un niveau est annulée, les nouvelles entrées d'accès de ce niveau le sont aussi. Un appelant qui veut économiser cette dépense peut joindre à sa transaction une liste d'accès (access list) au titre de l'EIP-2930, en déclarant à l'avance les adresses et les slots concernés, au prix d'un frais fixe par entrée.

La valeur et la création de compte entraînent des suppléments. Un CALL portant un value non nul est majoré de 9000 gas ; si le destinataire est un compte dead (inexistant ou vide) et qu'il faut donc le créer dans l'arbre d'état, 25000 s'y ajoutent. Un CALLCODE portant un value non nul est majoré des mêmes 9000, sa valeur étant simplement transférée au compte courant, sans création de compte. DELEGATECALL et STATICCALL n'ont pas de paramètre value et donc ni l'un ni l'autre de ces suppléments.

Le stipend de 2300 gas est un autre point facile à retenir de travers. Un CALL ou un CALLCODE portant un value non nul fait recevoir 2300 gas supplémentaires au sous-appel ; la table des frais du Yellow Paper enregistre ce montant comme « déduit de G_callvalue », autrement dit il est compris dans les 9000 de majoration pour transfert de valeur, et ne constitue pas une subvention venant s'y ajouter. C'est aussi l'origine du plafond de gas de transfer() et send() : ces deux méthodes ne transmettent que 2300 gas, si bien qu'elles échouent dès que le destinataire doit écrire dans le storage ou émettre un journal. La documentation Solidity classe désormais send() et transfer() comme déconseillées et prévoit de les retirer, en recommandant de passer à CALL avec vérification du code de retour.

L'EIP-150 a en outre introduit la règle de « rétention de 1/64 » : lorsque le gas demandé au transfert dépasse les 63/64 du gas encore disponible chez l'appelant, seuls 63/64 sont transmis, la portée parente conservant toujours une part de gas pour traiter le retour d'échec. Cette règle a remplacé la surface d'attaque fondée sur la profondeur d'appel par une limite fondée sur le gas, et explique pourquoi un appel parent récupère le marqueur d'échec et poursuit son exécution même si le sous-appel a épuisé tout son gas, au lieu que la transaction entière s'épuise avec lui.

DELEGATECALL est la base des contrats proxy, au prix de contraintes sur le storage layout

C'est avec DELEGATECALL que le modèle du proxy devient viable : le contrat proxy détient l'état et les actifs, il relaie les appels au contrat d'implémentation, l'implémentation lit et écrit le storage du proxy dans le contexte de celui-ci, et une mise à niveau ne change que l'adresse d'implémentation, l'état étant conservé sur place. C'est également la méthode standard des contrats upgradables sur l'EVM. Une fonction de relais minimale s'écrit à peu près comme suit, l'adresse d'implémentation étant passée en paramètre et n'occupant délibérément aucun storage, pour éviter tout conflit avec la disposition du contrat d'implémentation.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Forwarder {
    function _delegate(address impl) external payable {
        assembly {
            calldatacopy(0, 0, calldatasize())
            // Exécute le code de impl dans le contexte de l'appelant (le proxy) : storage et msg.sender restent du côté du proxy
            let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch ok
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Le prix à payer est l'alignement obligatoire de la disposition du stockage (storage layout). DELEGATECALL ne renomme ni ne migre les slots : le code lit et écrit selon le numéro de slot ou le décalage attribué par le compilateur. Si le proxy déclare lui-même une variable et que le contrat d'implémentation en déclare une au même slot, les deux se recouvrent mutuellement. L'EIP-1967 écrit donc l'adresse d'implémentation dans le slot calculé par bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1), soit 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, position que le compilateur n'a qu'une probabilité négligeable d'attribuer à une variable métier. De même, un contrat d'implémentation ne peut ajouter des variables d'état qu'à la fin : supprimer ou réordonner des variables décale l'ensemble des lectures et écritures après la mise à niveau.

Le constructeur est lui aussi soumis à une contrainte spécifique en mode proxy. Lors du déploiement du contrat d'implémentation, le constructeur s'exécute dans son propre contexte et écrit dans le storage propre du contrat d'implémentation, sans toucher à l'état du proxy. L'initialisation du proxy doit donc passer par une fonction d'initialisation explicite, assortie d'un marqueur garantissant une exécution unique, sans quoi n'importe qui peut réinitialiser le contrat et en prendre le contrôle.

Une autre catégorie de risques vient du code appelé lui-même. DELEGATECALL confère au code cible tous les droits du proxy : écrire n'importe quel slot, détourner le solde, appeler d'autres contrats, et même compromettre la capacité d'exécution du proxy via selfdestruct. Si l'adresse d'implémentation est modifiable à volonté, ou si l'on introduit une bibliothèque non auditée, c'est le contrôle du proxy tout entier que l'on cède. Les fonctions publiques d'une bibliothèque (library) Solidity sont précisément appelées par DELEGATECALL ; le code d'une bibliothèque ne peut donc pas déclarer ses propres variables d'état, sous peine d'entrer en conflit avec la disposition de l'appelant. Les fonctions d'une bibliothèque qui ne sont ni view ni pure sont conçues pour n'être appelées que par DELEGATECALL : leur code d'exécution embarque une protection d'appel, et un CALL lancé directement vers l'adresse de la bibliothèque provoque un revert à l'exécution (les appels aux fonctions view ou pure ne bénéficient pas de cette protection).

STATICCALL transforme les modifications d'état en exceptions

STATICCALL a été introduit par l'EIP-214 et activé avec la mise à niveau Byzantium. L'EIP-214 ne donne que l'opcode et la sémantique, sans nommer la bifurcation : son rattachement provient de l'EIP-609, qui y inscrit l'EIP-214 dans la liste d'inclusion de Byzantium. La motivation est la suivante : après un CALL ordinaire, l'appelant ne peut pas supposer que l'état du contrat appelé est resté inchangé, ce qui rend les problèmes de réentrance difficiles à raisonner localement. L'objectif énoncé par l'EIP-214 est que l'état de tous les comptes soit identique avant et après un appel statique : cet appel peut être vu comme une fonction pure qui ne renvoie que des sorties, sans effet de bord.

La mise en œuvre ajoute un marqueur STATIC à l'EVM. Ce marqueur vaut false par défaut, il est généralement recopié tel quel à l'entrée d'un sous-appel, et seul STATICCALL le passe à true avant de le restaurer au retour. Les opérations interdites ont été listées à la section précédente ; le point clé est que ce marqueur se propage le long de la chaîne d'appels : un CALL lancé depuis un appel statique place lui aussi le sous-appel en mode statique, sans qu'un niveau d'indirection supplémentaire permette de contourner l'interdiction.

Deux bénéfices d'ingénierie en découlent. Les requêtes en lecture seule se composent sans risque : les vues de prix et les appels agrégés multi-contrats n'ont plus à craindre que l'appelé réécrive l'état. La protection contre la réentrance se referme plus tôt : une fois les appels externes en lecture seule placés en mode statique, une écriture par réentrance échoue directement au niveau de l'EVM. Depuis Solidity 0.5.0, les appels aux fonctions view et pure hors bibliothèque sont compilés en STATICCALL par défaut, ce qui ajoute un filet d'exécution aux contraintes de compilation. Les fonctions view des bibliothèques font exception et continuent d'utiliser DELEGATECALL, car l'EVM ne possède aucune instruction réunissant à la fois la sémantique statique et la sémantique de délégation : les fonctions en lecture seule des bibliothèques n'ont donc pas de protection d'état à l'exécution, point à vérifier séparément lors d'un audit.

Les limites doivent être énoncées tout aussi clairement. STATICCALL interdit seulement la modification d'état, pas la lecture ni les rappels : il bloque le chemin d'écriture de la réentrance, ce qui ne signifie pas que le problème de réentrance disparaît entièrement. Il ne remplace pas non plus la vérification à la compilation de view / pure, les deux couvrant des modes de défaillance différents. Un appel statique consomme lui aussi du gas, et le contrat cible peut délibérément faire un revert pour transformer les frais de l'appelant en coût d'attaque ; le mode statique garantit uniquement que l'état ne change pas, pas que l'appel réussit ni que le résultat est fiable.

La place historique de CALLCODE et son retrait

CALLCODE est une instruction présente dès la version Frontier, et la section Motivation de l'EIP-7 en fait directement le pendant de DELEGATECALL. Son objectif de conception est proche de celui de DELEGATECALL — « emprunter le code d'autrui et l'exécuter sur le compte courant » — mais le traitement des champs de message diffère : msg.sender est remplacé par le contrat courant et msg.value peut être fixé arbitrairement. Quand il faut transmettre tels quels l'expéditeur et la valeur d'origine, il est inapte ; l'EIP-7 a comblé cette lacune en les propageant.

La confusion de sa sémantique de valeur a accéléré son retrait. Un CALLCODE portant un value non nul exige que le solde du compte courant soit suffisant, mais transfère la valeur à sa propre adresse, le sous-appel y lit un msg.value réécrit, et aucun mouvement n'apparaît en comptabilité : ce comportement se prête mal au raisonnement intuitif. Quant à l'usage réel, l'auteur de l'EIP-2488 estime que CALLCODE n'a jamais vraiment été utilisé, et la généralisation du modèle du proxy est venue plus tard (DELEGATECALL est arrivé avec Homestead, l'EIP-7 indiquant un bloc d'activation sur le mainnet à 1,150,000) ; ces deux points relèvent d'un jugement fondé sur la chronologie publique et les déclarations de l'auteur, faute de statistiques d'appel quantifiables.

Le retrait s'est fait en deux temps. Depuis Solidity 0.5.0, callcode n'est plus autorisé et seul l'assemblage en ligne peut encore émettre l'instruction 0xf2. La couche protocolaire n'a pas réellement supprimé cet opcode : l'EIP-2488 propose que CALLCODE renvoie systématiquement un échec à partir d'une certaine hauteur de bloc, au motif que supprimer l'opcode ferait avorter sur exception les contrats qui le rencontrent, tandis qu'un retour d'échec laisse à ces contrats la possibilité de le détecter et de se rétablir ; cette proposition reste à ce jour au statut stagnant (Stagnant). Conclusion : CALLCODE a quitté la scène au niveau du langage, mais demeure au niveau du bytecode une sémantique héritée que les implémentations doivent traiter correctement — ne confondons pas les deux.

Le contexte d'appel détermine dans quel slot atterrit l'écriture d'état

Les quatre instructions répondent finalement à la même question : dans le contexte de qui ce code s'exécute-t-il, et dans quel stockage écrit-il ? CALL et STATICCALL écrivent l'état dans le contrat appelé, DELEGATECALL et CALLCODE dans le contrat qui lance l'appel. Pour une exécution monothread, cette distinction n'affecte que la correction ; pour une exécution parallèle, elle détermine l'entrée de la détection de conflits. Pour juger si deux transactions vont interférer, l'ordonnanceur doit savoir quelles combinaisons « adresse plus slot de stockage » elles finissent par écrire, et le contexte d'appel est précisément ce qui décide si l'adresse retenue est celle du proxy ou celle de l'implémentation : en mode proxy, un relais écrit dans les slots du proxy, pas dans ceux du contrat d'implémentation. La manière dont la détection de conflits s'organise autour d'un même slot de stockage sera traitée plus tard, avec la disposition du stockage et les ensembles de lecture/écriture.

Sources

  • EIP-7 « DELEGATECALL », opcode 0xf4, propagation de l'expéditeur et de la valeur, bloc d'activation Homestead 1,150,000 et motivation historique : https://eips.ethereum.org/EIPS/eip-7
  • EIP-214 « New opcode STATICCALL », marqueur statique, liste des opérations interdites, exception CALLCODE : https://eips.ethereum.org/EIPS/eip-214
  • EIP-609 « Hardfork Meta: Byzantium », méta-proposition inscrivant les EIP-211 et EIP-214 dans la liste d'inclusion de Byzantium : https://eips.ethereum.org/EIPS/eip-609
  • EIP-211 « New opcodes: RETURNDATASIZE and RETURNDATACOPY », données de retour dynamiques et BYZANTIUM_FORK_BLKNUM : https://eips.ethereum.org/EIPS/eip-211
  • EIP-150 « Gas cost changes for IO-heavy operations », coût de base d'appel de 700 et règle de rétention 63/64 : https://eips.ethereum.org/EIPS/eip-150
  • EIP-2929 « Gas cost increases for state access opcodes », accès froid à 2600, accès chaud à 100 et ensemble d'accès : https://eips.ethereum.org/EIPS/eip-2929
  • ERC-1967 (ex-EIP-1967) « Proxy Storage Slots », slot de l'adresse d'implémentation keccak256("eip1967.proxy.implementation") - 1 : https://eips.ethereum.org/EIPS/eip-1967
  • EIP-2488 « Deprecate the CALLCODE opcode », motivation d'un retour d'échec systématique, statut stagnant (Stagnant) et non activé : https://eips.ethereum.org/EIPS/eip-2488
  • Ethereum Yellow Paper, définitions et table d'opcodes de CALL / CALLCODE / DELEGATECALL / STATICCALL à l'annexe H (dont la condition value ≤ solde de CALLCODE), table des frais à l'annexe G : https://ethereum.github.io/yellowpaper/paper.pdf
  • Changements incompatibles de Solidity 0.5.0 (callcode n'est plus autorisé), usage de STATICCALL pour view / pure dans la documentation des contrats, usage de DELEGATECALL pour les fonctions view des bibliothèques, protection d'appel des bibliothèques et mention du caractère déconseillé de send() / transfer() : https://docs.soliditylang.org/en/v0.5.0/050-breaking-changes.html et https://docs.soliditylang.org/en/latest/contracts.html
  • Référence des opcodes EVM d'ethereum.org et table dynamique des coûts en gaz de wolflo/evm-opcodes, transfert de valeur 9000, création de compte 25000, stipend 2300 : https://ethereum.org/en/developers/docs/evm/opcodes/ et https://github.com/wolflo/evm-opcodes/blob/main/gas.md

Pour aller plus loin

Ce que signifie réellement la compatibilité EVM : bytecode, précompilations, JSON-RPC et outillage Points chauds de conflit et charges de travail : quand l'EVM parallèle aide vraiment Parallélisation optimiste de Bitroot : détection, réexécution et déterminisme
← PrécédentL'ABI en détail : sélecteur, paramètres statiques et types dynamiques
Sommaire
Numéros et paramètres de pile des quatre instructionsComparaison détaillée : d'où vient le code, où s'écrit l'état, à qui appartiennent les champs de messageUn tableau comparatifGas : coût de base, coût d'accès et règle de rétention 63/64DELEGATECALL est la base des contrats proxy, au prix de contraintes sur le storage layoutSTATICCALL transforme les modifications d'état en exceptionsLa place historique de CALLCODE et son retraitLe contexte d'appel détermine dans quel slot atterrit l'écriture d'étatSourcesPour aller plus loin
Réglages de lecture