Tutoriel·8 min de lecture·Par Solingo

Transient Storage (EIP-1153): tstore and tload in Practice

Transient storage gives the EVM a cheap, transaction-scoped memory that resets automatically. Learn how TSTORE and TLOAD work, and build a reentrancy guard that costs a fraction of the storage-based one.

# Transient Storage (EIP-1153): tstore and tload in Practice

Every Solidity developer knows the pain of SSTORE. Writing a single storage slot from zero can cost 20,000 gas, and even updating a non-zero slot costs 5,000 gas. A lot of that data does not need to survive past the current transaction. Transient storage, introduced by EIP-1153, gives you a second key-value store that lives only for the duration of one transaction and then disappears at no cost. This article explains the two new opcodes, walks through a concrete reentrancy guard, compares the gas, and lists the caveats you must understand before shipping it.

What transient storage actually is

Transient storage is a separate address space inside the EVM, distinct from regular (persistent) storage. It is organized exactly like persistent storage: a mapping from 256-bit keys to 256-bit values, scoped per contract address. The crucial difference is its lifetime.

  • Persistent storage is written with SSTORE, read with SLOAD, and survives across transactions until you overwrite it.
  • Transient storage is written with TSTORE, read with TLOAD, and is automatically wiped to zero at the end of every transaction.

The two opcodes were added in the Cancun upgrade:

  • TLOAD (opcode 0x5c): pops a key from the stack, pushes the stored value.
  • TSTORE (opcode 0x5d): pops a key and a value from the stack, writes the value.

Both are flat-priced. A TLOAD and a TSTORE each cost 100 gas, the same as a warm storage access, with no surprises around zero versus non-zero values or refunds. This predictability is part of what makes transient storage attractive.

Lifetime within a transaction

This is the single most important property, so read it twice. Transient storage is cleared at the end of the transaction, not at the end of a single call frame.

That means a value you TSTORE in one external call is still readable by TLOAD in a later call within the same transaction. If contract A calls contract B which calls back into A, the transient slot written at the start of A's first call is still set when the reentrant call arrives. That is precisely the behavior that makes a reentrancy guard possible.

Keep in mind the boundaries:

  • Across two separate transactions: transient storage always starts at zero.
  • Within one transaction across nested calls: values persist.
  • On revert: changes to transient storage are rolled back, just like persistent storage, so a reverted sub-call leaves the slot at whatever value it held before that sub-call.

Building a reentrancy guard

The classic OpenZeppelin ReentrancyGuard uses a persistent storage slot toggled between two states. It pays SSTORE on entry and SSTORE on exit of every protected function. Transient storage does the same job for far less.

In Solidity 0.8.24 and later you can use inline assembly directly. The opcodes are exposed as tstore and tload:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

abstract contract TransientReentrancyGuard {

// A fixed, arbitrary slot for the lock flag.

// keccak256("transient.reentrancy.guard") - 1 keeps it clear of

// normal storage layout, even though transient space is separate.

bytes32 private constant LOCK_SLOT =

0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00;

error ReentrantCall();

modifier nonReentrant() {

_checkAndLock();

_;

_unlock();

}

function _checkAndLock() private {

assembly {

if tload(LOCK_SLOT) {

// revert with ReentrantCall()

mstore(0x00, 0xab143c06)

revert(0x1c, 0x04)

}

tstore(LOCK_SLOT, 1)

}

}

function _unlock() private {

assembly {

tstore(LOCK_SLOT, 0)

}

}

}

Using it is identical to the persistent version:

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");

}

}

Because the lock is set with tstore and the transaction-scoped lifetime keeps it set across the reentrant call, a malicious receiver calling back into withdraw hits tload(LOCK_SLOT) != 0 and reverts. When the outer call finishes, _unlock clears the slot, and the automatic end-of-transaction wipe is a safety net even if you forget.

Higher-level syntax

If you are on a recent compiler, you can declare a state variable as transient and skip the assembly entirely:

pragma solidity ^0.8.28;

abstract contract TransientGuard {

bool transient locked;

error ReentrantCall();

modifier nonReentrant() {

if (locked) revert ReentrantCall();

locked = true;

_;

locked = false;

}

}

The transient data location compiles down to the same TSTORE and TLOAD opcodes, but the compiler manages the slot for you. Reading and writing a transient variable is type-safe and far less error prone than hand-written assembly.

Gas savings versus SSTORE

The headline number is simple. A guard backed by persistent storage pays, in the common warm case, around 5,000 gas to set the lock and a smaller amount to reset it (a storage refund applies on the reset, but refunds are capped per transaction and never fully recover the cost). A transient guard pays a flat 100 gas for tstore and 100 gas for tload.

| Operation | Persistent storage | Transient storage |

| --- | --- | --- |

| Write a fresh slot | 20,000 gas | 100 gas |

| Update a warm slot | 5,000 gas | 100 gas |

| Read a warm slot | 100 gas | 100 gas |

| End-of-tx cleanup | manual reset, partial refund | automatic, free |

For a reentrancy guard the practical saving is on the order of a few thousand gas per protected call. The win is even larger for patterns that previously abused persistent storage as a scratchpad inside a single transaction, such as flash-accounting in an AMM, callback context for flash loans, or passing data between calls without touching calldata.

Real use cases beyond reentrancy

  • Flash accounting: a DEX can record token deltas in transient storage during a multi-step swap and require them to net to zero before the transaction ends, avoiding repeated SSTORE writes.
  • Reusable callback context: a flash loan provider can stash the borrower and expected repayment in transient slots, then read them back in the callback without encoding everything into calldata.
  • Per-transaction caches: any value that is expensive to recompute and only needed within one transaction can live in transient storage instead of memory that does not cross call frames.

Caveats you must respect

  • Composability risk: because transient storage persists across calls within a transaction, a contract that does not clear its slots can leak state into later calls in the same transaction. Always reset what you set, even though the end-of-transaction wipe exists.
  • Reverts roll back transient writes: this is usually what you want, but do not rely on a TSTORE surviving a reverted sub-call.
  • Slot collisions: in assembly you choose raw slots. Use a hashed constant, as in the example, to avoid colliding with another library that also uses transient storage in the same contract.
  • No cross-transaction memory: never use transient storage for data that must survive the transaction. It is not a cheaper SSTORE, it is a different tool.
  • Chain support: the opcodes require the Cancun upgrade. Confirm your target chain and your local toolchain (EVM version set to cancun or later) before deploying.
  • Practice it hands-on

    The fastest way to internalize transient storage is to write both versions of the guard, deploy them, and watch the gas reports diverge. You can do exactly that, with an interactive editor and gas comparisons, in the smart contract exercises on app.solingo-blockchain.xyz. Try replacing a persistent guard with a transient one, then add a malicious receiver and confirm the reentrant call still reverts. Seeing the lock hold across the reentrant frame is the moment the lifetime rule clicks.

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement