---
id: 23
title: "Ne pas confondre les trois types de stockage : Memory, Storage et Transient Storage"
slug: 0-5-three-kinds-of-storage
date: 2026/09/22
summary: "Les mêmes 32 octets placés en memory, en storage ou en transient storage peuvent coûter deux ordres de grandeur de gaz, et leur durée de vie n'a rien de commun. L'article distingue les trois types de stockage selon trois axes — appartenance, durée de vie et tarification — et précise où placer respectivement un verrou de réentrance, un tableau temporaire et une variable d'état."
keywords: mémoire EVM,stockage EVM,stockage transitoire,EIP-1153,SSTORE
heroImage: /images/articles/photos/0-5-three-kinds-of-storage.jpg
---

Un verrou de réentrance placé dans le storage : faire passer pour la première fois un slot froid de 0 à 1 coûte 2 100 de surcoût d'accès froid plus 20 000 pour le Gsset, puis une réécriture à 0 dans la même transaction coûte encore 100, soit une consommation brute d'environ 22 200 gaz ; cette écriture génère dans le même temps un remboursement de 19 900 gaz, alors que le plafond de remboursement est fixé au cinquième de la consommation totale de la transaction. Avec une variable de transient storage, les deux écritures coûtent 100 gaz chacune et ne dépendent plus du compteur de remboursement. La fonctionnalité est strictement identique ; le coût brut varie de deux ordres de grandeur.

L'EVM comporte trois zones de données inscriptibles : la mémoire (memory), le stockage (storage) et le stockage transitoire (transient storage, EIP-1153). Toutes trois se lisent et s'écrivent par mots de 32 octets et peuvent recevoir une affectation au sein d'un contrat ; en revanche, leur unité d'appartenance, leur durée de vie et leurs règles de tarification n'ont rien de commun. Se tromper d'emplacement ne provoque généralement aucune erreur : l'état disparaît simplement là où on ne l'attendait pas, ou la facture de gaz devient dix fois plus élevée. La question à trancher est donc : à qui appartient chacune de ces zones, combien de temps vit-elle, selon quelles règles est-elle tarifée, et que peut-elle porter ?

## Cycle de vie : cadre, compte et transaction, trois échelles distinctes

La mémoire (memory) appartient au cadre d'exécution (execution frame). Le contexte ouvert par un CALL, un DELEGATECALL, un STATICCALL ou un CREATE constitue un cadre : à l'entrée dans le cadre, la mémoire part de zéro, et elle est intégralement jetée lorsque le cadre rend la main ou est annulé. La mémoire du cadre parent n'est pas visible du cadre enfant, et celle du cadre enfant n'est pas renvoyée au cadre parent ; pour transmettre des données entre deux cadres, il n'existe que le calldata et le returndata.

Le stockage (storage) appartient au compte. Il se présente comme une correspondance de slots de 256 bits vers des valeurs de 256 bits, rattachée au nom du compte ; ce qu'on y écrit entre dans le storage trie du compte, devient une partie de l'état global et subsiste à travers les transactions et les blocs. Les valeurs nulles ne sont pas écrites dans le trie : remettre un slot à zéro en retire donc le nœud correspondant, ce qui détermine directement la conception des règles de remboursement.

