Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x5D |
| Mnemonic | TSTORE |
| Gas | 100 |
| Stack Input | key (32-byte storage slot), val (32-byte value) |
| Stack Output | (none) |
| Behavior | Writes val to the transient storage slot identified by key in the current contract’s transient storage space. Transient storage was introduced in EIP-1153 (Cancun hard fork, March 2024). Values persist across all calls within the same transaction but are unconditionally cleared when the transaction ends. TSTORE is prohibited inside a STATICCALL context and will cause the call to revert. Within a DELEGATECALL, TSTORE writes to the caller’s transient storage, not the delegate’s. Transient storage is scoped per contract address — each contract has its own independent key space. |
Threat Surface
TSTORE enables cheap, transaction-scoped mutable state at 100 gas per write (vs. 5,000-22,100 gas for SSTORE). Its primary intended use case is reentrancy locks: a contract writes a lock flag with TSTORE on entry, checks it with TLOAD, and the lock automatically clears when the transaction ends — no gas-expensive storage cleanup needed. Uniswap V4 adopted this pattern, saving ~24,000 gas per swap compared to SSTORE-based locks.
The threat surface emerges from four properties that distinguish transient storage from persistent storage:
-
Transient storage persists across ALL calls within a transaction, not just the current call frame. When contract A calls contract B, then calls contract C, all three share the same transaction context. If A stores a value with TSTORE, that value is readable (via TLOAD) by A in any subsequent call frame within the same transaction. More critically, if A is re-entered through a different external call path (e.g., A → B → A via a different function), the re-entering call sees the same transient storage state. This creates cross-call-frame side channels that don’t exist with memory (call-local) or persistent storage (global but expensive to manipulate).
-
TSTORE has no minimum gas requirement, unlike SSTORE. EIP-2200 enforces a 2,300-gas minimum for SSTORE to prevent state changes during low-gas transfers (the 2,300-gas stipend from
transfer()andsend()). TSTORE has no such floor. At 100 gas, TSTORE can execute within the 2,300-gas stipend, meaning areceive()orfallback()function invoked viatransfer()can modify transient storage. This breaks a long-standing Ethereum security assumption: thattransfer()prevents the recipient from making any state changes. -
Transient storage IS reverted within a call frame that reverts, but persists if a sub-call succeeds. If contract A calls contract B and B reverts, any TSTORE operations B performed are rolled back (same as SSTORE). But if B succeeds and then A continues execution, B’s transient writes persist for the rest of the transaction. Developers who assume “transient = ephemeral within this call” will write bugs. The lifetime is the entire transaction, modulo revert semantics.
-
DELEGATECALL shares the caller’s transient storage. When A delegates to implementation contract B, B’s TSTORE/TLOAD operations act on A’s transient storage space. This is consistent with how DELEGATECALL shares persistent storage (SSTORE/SLOAD), but introduces the same class of slot collision risks — and developers building proxy/diamond patterns with transient storage must be careful about slot namespace conflicts between the proxy and implementation.
Smart Contract Threats
T1: Reentrancy Guard Bypass via Alternative Entry Points (High)
TSTORE-based reentrancy locks are the canonical use case for transient storage. The standard pattern stores a lock flag at a fixed slot on function entry and checks it at the start of protected functions. The vulnerability arises when a contract has multiple external entry points that share transient storage but don’t all check the same lock:
-
Multiple-function reentrancy. Contract A has functions
deposit()andwithdraw(). Both use a TSTORE-based reentrancy guard on the same slot. An attacker callsdeposit(), which makes an external call. During that external call, the attacker re-enters viawithdraw(). Ifwithdraw()checks the lock, the reentrancy is blocked. But if the developer added the guard only todeposit()(or uses a different slot forwithdraw()), the re-entrancy succeeds. -
Cross-function transient state leakage. A contract uses transient storage slot 0 for a reentrancy lock and slot 1 for intermediate accounting (e.g., tracking a flash loan amount). An attacker re-enters through a different function that reads slot 1 but doesn’t check slot 0. The attacker can observe or manipulate the intermediate accounting state during reentrancy.
-
Upgradeable contracts with inconsistent guards. In proxy patterns, the implementation contract may be upgraded to add new functions. If new functions don’t apply the same transient storage reentrancy guard, they become reentrant entry points into a contract that was previously reentrancy-safe.
Why it matters: Transient storage reentrancy locks are cheaper and more convenient than SSTORE-based ones, encouraging their adoption. But they only work if every external entry point consistently checks the lock. The cheapness of TSTORE may encourage developers to use transient storage for multiple purposes beyond locking, increasing the surface for slot reuse bugs.
T2: Transient Storage Slot Collision — The SIR.trading Pattern (Critical)
Reusing the same transient storage slot for different purposes within a transaction is the single most dangerous TSTORE pattern, and it has already caused a real-world exploit. The vulnerability occurs when:
-
A slot stores one value (e.g., an address) at one point in the transaction, then gets overwritten with a different type of value (e.g., an amount). A subsequent read of the slot interprets the new value under the old assumption. In the SIR.trading exploit (March 2025), slot 0x1 was used to store a Uniswap V3 pool address for callback verification. Later in the same transaction, the same slot was overwritten with a return amount. The attacker brute-forced a CREATE2 address whose numeric value matched a crafted amount, allowing them to pass the pool address verification with an attacker-controlled contract.
-
Type confusion between address and uint256. Transient storage slots store raw 32-byte words. When the same slot is read as an address in one context and as a uint256 in another, an attacker who controls one interpretation can craft a value that is valid under both interpretations. Addresses are 20 bytes (160 bits), leaving 12 zero bytes in the high-order positions. A uint256 amount that happens to have zeros in the high 12 bytes is also a valid address.
-
Multi-step operations with callbacks. Protocols that perform multi-step operations (swap → callback → verify) and store intermediate state in transient storage are especially vulnerable. The callback gives external code a chance to execute between the store and the verification read.
Why it matters: This is not a theoretical risk. The SIR.trading exploit cost $355K and was the first real-world exploitation of transient storage semantics. Any protocol that reuses transient storage slots across different phases of a transaction is exposed.
T3: Low-Gas Reentrancy via TSTORE in 2300-Gas Stipend (High)
ChainSecurity identified that TSTORE’s lack of a minimum gas requirement creates a novel reentrancy vector. The attack works against contracts that:
-
Use
transfer()orsend()to prevent reentrancy. These Solidity functions forward only 2,300 gas to the recipient’sreceive()function. Historically, this was insufficient for any storage write (SSTORE requires > 2,300 gas per EIP-2200). But TSTORE costs 100 gas, and TLOAD costs 100 gas, meaning areceive()function can perform ~20 transient storage operations within the 2,300-gas stipend. -
Rely on transient storage locks that the recipient can manipulate. If a contract sends ETH via
transfer()to an attacker contract, the attacker’sreceive()can call TSTORE to flip a transient storage flag in its own transient storage. This is only directly dangerous if the sending contract subsequently checks the attacker’s transient storage (unlikely). The real danger is in callback-based protocols where the 2,300-gas call is part of a larger interaction pattern. -
Concrete attack (from ChainSecurity’s PoC). An ETH vault contract uses TSTORE for its reentrancy lock. A user withdraws ETH via
transfer(). The recipient’sreceive()function uses its 2,300 gas to call back into the vault through a function that doesn’t check the reentrancy lock, or to modify transient storage that the vault reads later. With SSTORE, this would be impossible; with TSTORE at 100 gas, it fits within the stipend.
Why it matters: The security invariant “transfer() prevents reentrancy” has been relied upon for 8+ years of Ethereum development. TSTORE breaks this invariant. Contracts written before Cancun that rely on gas-limited calls for reentrancy safety are not affected (they don’t use transient storage), but new contracts that mix transfer() with transient storage patterns are vulnerable.
T4: DELEGATECALL Transient Storage Context Sharing (Medium)
When contract A uses DELEGATECALL to invoke code in contract B, B’s TSTORE/TLOAD operations read and write A’s transient storage. This mirrors DELEGATECALL’s behavior for persistent storage but creates new risks:
-
Slot namespace collisions in proxy patterns. A proxy contract uses transient slot 0 for a reentrancy lock. The implementation contract, unaware of the proxy’s convention, uses transient slot 0 for intermediate accounting. When the proxy delegates to the implementation, the implementation’s TSTORE to slot 0 overwrites the proxy’s reentrancy lock, potentially disabling reentrancy protection for the rest of the transaction.
-
Diamond pattern (EIP-2535) conflicts. In diamond proxies with multiple facets, each facet may independently decide to use transient storage. Without a namespace convention (analogous to EIP-7201 for persistent storage), facets will collide on commonly-used slot indices (0, 1, 2, …).
-
Malicious implementation upgrade. If an upgradeable proxy’s implementation is compromised, the attacker can use TSTORE to write arbitrary values into the proxy’s transient storage. While transient storage clears after the transaction, this enables same-transaction exploits where the attacker manipulates transient state that other functions in the proxy depend on.
Why it matters: Proxy patterns are ubiquitous in production DeFi. Developers accustomed to persistent storage namespace conventions may overlook transient storage namespace isolation, especially since transient storage is newer and has less tooling for namespace management.
T5: Flash Loan + Transient Storage Interactions (Medium)
Flash loans execute an entire borrow-use-repay cycle within a single transaction. Since transient storage persists for the full transaction, it creates interactions:
-
Transient state survives across flash loan callbacks. A protocol stores a temporary flag or value in transient storage before a flash loan callback. The flash loan callback invokes external code (the borrower’s logic). If the borrower’s logic calls back into the original protocol, the transient state is still present and may influence the protocol’s behavior in unintended ways.
-
Flash loan-enabled reentrancy via transient state. A lending protocol uses transient storage to track “active borrowers” during a transaction. An attacker takes a flash loan, which sets a transient flag. During the callback, the attacker calls another function that reads the transient flag and grants privileges based on “active borrower” status. The transient flag acts as an unintended credential.
-
Composability assumptions. Protocols that use transient storage for single-call bookkeeping may not account for the fact that flash loans allow arbitrary external code execution during the transaction. Any transient state written before the callback is visible and mutable by the callback’s code (if it calls back into the original contract).
Why it matters: Flash loans are the standard mechanism for capital-efficient attacks. Transient storage’s transaction-scoped lifetime aligns exactly with flash loan scope, making it a natural vector for same-transaction state manipulation.
Protocol-Level Threats
P1: STATICCALL Prohibition and State Mutability Detection (Low)
TSTORE is prohibited in a STATICCALL context per EIP-1153. If a contract executes TSTORE during a STATICCALL, the call reverts. This is consistent with SSTORE’s behavior in STATICCALL but has implications:
-
Correct enforcement. The EVM correctly prevents transient storage writes in read-only contexts. There is no bypass risk here; the prohibition is enforced at the opcode level by the EVM execution engine.
-
ABI/interface ambiguity. Solidity
viewandpurefunctions are compiled to use STATICCALL when called externally. If aviewfunction internally calls a library that uses TSTORE (e.g., via inline assembly), the call will revert at runtime despite compiling without warnings. This is a developer-experience issue rather than a security vulnerability.
P2: Cancun Hard Fork Compatibility and Client Implementation (Low)
EIP-1153 was activated in the Cancun hard fork (March 13, 2024). All major Ethereum clients (Geth, Nethermind, Besu, Erigon) implement TSTORE:
-
Pre-Cancun contracts are unaffected. Contracts deployed before Cancun that don’t use opcode 0x5D are safe. The opcode was previously INVALID, so no legacy contract can accidentally invoke TSTORE.
-
L2 support varies. OP Stack chains (Optimism, Base) and Arbitrum adopted Cancun opcodes on their own schedules. Contracts using TSTORE must verify that the target L2 supports EIP-1153. Deploying a TSTORE-using contract to an L2 that hasn’t upgraded results in INVALID opcode execution.
P3: Gas Repricing Risk (Low)
TSTORE’s 100-gas cost was chosen to be comparable to warm SLOAD (100 gas) since transient storage operations don’t touch disk. If future EIPs reprice TSTORE (upward for DoS prevention or downward for further optimization), contracts that rely on precise gas calculations around TSTORE may break:
-
Reentrancy locks and gas forwarding. Contracts that calculate exact gas amounts for sub-calls, accounting for the 100-gas TSTORE cost, will malfunction if the cost changes.
-
2,300-gas stipend interactions. The low-gas reentrancy vector (T3) exists precisely because TSTORE fits within 2,300 gas. A repricing to > 2,300 gas would eliminate T3 but break contracts relying on TSTORE within stipend-limited calls.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
| TSTORE in a reverting call frame | Transient storage writes within a call frame that reverts are rolled back, identical to SSTORE revert semantics. | Developers who assume “transient storage is never reverted” will write bugs. The revert behavior is per-call-frame, not per-transaction. |
| TSTORE in STATICCALL | Execution immediately reverts. TSTORE is a state-mutating operation, prohibited in read-only contexts. | Correct enforcement. No security issue, but calling a TSTORE-using library from a view function will fail at runtime. |
| Transient storage persistence within a transaction | Values written by TSTORE persist across all subsequent CALL, DELEGATECALL, and CALLCODE frames within the same transaction. | Cross-call-frame state leakage. Any function invoked later in the transaction can read values stored earlier (via TLOAD). This is the fundamental enabler for T1, T2, and T5 threats. |
| Transient storage at transaction boundary | All transient storage is unconditionally cleared when the transaction ends. No values carry over to the next transaction. | No persistent pollution risk. But within a single transaction, transient storage is fully mutable state, so all same-transaction attacks apply. |
| DELEGATECALL context | TSTORE writes to the calling contract’s transient storage, not the delegate target’s. Mirrors SSTORE behavior in DELEGATECALL. | Slot collisions between proxy and implementation (T4). Developers must coordinate transient storage slot usage across proxy and implementation contracts. |
| Nested calls: A → B → A (reentrancy) | When A is re-entered, A’s TSTORE values from the outer call are still present and modifiable. | This is intentional and enables reentrancy locks. But it also means re-entrant code sees the “dirty” transient state from the outer call. |
| Multiple contracts in same transaction | Each contract has its own transient storage namespace. Contract A cannot directly read or write contract B’s transient storage (except via DELEGATECALL). | Cross-contract transient storage pollution is not possible at the EVM level. But if A calls B and B returns values derived from its transient storage, information can leak indirectly. |
| TSTORE with key = 0 | Slot 0 is a valid transient storage slot, commonly used for reentrancy locks. No special behavior. | No edge case, but collisions on slot 0 are the most common namespace conflict. |
| Gas: TSTORE with < 2,300 gas remaining | TSTORE succeeds (100 gas cost is satisfied). Unlike SSTORE, there is no EIP-2200 minimum-gas check. | Enables low-gas reentrancy (T3). Contracts relying on transfer() for reentrancy safety may be vulnerable. |
| TSTORE to same slot multiple times | Each write overwrites the previous value. The last write before a TLOAD determines the read value. No gas refund for clearing (unlike SSTORE). | No gas refund incentive to clear transient storage. Developers who expect refunds (as with SSTORE 0-value writes) will miscalculate gas costs. |
Real-World Exploits
Exploit 1: SIR.trading (Synthetics Implemented Right) — Transient Storage Slot Collision ($355K, March 2025)
Root cause: The SIR.trading Vault contract reused transient storage slot 0x1 for two different purposes within the same transaction: first to store a Uniswap V3 pool address for callback verification, then to store a swap return amount. An attacker exploited this overwrite to bypass access control.
Details: On March 30, 2025, an attacker exploited SIR.trading’s Vault contract, draining approximately $355,000 (17,814 USDC, 1.4 WBTC, 119.8 WETH). The attack targeted the Uniswap V3 swap callback verification mechanism:
- The Vault stored the address of the legitimate Uniswap V3 pool in transient slot 0x1 before initiating a swap.
- During the swap, the Vault’s callback handler (
uniswapV3SwapCallback) loaded slot 0x1 to verify that the caller was the expected pool. - However, the Vault also stored the swap’s return amount in the same transient slot 0x1, overwriting the pool address.
- The attacker brute-forced a vanity contract address using CREATE2:
0x00000000001271551295307acc16ba1e7e0d4281. This address, when interpreted as a uint256, matched a specific amount value (95759995883742311247042417521410689). - The attacker deployed dummy ERC-20 tokens, created a controlled Uniswap V3 pool, and triggered a swap that wrote the attacker-controlled amount to slot 0x1.
- The attacker then called
uniswapV3SwapCallbackagain. The Vault loaded slot 0x1 (now containing the amount that matched the attacker’s vanity address) and verifiedmsg.senderagainst it. The check passed because the attacker called from the vanity address. - The attacker drained the vault’s USDC, WETH, and WBTC.
TSTORE’s role: This exploit is fundamentally a transient storage slot collision. The Vault assumed slot 0x1 would retain the pool address throughout the callback flow, but its own code overwrote the slot with an amount. Transient storage’s persistence across the full transaction meant the overwritten value was available to the attacker in a subsequent call. With persistent storage (SSTORE), the same bug could theoretically occur, but the 22,100-gas cost of storage writes makes such patterns less likely (developers are more deliberate about storage slot usage).
Impact: $355,000 stolen. First real-world exploitation of EIP-1153 transient storage semantics. SIR.trading had completed one security audit before launch but the auditors did not flag the slot reuse pattern.
References:
- PANews: Deadly Remains — $300,000 On-Chain Heist Triggered by Transient Storage
- Verichains: EIP-1153 Transient Storage — Save Gas, Lose Bag
- SolidityScan: SIR Hack Analysis
- DeFiHackLabs: SIR Exploit ~355K Loss
Exploit 2: ChainSecurity — TSTORE Low-Gas Reentrancy (Disclosure, January 2024)
Root cause: TSTORE has no minimum gas requirement, unlike SSTORE which requires > 2,300 gas per EIP-2200. This allows transient storage operations to execute within the 2,300-gas stipend forwarded by Solidity’s transfer() and Vyper’s send(), breaking a long-standing reentrancy prevention assumption.
Details: In January 2024 (before the Cancun hard fork went live), ChainSecurity published a detailed disclosure demonstrating that TSTORE breaks the gas-based reentrancy barrier:
- Solidity’s
address.transfer()andaddress.send()forward exactly 2,300 gas to the recipient. Since EIP-2200 (Constantinople, 2019), this has been insufficient for any SSTORE operation, makingtransfer()a de facto reentrancy guard. - TSTORE costs 100 gas. Within 2,300 gas, a recipient’s
receive()function can execute approximately 20 TSTORE operations. - ChainSecurity demonstrated three proof-of-concept contracts: (a) a simple reentrancy via TSTORE lock manipulation, (b) an ETH locker contract where the attacker’s
receive()flips a transient lock during atransfer(), and (c) a modified WETH contract where TSTORE-based accounting is manipulable during low-gas calls. - The core issue: contracts that mix TSTORE-based state management with
transfer()-based ETH sends allow recipients to modify transient state that the sending contract reads after the transfer returns.
TSTORE’s role: The vulnerability exists solely because of the gas cost asymmetry between TSTORE (100 gas, no minimum) and SSTORE (5,000+ gas, 2,300-gas minimum). If TSTORE had the same minimum gas check as SSTORE, the attack would be impossible within the 2,300-gas stipend.
Impact: No funds lost (pre-emptive disclosure before Cancun activation). However, this disclosure influenced the Solidity compiler to emit warnings when TSTORE is used in inline assembly (Solidity 0.8.24+). The Solidity team explicitly cautioned developers against advanced transient storage use cases.
References:
- ChainSecurity: TSTORE Low Gas Reentrancy
- GitHub: ChainSecurity/TSTORE-Low-Gas-Reentrancy (PoC code)
- Solidity Blog: Transient Storage Opcodes in Solidity 0.8.24
Attack Scenarios
Scenario A: Transient Storage Slot Collision (SIR.trading Pattern)
contract VulnerableVault {
// Uses transient slot 0x1 for pool address verification AND amount storage
function swap(address pool, uint256 amount) external {
// Store pool address for callback verification
assembly { tstore(0x1, pool) }
IUniswapV3Pool(pool).swap(/* params */);
}
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata data
) external {
// VULNERABLE: Reads slot 0x1 expecting the pool address,
// but _processAmount() may have overwritten it with an amount value
address expectedPool;
assembly { expectedPool := tload(0x1) }
require(msg.sender == expectedPool, "unauthorized");
// Transfer tokens to the caller (attacker if slot was overwritten)
IERC20(token).transfer(msg.sender, uint256(amount0Delta));
}
function _processAmount(uint256 amount) internal {
// DANGEROUS: Overwrites the same transient slot with an amount
assembly { tstore(0x1, amount) }
}
// Attack: Attacker brute-forces a CREATE2 address whose numeric value
// matches a crafted amount. When _processAmount writes that amount to
// slot 0x1, uniswapV3SwapCallback reads it as an address and the
// attacker's contract passes the msg.sender == expectedPool check.
}Scenario B: Low-Gas Reentrancy via TSTORE
contract VulnerableETHVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
// TSTORE-based reentrancy lock
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
// VULNERABLE: transfer() forwards 2300 gas.
// With SSTORE, 2300 gas is insufficient for state changes.
// With TSTORE, the attacker's receive() can execute TSTORE
// (100 gas) to clear the lock before withdraw() continues.
payable(msg.sender).transfer(amount);
// Clear reentrancy lock
assembly { tstore(0, 0) }
}
}
contract Attacker {
VulnerableETHVault vault;
receive() external payable {
// Within the 2300 gas stipend from transfer():
// TSTORE costs 100 gas -- this is possible!
// NOTE: The attacker can only modify their OWN transient storage,
// not the vault's. The real danger is in more complex callback
// scenarios where the attacker's transient state influences
// the vault's subsequent logic.
}
}Scenario C: Reentrancy Guard Bypass via Alternative Entry Point
contract VulnerableLending {
// Reentrancy lock in transient slot 0
modifier nonReentrant() {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
_;
assembly { tstore(0, 0) }
}
// Protected function
function borrow(uint256 amount) external nonReentrant {
// ... transfer tokens to msg.sender ...
IERC20(token).transfer(msg.sender, amount);
// External callback to borrower
IBorrower(msg.sender).onBorrow(amount);
// Verify collateral is sufficient after callback
require(isCollateralized(msg.sender), "undercollateralized");
}
// VULNERABLE: This function lacks the nonReentrant modifier
// but modifies critical state that borrow() depends on
function updateCollateralPrice(address oracle) external {
// Attacker calls this during the onBorrow callback
// to manipulate the price used in isCollateralized()
uint256 price = IOracle(oracle).getPrice();
collateralPrice = price;
}
}
// Attack: Attacker calls borrow() -> receives callback via onBorrow() ->
// calls updateCollateralPrice() with a manipulated oracle ->
// isCollateralized() passes with the inflated price.
// The reentrancy lock on borrow() doesn't prevent entry to
// updateCollateralPrice() because it lacks the guard.Scenario D: DELEGATECALL Transient Storage Slot Collision
contract Proxy {
// Uses transient slot 0 for reentrancy lock
modifier nonReentrant() {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
_;
assembly { tstore(0, 0) }
}
function execute(address impl, bytes calldata data)
external nonReentrant
{
// DELEGATECALL: impl's TSTORE writes to Proxy's transient storage
(bool ok,) = impl.delegatecall(data);
require(ok);
// After delegatecall, check the lock is still set
// VULNERABLE: If impl wrote to transient slot 0 (clearing or
// changing the lock), the reentrancy protection is broken
assembly { tstore(0, 0) }
}
}
contract MaliciousImpl {
function attack() external {
// Executing via DELEGATECALL, this writes to Proxy's transient storage
// Clears the reentrancy lock
assembly { tstore(0, 0) }
// Now the proxy's nonReentrant guard is disabled for this tx
}
}Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Reentrancy guard bypass via alternative entry points | Apply reentrancy guard to ALL external/public functions that modify state | Use OpenZeppelin’s ReentrancyGuardTransient (v5.1+) as a base contract; the modifier applies a single transient lock across all guarded functions. |
| T2: Transient storage slot collision | Never reuse transient storage slots for different purposes within a transaction | Assign unique, well-documented slot constants (use keccak256("namespace.purpose") for slot derivation, analogous to EIP-7201 for persistent storage). Clear slots explicitly after use with tstore(slot, 0). |
| T2: Type confusion between address and uint256 | Validate types when reading transient storage | When a slot stores an address, mask the value: address(uint160(tload(slot))). Verify against known values, not just msg.sender equality. |
| T3: Low-gas reentrancy via 2300-gas stipend | Avoid mixing transfer()/send() with TSTORE-based patterns | Use call{value: amount}("") with explicit reentrancy guards instead of relying on the 2,300-gas stipend for reentrancy safety. |
| T3: TSTORE in gas-limited contexts | Add explicit gas checks before critical TSTORE operations | require(gasleft() > MIN_GAS_FOR_OPERATION) before TSTORE-dependent logic. |
| T4: DELEGATECALL slot collisions | Namespace transient storage slots per contract/facet | Use keccak256(abi.encode("contract.name", purpose)) as slot keys in proxy/diamond patterns. Document all transient slot allocations. |
| T5: Flash loan + transient storage | Clear all transient state before external callbacks | tstore(slot, 0) for all sensitive slots before invoking flash loan callbacks or any external code. Re-set after callback returns. |
| General: Slot hygiene | Always clear transient storage after use | Even though transient storage clears at transaction end, clearing slots after use prevents downstream functions in the same transaction from reading stale values. |
| General: Compiler warnings | Use Solidity >= 0.8.28 for native transient keyword | Native transient storage support provides type safety and compiler checks. Avoid raw tstore/tload in inline assembly when possible. |
Compiler/EIP-Based Protections
- Solidity 0.8.24 (January 2024): Added inline assembly support for
tstoreandtload. Emits a warning when these opcodes are used, directing developers to review security implications. - Solidity 0.8.28 (October 2024): Added native
transientstorage layout qualifier. Variables declared astransientget automatic TSTORE/TLOAD compilation with type safety, reducing the risk of raw slot manipulation errors. - OpenZeppelin Contracts v5.1+: Includes
ReentrancyGuardTransient, a drop-in replacement forReentrancyGuardthat uses TSTORE/TLOAD for the lock. Costs ~24,000 gas less per guarded function call compared to the SSTORE-based version. - EIP-7201 (Namespaced Storage Layout): While designed for persistent storage, the same namespace convention (
keccak256("namespace") - 1as the storage slot base) should be applied to transient storage in proxy patterns to prevent slot collisions.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | High | Medium | Common reentrancy pattern; no specific TSTORE exploit yet, but TSTORE makes locks cheaper and thus more widely adopted with inconsistent coverage. |
| T2 | Smart Contract | Critical | Medium | SIR.trading exploit ($355K, March 2025). First real-world transient storage exploitation. |
| T3 | Smart Contract | High | Low | ChainSecurity pre-emptive disclosure (Jan 2024). No funds lost, but PoC code demonstrated viability. Solidity compiler now warns. |
| T4 | Smart Contract | Medium | Low | No exploit yet. Risk is theoretical but follows the same slot collision pattern as persistent storage in proxy contracts (well-documented hazard). |
| T5 | Smart Contract | Medium | Medium | No specific exploit. Flash loan + transient storage interaction is a natural extension of flash loan attack patterns. |
| P1 | Protocol | Low | N/A | Correct STATICCALL enforcement by EVM. No vulnerability. |
| P2 | Protocol | Low | N/A | Client implementations are consistent post-Cancun. L2 adoption timeline is the primary risk. |
| P3 | Protocol | Low | Low | No repricing proposed. Theoretical future risk. |
Related Opcodes
| Opcode | Relationship |
|---|---|
| TLOAD (0x5C) | The read complement to TSTORE. TLOAD reads a value from transient storage at the given key. Same 100-gas cost, same transaction-scoped lifetime. All TSTORE threats involving slot collisions and cross-call-frame leakage are exploited through TLOAD reads. |
| SSTORE (0x55) | Persistent storage write. SSTORE costs 5,000-22,100 gas (vs. 100 for TSTORE) and persists across transactions. SSTORE has a 2,300-gas minimum check (EIP-2200) that TSTORE lacks. TSTORE is the transient-scoped equivalent, sharing the same slot collision risks but at much lower cost. |
| SLOAD (0x54) | Persistent storage read. SLOAD reads permanent state; TLOAD reads transient state. Contracts migrating from SSTORE/SLOAD to TSTORE/TLOAD for same-transaction state must ensure the transient lifetime is sufficient. |
| DELEGATECALL (0xF4) | DELEGATECALL causes TSTORE/TLOAD to operate on the caller’s transient storage, not the delegate’s. This creates the slot collision risks described in T4. Same behavior as DELEGATECALL + SSTORE/SLOAD. |
| STATICCALL (0xFA) | TSTORE is prohibited in STATICCALL context. Any attempt to execute TSTORE during a STATICCALL causes the call to revert, enforcing read-only semantics. |
| CALL (0xF1) | Regular CALL creates a new call frame with separate revert scope. TSTORE operations in a sub-call that reverts are rolled back, but successful sub-call TSTORE operations persist for the rest of the transaction. |