# Transient storage (EIP-1153) : tstore et tload en pratique
Tout developpeur Solidity connait la douleur du SSTORE. Ecrire un seul slot de storage depuis zero peut couter 20 000 gas, et meme mettre a jour un slot non nul coute 5 000 gas. Or beaucoup de ces donnees n'ont pas besoin de survivre au-dela de la transaction en cours. Le transient storage, introduit par l'EIP-1153, vous donne un second magasin cle-valeur qui ne vit que le temps d'une transaction, puis disparait sans frais. Cet article explique les deux nouveaux opcodes, deroule un garde anti-reentrance concret, compare le gas et liste les pieges a comprendre avant de l'utiliser en production.
Ce qu'est reellement le transient storage
Le transient storage est un espace d'adressage separe a l'interieur de l'EVM, distinct du storage classique (persistant). Il est organise exactement comme le storage persistant : une correspondance entre des cles de 256 bits et des valeurs de 256 bits, propre a chaque adresse de contrat. La difference cruciale tient a sa duree de vie.
- Le storage persistant s'ecrit avec
SSTORE, se lit avecSLOAD, et survit d'une transaction a l'autre jusqu'a ce que vous l'ecrasiez.
- Le transient storage s'ecrit avec
TSTORE, se lit avecTLOAD, et est automatiquement remis a zero a la fin de chaque transaction.
Les deux opcodes ont ete ajoutes lors de la mise a niveau Cancun :
TLOAD(opcode0x5c) : depile une cle, empile la valeur stockee.
TSTORE(opcode0x5d) : depile une cle et une valeur, ecrit la valeur.
Les deux ont un cout fixe. Un TLOAD et un TSTORE coutent chacun 100 gas, comme un acces storage chaud (warm), sans surprise entre valeur nulle et non nulle, et sans remboursement. Cette previsibilite fait partie de ce qui rend le transient storage attractif.
Duree de vie a l'echelle de la transaction
C'est la propriete la plus importante, alors lisez-la deux fois. Le transient storage est efface a la fin de la transaction, pas a la fin d'un seul cadre d'appel (call frame).
Cela signifie qu'une valeur ecrite par TSTORE dans un appel externe reste lisible par TLOAD dans un appel ulterieur de la meme transaction. Si le contrat A appelle le contrat B qui rappelle A, le slot transient ecrit au debut du premier appel de A est toujours present quand l'appel reentrant arrive. C'est precisement ce comportement qui rend possible un garde anti-reentrance.
Gardez les frontieres en tete :
- Entre deux transactions distinctes : le transient storage repart toujours de zero.
- Au sein d'une transaction, a travers des appels imbriques : les valeurs persistent.
- En cas de revert : les modifications du transient storage sont annulees, exactement comme pour le storage persistant ; un sous-appel revert laisse donc le slot a la valeur qu'il avait avant ce sous-appel.
Construire un garde anti-reentrance
Le ReentrancyGuard classique d'OpenZeppelin utilise un slot de storage persistant bascule entre deux etats. Il paie un SSTORE a l'entree et un SSTORE a la sortie de chaque fonction protegee. Le transient storage fait le meme travail pour bien moins cher.
A partir de Solidity 0.8.24, vous pouvez utiliser directement l'assembleur inline. Les opcodes sont exposes sous les noms tstore et tload :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
abstract contract TransientReentrancyGuard {
// Un slot fixe et arbitraire pour le drapeau de verrou.
// keccak256("transient.reentrancy.guard") - 1 le tient a l'ecart
// de l'agencement de storage normal, meme si l'espace transient
// est separe.
bytes32 private constant LOCK_SLOT =
0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;
error ReentrantCall();
modifier nonReentrant() {
_checkAndLock();
_;
_unlock();
}
function _checkAndLock() private {
assembly {
if tload(LOCK_SLOT) {
// revert avec ReentrantCall()
mstore(0x00, 0xab143c06)
revert(0x1c, 0x04)
}
tstore(LOCK_SLOT, 1)
}
}
function _unlock() private {
assembly {
tstore(LOCK_SLOT, 0)
}
}
}
Son usage est identique a la version persistante :
contract Vault is TransientReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "insufficient");
balances[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}
}
Comme le verrou est pose avec tstore et que la duree de vie a l'echelle de la transaction le maintient a travers le call reentrant, un receveur malveillant qui rappelle withdraw tombe sur tload(LOCK_SLOT) != 0 et revert. Quand l'appel externe se termine, _unlock efface le slot, et le nettoyage automatique en fin de transaction sert de filet de securite si vous oubliez.
Syntaxe de plus haut niveau
Si vous utilisez un compilateur recent, vous pouvez declarer une variable d'etat en transient et vous passer entierement de l'assembleur :
pragma solidity ^0.8.28;
abstract contract TransientGuard {
bool transient locked;
error ReentrantCall();
modifier nonReentrant() {
if (locked) revert ReentrantCall();
locked = true;
_;
locked = false;
}
}
L'emplacement de donnees transient se compile vers les memes opcodes TSTORE et TLOAD, mais le compilateur gere le slot pour vous. Lire et ecrire une variable transient est typee et bien moins source d'erreurs que l'assembleur ecrit a la main.
Economies de gas face a SSTORE
Le chiffre principal est simple. Un garde adosse au storage persistant paie, dans le cas chaud habituel, environ 5 000 gas pour poser le verrou et un peu moins pour le reinitialiser (un remboursement de storage s'applique a la reinitialisation, mais les remboursements sont plafonnes par transaction et ne recuperent jamais entierement le cout). Un garde transient paie un forfait de 100 gas pour tstore et 100 gas pour tload.
| Operation | Storage persistant | Transient storage |
| --- | --- | --- |
| Ecrire un slot vierge | 20 000 gas | 100 gas |
| Mettre a jour un slot chaud | 5 000 gas | 100 gas |
| Lire un slot chaud | 100 gas | 100 gas |
| Nettoyage en fin de tx | reinit manuelle, remboursement partiel | automatique, gratuit |
Pour un garde anti-reentrance, l'economie pratique est de l'ordre de quelques milliers de gas par appel protege. Le gain est encore plus net pour les patterns qui detournaient auparavant le storage persistant en brouillon a l'interieur d'une seule transaction : flash-accounting dans un AMM, contexte de callback pour les flash loans, ou transmission de donnees entre appels sans toucher au calldata.
Cas d'usage reels au-dela de la reentrance
- Flash accounting : un DEX peut enregistrer les deltas de tokens en transient storage pendant un swap en plusieurs etapes et exiger qu'ils s'annulent (net a zero) avant la fin de la transaction, evitant des
SSTORErepetes.
- Contexte de callback reutilisable : un fournisseur de flash loan peut ranger l'emprunteur et le remboursement attendu dans des slots transient, puis les relire dans le callback sans tout encoder dans le
calldata.
- Caches par transaction : toute valeur couteuse a recalculer et utile seulement pendant une transaction peut vivre en transient storage plutot que dans une memoire qui ne traverse pas les cadres d'appel.
Pieges a respecter
TSTORE pour survivre a un sous-appel revert.SSTORE moins cher, c'est un outil different.cancun ou ulterieure) avant de deployer.Pratiquez par vous-meme
Le moyen le plus rapide d'assimiler le transient storage est d'ecrire les deux versions du garde, de les deployer et de regarder les rapports de gas diverger. Vous pouvez faire exactement cela, avec un editeur interactif et des comparaisons de gas, dans les exercices de smart contracts sur app.solingo-blockchain.xyz. Essayez de remplacer un garde persistant par un garde transient, puis ajoutez un receveur malveillant et confirmez que l'appel reentrant revert toujours. Voir le verrou tenir a travers le cadre reentrant, c'est l'instant ou la regle de duree de vie devient evidente.