Le stockage transitoire (transient storage) appartient lui aussi au compte, mais sa portée est celle d'une transaction. Au sein d'une même transaction, tous les cadres de ce compte partagent le même stockage transitoire : la valeur écrite par un appel interne est lisible par l'appel externe. Lorsqu'un cadre est annulé, les écritures effectuées dans ce cadre sont annulées avec lui, comme pour le stockage ; en revanche, un retour normal du cadre ne les annule pas, contrairement à la mémoire. À la fin de la transaction, tout le stockage transitoire est remis à zéro sans condition. L'appartenance connaît une exception : celui d'un DELEGATECALL ou d'un CALLCODE appartient à l'appelant (le contrat qui émet l'instruction), tandis que celui d'un CALL ou d'un STATICCALL appartient à l'appelé.

On peut retenir les trois échelles ainsi : la mémoire a pour unité le cadre, le stockage a pour unité le compte plus la permanence, le stockage transitoire a pour unité le compte plus la transaction.

| Dimension | Mémoire (Memory) | Stockage (Storage) | Stockage transitoire (Transient Storage) |
|------|--------|---------|-------------------|
| Appartenance | Cadre d'exécution | Compte | Compte |
| Durée de vie | Créé à l'entrée du cadre, jeté à sa sortie | Persistant, écrit dans l'état global | Remis à zéro à la fin de la transaction |
| Adressage | Adresse en octets, extension par mots de 32 octets | Slot de 256 bits | Slot de 256 bits |
| Entre appels internes | Non partagé | Partagé | Partagé par tous les cadres du compte |
| Annulation du cadre | Jeté avec le cadre | Annule les écritures du cadre | Annule les écritures du cadre |
| Coût d'une écriture | 3 gaz plus frais d'extension | 100 à 20 000 plus frais d'accès froid | 100 gaz fixes |

## Mémoire (memory) : extension par mots, un coût d'agrandissement quadratique

La mémoire s'adresse en octets, mais son allocation se fait au grain du mot de 32 octets. Accéder à un mot encore jamais touché déclenche une extension ; les frais d'extension se calculent sur l'occupation totale, selon la formule C_mem(a) = 3a + ⌊a² / 512⌋, où a est le nombre de mots. Le montant réellement débité est la différence entre le C_mem après extension et le C_mem avant extension. MSIZE ne fait que croître : il est impossible de libérer de la mémoire déjà allouée à l'intérieur d'un cadre.

Dans cette expression, la première moitié est un terme linéaire et la seconde un terme quadratique. Tant que a < 23 (soit 704 octets), le terme quadratique est arrondi vers le bas à 0 et le coût paraît modéré ; au-delà de ce point, la croissance s'accélère. Les deux valeurs absolues ci-dessous sont calculées d'après la formule (328) du Yellow Paper, sur la base d'une extension unique d'un cadre parti d'une mémoire vide et hors coût de base de MLOAD et MSTORE : pour 32 Ko de mémoire, a = 1 024 et les frais d'extension valent 3 × 1 024 + 1 024² / 512 = 5 120 gaz ; pour 1 Mo de mémoire, a = 32 768 et les frais valent 98 304 + 2 097 152 ≈ 2,19 millions de gaz. Un seul cadre peut ainsi brûler plusieurs millions de gaz rien qu'en étendant sa mémoire, ce qui constitue généralement une contrainte dure pour les gros tampons alloués dans un cadre.

MLOAD et MSTORE ont un coût de base de 3 gaz (Gverylow), les frais d'extension venant en sus. Lire une mémoire jamais écrite renvoie 0, mais il faut tout de même payer l'extension des mots nouvellement touchés, car l'allocation a bien eu lieu. C'est aussi pourquoi les écritures clairsemées à une adresse élevée coûtent si cher : écrire une seule valeur à un décalage lointain fait facturer tous les mots intermédiaires comme alloués.

Les tableaux temporaires, les tampons d'encodage et de décodage ABI et les entrées de fonction de hachage vont dans la mémoire. Les variables memory, les tableaux memory et les structures memory de Solidity atterrissent tous dans cette zone. L'extrait suivant alloue la longueur en une seule fois, pour éviter de toucher de nouveaux mots à répétition dans la boucle :

```solidity
// Extension en une seule fois à n mots, les écritures suivantes ne génèrent plus de frais d'extension
uint256[] memory buf = new uint256[](n);
for (uint256 i = 0; i < n; ++i) {
    buf[i] = i;
}
```

La frontière est nette : ce que la mémoire transmet à un cadre enfant via CALL est une copie du contenu, le pointeur lui-même n'étant valide que dans le cadre courant. Placer dans la mémoire une valeur intermédiaire destinée à être partagée entre cadres revient à supposer que le cadre enfant voit la mémoire du cadre parent — supposition qui ne tient pas.

Dans la pratique, le choix le plus fréquent est de contourner la mémoire. Les gros blocs de données passent en calldata et ressortent en returndata : le coût se compte alors à l'octet et n'occupe pas la mémoire du cadre. Il ne devient nécessaire de les déployer en mémoire qu'en cas de lectures et écritures répétées dans un même cadre, ou lorsqu'il faut construire une entrée de hachage. Cet arbitrage explique pourquoi de nombreux contrats déportent le calcul dans un script externe ou un sous-appel, puis en agrègent le résultat via les valeurs de retour, de façon à garder petits les tampons d'un même cadre.

## Stockage (storage) : écrit dans le storage trie du compte, un ordre de grandeur plus cher en écriture qu'en lecture

Dans l'état global, chaque compte porte un storage trie dont les slots sont des clés de 256 bits et les valeurs des mots de 256 bits, les valeurs nulles n'entrant pas dans l'arbre. La lecture passe par SLOAD. Avec le mécanisme d'accès froid et chaud introduit par l'EIP-2929 (hard fork Berlin, bloc 12 244 000, 15 avril 2021), le premier accès à une paire (adresse, slot) est un accès froid, facturé 2 100 gaz (Gcoldsload) ; un accès déjà effectué dans la transaction courante est un accès chaud, facturé 100 gaz (Gwarmaccess). L'ensemble des accès froids et chauds a pour portée la transaction ; il est annulé en même temps qu'elle.

L'écriture passe par SSTORE, dont le coût obéit aux règles de comptage net de l'EIP-2200 et dépend de trois valeurs : la valeur d'origine du slot au début de la transaction, sa valeur courante et la nouvelle valeur à écrire. Après Berlin, les constantes sont les suivantes : un slot froid coûte 2 100 de plus ; lorsque la valeur d'origine égale la valeur courante (le slot n'a pas encore été modifié par la transaction), écrire une valeur non nulle à partir de 0 coûte Gsset = 20 000, et écrire une autre valeur ou 0 à partir d'une valeur non nulle coûte Gsreset = 2 900 ; lorsque le slot a déjà été modifié par la transaction (valeur d'origine différente de la valeur courante), seul l'accès chaud à 100 gaz est facturé ; une écriture inutile, où la nouvelle valeur égale la valeur courante, coûte également 100.

Les règles de remboursement ont été resserrées par l'EIP-3529 (hard fork London, bloc 12 965 000, 5 août 2021). Le remboursement pour réécriture d'une valeur non nulle vers zéro passe de 15 000 à 4 800 (SSTORE_RESET_GAS plus ACCESS_LIST_STORAGE_KEY_COST), le remboursement de SELFDESTRUCT est supprimé, et le plafond de remboursement total d'une transaction est ramené à gas_used // 5. Le schéma où la valeur d'origine vaut 0 et où la transaction écrit d'abord une valeur non nulle puis la remet à 0 produit toujours un remboursement de 19 900 gaz (20 000 moins 100), mais reste soumis au plafond du cinquième.

| Opération (tarification Berlin / London) | Gaz |
|--------------------------|-----|
| SLOAD, accès froid / accès chaud | 2 100 / 100 |
| SSTORE, valeur d'origine égale à la valeur courante, écriture de 0 vers une valeur non nulle | 20 000, plus 2 100 d'accès froid |
| SSTORE, valeur d'origine égale à la valeur courante, valeur non nulle vers non nulle ou vers zéro | 2 900, plus 2 100 d'accès froid ; remboursement de 4 800 en cas de remise à zéro |
| SSTORE, slot déjà modifié par la transaction | 100 |
| Verrou de réentrance 0 → 1 → 0 (même transaction) | 22 200 brut, 19 900 remboursés |

Ce qui doit survivre d'une transaction à l'autre va dans le stockage : soldes, propriété, configuration, compteurs cumulés. Le coût est double : d'une part le gaz, d'autre part le gonflement de l'état, puisque tous les nœuds complets doivent conserver durablement ces slots. Le remboursement crée facilement l'illusion que « remettre à 0 est gratuit » ; en réalité, il n'est réglé qu'après la fin de la transaction et ne peut compenser que 20 % de la consommation totale. Une transaction qui ne consomme que 30 000 gaz ne peut donc récupérer que 6 000 gaz de remboursement.

## Stockage transitoire (transient storage) : valable entre appels internes, vidé à la fin de la transaction

L'EIP-1153 a introduit TLOAD (0x5c) et TSTORE (0x5d) avec le hard fork Cancun (bloc 19 426 587, 13 mars 2024). L'adressage est le même que celui de SLOAD et SSTORE : une adresse de 32 octets pointe vers une valeur de 32 octets. Ces deux opérations ont un coût fixe de 100 gaz, sans distinction froid/chaud, sans remboursement, et sans provision à prévoir pour un futur effacement, puisque dans la spécification elles ne sont jamais écrites sur disque.

Face au stockage, les différences de comportement se concentrent en trois points. Sur l'échelle de temps, tout est remis à zéro à la fin de la transaction et aucune valeur n'est sérialisée dans une structure persistante. Sur la sémantique d'annulation, l'annulation d'un cadre annule les écritures de ce cadre, comme pour le stockage et contrairement à la mémoire (celle-ci étant intégralement jetée au retour ou à l'annulation du cadre). Sur les restrictions de contexte, TSTORE lève une exception dans un STATICCALL, alors que TLOAD est autorisé. Par ailleurs, l'EIP-1153 exempte explicitement TSTORE de la restriction que l'EIP-2200 impose à SSTORE : TSTORE n'exige pas un gasleft supérieur à 2 300 pour bénéficier de l'allocation d'appel.

Cette conception vise directement la « communication entre cadres ». Avant l'EIP-1153, transmettre un état temporaire entre contrats passait soit par les arguments et la valeur de retour d'un CALL (un contrat intermédiaire non fiable pouvant les altérer), soit par une écriture dans le stockage (coûteuse et dépendante du remboursement). Une fois le remboursement ramené au cinquième de gas_used par l'EIP-3529, les petites transactions ne récupèrent pratiquement plus leur coût. Une écriture de verrou 0 → 1 → 0 ajoute 19 900 gaz au compteur de remboursement ; en inversant la formule du plafond gas_used // 5, il faut environ 99 500 gaz sur l'ensemble de la transaction pour récupérer intégralement ce remboursement. Ce 99 500 est un gas_used total déduit de la formule de plafonnement de l'EIP-3529, et non une valeur donnée par le texte de l'EIP. Le corps de l'EIP-1153 avance de son côté une estimation de l'auteur : la transaction doit dépenser environ 80k de gaz en « autres opérations » pour obtenir le remboursement intégral d'un verrou de réentrance. Les deux chiffres n'ont pas la même base mais concordent : 99 500 moins la consommation brute d'environ 22 200 gaz du verrou lui-même donne environ 77 300, du même ordre de grandeur que l'estimation de 80k de l'auteur pour les « autres opérations ». Le stockage transitoire ne participe pas au compteur de remboursement et échappe donc à ce seuil.

Les verrous de réentrance, les autorisations valables le temps d'une transaction, la vérification d'équilibre des soldes en fin de rappel et la transmission de métadonnées d'un contrat proxy vers l'aval ont tous leur place ici. Depuis Solidity 0.8.28, les variables d'état de type valeur transient sont prises en charge, à condition de régler la version EVM sur cancun ; les types par référence (tableaux, mappings, structures) ainsi que les variables locales et les paramètres ne sont pas pris en charge et exigent de l'assemblage inline écrit à la main. Voici la forme minimale d'un verrou de réentrance :

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28; // La version EVM doit être cancun

contract TransientLock {
    uint256 transient entered;

    modifier nonReentrant() {
        require(entered == 0, "reentrant");
        entered = 1; // TSTORE, 100 gaz
        _;
        entered = 0; // Les appels ultérieurs de la même transaction liront 0
    }

    function withdraw() external nonReentrant {}
}
```

Si la version du compilateur ou la chaîne cible ne remplit pas la condition, on peut passer directement à l'assemblage, la sémantique étant strictement identique :

```solidity
assembly {
    tstore(0, 1)       // Écrit 1 dans le slot 0, 100 gaz
    let v := tload(0)  // Relecture, 100 gaz
}
```

## Quatre conséquences typiques d'un mauvais usage

Placer dans la mémoire ou le stockage transitoire un état qui doit être lu d'une transaction à l'autre donne le symptôme d'un « état perdu ». À la fin de la transaction la valeur est remise à zéro, l'appel suivant lit la valeur par défaut 0, le code ne signale aucune erreur et pourtant la logique métier est déjà fausse. Ce type de bug échappe souvent aux tests locaux, car ceux-ci s'exécutent généralement dans une même transaction ou un même environnement simulé.

Placer dans le stockage une valeur intermédiaire qui n'a de sens que dans une seule transaction donne un coût incontrôlé, et qui plus est dépendant de la taille de la transaction. Un verrou de réentrance ou une autorisation temporaire peuvent fonctionnellement s'implémenter avec le stockage, mais économiquement, il faut que la transaction soit assez grosse pour récupérer le remboursement ; plus la transaction est petite, plus le coût net réel se rapproche du coût brut.

Placer dans la mémoire une valeur intermédiaire destinée à être partagée entre appels donne un cadre enfant incapable de la lire. Un CALL change de cadre d'exécution et le cadre enfant reçoit une mémoire indépendante repartant de zéro ; ce que le cadre parent y a écrit n'apparaît pas chez l'enfant. Les seules voies de transmission légitimes sont le calldata et le returndata.

Utiliser le stockage transitoire à la place d'un mapping en mémoire donne un comportement inattendu en cas de réentrance. L'EIP-1153 le signale expressément dans ses considérations de sécurité : le stockage transitoire n'est pas jeté au retour d'un appel, et si on l'emploie comme un mapping de mémoire, un appel réentrant de la même transaction y verra les valeurs laissées par le tour précédent. Outre ce problème de sémantique, un coût unitaire de 100 gaz reste bien supérieur à celui d'une écriture en mémoire.

Un dernier mauvais usage, facile à négliger, consiste à oublier de remettre à zéro. Si, après avoir écrit 1 dans un verrou de réentrance, une branche rend la main prématurément sans réécrire 0, les appels ultérieurs de la même transaction restent bloqués définitivement par ce verrou. La spécification de l'EIP-1153 le dit sans détour : une valeur non nulle ne doit subsister que si ces slots seront effectivement lus par des appels ultérieurs de la transaction.

## Frontières et zones d'incertitude

Tous les chiffres de gaz du corps de l'article sont donnés avec leur base : prix des accès froids et chauds après Berlin (15 avril 2021, bloc 12 244 000), règles de remboursement après London (5 août 2021, bloc 12 965 000), stockage transitoire introduit par Cancun (13 mars 2024, bloc 19 426 587). L'EVM n'est pas une spécification figée : avant tout déploiement multichaîne, il faut vérifier une par une les EIP implémentées par la chaîne cible. Sur une chaîne compatible EVM arrêtée à London ou avant, TLOAD et TSTORE interrompent l'exécution en tant qu'opcodes invalides.

La prise en charge du stockage transitoire par Solidity a connu un défaut de compilateur depuis corrigé : de la version 0.8.28 à la 0.8.33, lorsque le pipeline IR (--via-ir) est activé, si une même unité de compilation nettoie une variable transitoire avec delete tout en nettoyant un stockage persistant du même type de valeur, les fonctions auxiliaires de nettoyage Yul générées sont réutilisées à cause de leur nom identique et émettent alors un opcode erroné (un SSTORE là où il fallait un TSTORE, ou l'inverse) ; le défaut est corrigé en 0.8.34. Il n'affecte que le pipeline IR, le pipeline legacy étant épargné. Les projets qui utilisent des variables transitoires avec via-ir doivent donc choisir une version de compilation en dehors de la plage concernée.

Deux points n'ont pas de calendrier public et ne peuvent être marqués que comme incertains. Les 100 gaz du stockage transitoire sont la valeur courante fixée par l'EIP-1153 ; aucun calendrier public ni aucune proposition déjà engagée dans le processus n'annonce un ajustement lors d'un futur hard fork. La mise en œuvre de l'arbre d'état (par exemple l'avancée de l'arbre de Verkle) modifiera la base de coût réelle des lectures de storage ; la motivation de l'EIP-2929 indique d'ailleurs qu'une refonte de la disposition de la base de données, permettant aux clients de lire directement le stockage, abaisserait encore le temps de traitement dans le pire des cas. Les constantes de tarification restent toutefois fixées par l'EIP-2929 et l'EIP-3529, et un écart peut subsister durablement entre les deux. Sur ces deux points, aucune conclusion quantitative citable n'est possible ; ils ne peuvent être conservés que comme jugements directionnels.

## Sources

- EIP-1153, Transient storage opcodes : https://eips.ethereum.org/EIPS/eip-1153
- EIP-2929, Gas cost increases for state access opcodes : https://eips.ethereum.org/EIPS/eip-2929
- EIP-3529, Reduction in refunds : https://eips.ethereum.org/EIPS/eip-3529
- EIP-2200, Structured Definitions for Net Gas Metering : https://eips.ethereum.org/EIPS/eip-2200
- Ethereum Yellow Paper, table des frais de l'annexe G et formule (328) de tarification de la mémoire : https://ethereum.github.io/yellowpaper/paper.pdf
- ethereum.org, référence des opcodes (TLOAD et TSTORE à 100 gaz chacun) : https://ethereum.org/en/developers/docs/evm/opcodes/
- Notes de version de Solidity 0.8.28 (prise en charge des variables d'état de type valeur transient) : https://www.soliditylang.org/blog/2024/10/09/solidity-0.8.28-release-announcement/
- Documentation Solidity, Transient Storage (version EVM exigée : cancun ; types par référence et variables locales pas encore pris en charge) : https://docs.soliditylang.org/en/latest/contracts.html#transient-storage
- Bug de collision des fonctions auxiliaires de nettoyage du stockage transitoire de Solidity (0.8.28 à 0.8.33, corrigé en 0.8.34) : https://www.soliditylang.org/blog/2026/02/18/transient-storage-clearing-helper-collision-bug/
- ethereum.org, historique des mises à niveau du réseau (hauteur de bloc et date de chaque fork) : https://ethereum.org/en/history/

## Pour aller plus loin

- [« Introduction au contrôle de concurrence optimiste (OCC) : des bases de données à l'exécution on-chain »](/fr/blog/optimistic-concurrency-control-intro)
- [« Glossaire des métriques de performance : TPS, BPS, latence de confirmation, finalité et taux de conflit »](/fr/blog/performance-metrics-glossary)
- [« Pourquoi un EVM monothread plafonne le TPS : histoire de la congestion et modèle d'exécution »](/fr/blog/evm-single-thread-bottleneck)

Cet article fait partie de la série sur les fondamentaux de l'EVM ; celle-ci comprend également 0.15 « Lecture approfondie de l'ABI : sélecteur, paramètres statiques et types dynamiques », qui traite de l'encodage et du décodage des données d'appel.
