Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x5C |
| Mnemonic | TLOAD |
| Gas | 100 |
| Stack Input | key (32-byte slot index) |
| Stack Output | tstorage[key] (32-byte value from transient storage) |
| Behavior | Reads a 32-byte value from transient storage at the given key. Transient storage is a per-contract, key-value data location introduced in EIP-1153 (Cancun/Dencun, March 2024). It behaves identically to persistent storage (SLOAD/SSTORE) in terms of addressing and execution context, except that all transient storage is discarded at the end of every transaction. There is no cold/warm access distinction — TLOAD always costs 100 gas. Writing to transient storage during a STATICCALL is not allowed (TSTORE reverts), but TLOAD is permitted. In a DELEGATECALL, transient storage operates on the calling contract’s transient state, not the callee’s — identical to how SLOAD behaves in delegatecall contexts. |
Threat Surface
Transient storage is the newest data location in the EVM (live since March 2024), making TLOAD/TSTORE one of the least battle-tested opcode pairs in production. While conceptually simple — “storage that auto-clears per transaction” — the interaction between transient lifetime semantics and existing Solidity/EVM patterns creates a threat surface that developers are still learning to navigate.
The threat surface centers on four properties:
-
Transient storage persists across calls within a transaction, not just within a single call frame. This is the most dangerous misconception. Developers familiar with memory (which is per-call-frame) may assume transient storage is also per-call-frame. It is not. A value written by TSTORE in call frame A is readable by TLOAD in call frame B, C, or D — anywhere in the same transaction. This means stale transient values from earlier calls can pollute later calls, enabling permission bypasses, reentrancy through residual state, and cross-function data corruption.
-
Transient storage does NOT persist across transactions. Developers familiar with SLOAD/SSTORE may assume transient storage survives across blocks. It does not. Any contract that uses TLOAD to read a “flag” or “lock” from a previous transaction will read 0. This is by design, but contracts that mix persistent and transient storage patterns without understanding the lifetime difference will silently fail.
-
TSTORE operates below the 2300 gas threshold, breaking reentrancy assumptions. SSTORE fails when remaining gas is below 2300 (EIP-2200), which is why Solidity’s
transfer()and Vyper’ssend()— both of which forward only 2300 gas — were historically considered reentrancy-safe. TSTORE has no such minimum gas requirement. A callee receiving ETH viatransfer()can now execute TSTORE, write to transient storage, and influence the caller’s subsequent TLOAD reads. This breaks a decade-old security assumption that low-gas ETH transfers cannot trigger meaningful state changes. -
Transient storage shares the same key space model as persistent storage but has a completely different lifetime. A contract can use slot 0 for both
SSTORE(persistent) andTSTORE(transient) without collision — they are separate address spaces. But developers who mentally merge them, or who refactor SLOAD/SSTORE code to TLOAD/TSTORE for gas savings, may introduce lifetime bugs where values that should survive across transactions are silently cleared.
Smart Contract Threats
T1: Stale Transient Storage Enabling Permission Bypass (Critical)
Transient storage values are not automatically cleared between internal calls within the same transaction. If a contract writes a value to transient storage (e.g., a callback address or a permission flag) and does not explicitly clear it after use, subsequent calls in the same transaction can read the stale value and exploit it.
The SIR.trading exploit (March 2025, $355K) is the canonical example. The protocol’s vault contract stored a Uniswap V3 pool address in transient slot 0x1 during the mint flow. After the swap callback completed, the slot was reused to store an amount value — but the pool address was never cleared. In a later call within the same transaction, the uniswapV3SwapCallback function read slot 0x1 expecting a pool address but found the amount. The attacker brute-forced a contract address (0x00000000001271551295307acc16ba1e7e0d4281) whose numeric representation matched the stored amount, then called uniswapV3SwapCallback directly from that address, bypassing the pool verification check.
Why it matters: Transient storage’s transaction-wide lifetime is its defining feature, but it is also its most dangerous property. Any slot left non-zero after a function completes is a potential attack surface for every subsequent call in the transaction.
T2: Transient Reentrancy Locks Not Cleared Between Calls (High)
Transient storage reentrancy locks are a recommended gas-efficient replacement for SSTORE-based locks (EIP-1153 explicitly mentions this use case). The standard pattern is:
function protectedFunction() external {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
// ... external calls ...
assembly {
tstore(0, 0)
}
}The footgun: if the function reverts after setting the lock but before clearing it, the lock remains set for the rest of the transaction. Depending on the revert scope, this can permanently lock the contract within the transaction (denial of service) or, conversely, if the lock is set in a subcall that reverts and the parent continues, the lock is cleared by the revert and the reentrancy guard is silently disabled.
The subtle case is a subcall that sets a transient lock, executes an external call, then reverts. Revert restores transient storage to its state before the subcall, including removing the lock. The parent call continues without the lock, and if the external call triggered a callback, the callback can reenter the now-unlocked function.
Why it matters: Transient storage reentrancy locks interact with EVM revert semantics in ways that differ from persistent storage locks. The “lock survives revert” assumption from SSTORE-based guards does not apply identically — transient storage is rolled back on revert within the subcall’s scope, just like persistent storage, but the transaction-wide visibility means locks can leak between unrelated call paths.
T3: Cross-Contract Transient Storage Assumptions (High)
Transient storage is per-contract — contract A cannot directly read contract B’s transient storage. However, contracts frequently make implicit assumptions about other contracts’ transient state:
-
Protocol-level reentrancy locks. A lending protocol may assume that setting a transient lock in contract A prevents reentrancy into contract B. It does not — each contract has independent transient storage. Cross-contract reentrancy guards require explicit inter-contract signaling (e.g., a shared singleton lock contract).
-
Callback ordering assumptions. A contract that calls out to an untrusted address may assume the callee cannot influence the caller’s transient storage. This is true for direct reads (transient storage is per-contract), but a malicious callee can call back into the caller and trigger functions that write to the caller’s own transient storage, poisoning subsequent TLOAD reads.
-
Composability with flash loans. Flash loan callbacks execute within the same transaction. If a flash loan borrower’s callback writes to transient storage that a subsequent protocol interaction reads, the stale transient state can lead to incorrect invariant checks or permission grants.
Why it matters: The per-contract isolation of transient storage provides safety against direct cross-contract reads, but the transaction-wide lifetime means any external call can indirectly influence a contract’s transient state through reentrancy.
T4: Transient Storage in DELEGATECALL Context (High)
In a DELEGATECALL, TLOAD and TSTORE operate on the calling contract’s transient storage, not the callee’s (mirroring SLOAD/SSTORE behavior). This has critical implications for proxy patterns:
-
Proxy and implementation share transient storage. When a proxy delegatecalls an implementation contract, both operate on the proxy’s transient storage. If the implementation sets a transient reentrancy lock, it is set in the proxy’s transient space. If the proxy has multiple implementation contracts (Diamond/EIP-2535 pattern), they all share the same transient key space and can collide on lock slots.
-
Library contracts called via delegatecall. A library designed for delegatecall that uses transient storage slot
0for a lock will collide with any other library using the same slot. Unlike persistent storage where EIP-7201 namespaced storage layout mitigates collisions, there is no established namespacing standard for transient storage yet. -
Transient storage metadata in proxy patterns. EIP-1153 suggests using transient storage to pass metadata from proxy to implementation (avoiding calldata modification). If the metadata slot is not cleared after use, subsequent calls through the proxy in the same transaction see stale metadata.
Why it matters: Proxy patterns are the most common deployment model in DeFi. Transient storage slot collisions between implementation contracts, or stale metadata from prior calls in the same transaction, can break access control and invariant enforcement.
T5: Assuming Transient Storage Persists Across Transactions (Medium)
Developers accustomed to SLOAD/SSTORE may refactor persistent storage reads to TLOAD for gas savings without realizing the lifetime difference. Patterns vulnerable to this mistake:
-
Governance or timelock flags. A flag set in transaction N via TSTORE is gone in transaction N+1. Any governance proposal status, timelock countdown, or configuration flag stored transiently will silently reset to 0 in the next block.
-
Accumulator patterns. A running total (e.g., cumulative fee counter, vote tally) stored in transient storage resets to 0 each transaction. The contract silently loses all accumulated state.
-
Migration from SSTORE to TSTORE. During gas optimization, developers may search-and-replace SSTORE/SLOAD with TSTORE/TLOAD. If any of the replaced slots need cross-transaction persistence, the contract breaks silently — TLOAD returns 0 for any key not written in the current transaction.
Why it matters: The SLOAD-to-TLOAD refactoring mistake is easy to make and hard to detect in testing, because most unit tests execute within a single transaction where transient and persistent storage behave identically.
Protocol-Level Threats
P1: No Cold Access DoS Vector (Low)
TLOAD costs a flat 100 gas with no cold/warm distinction. Unlike SLOAD (which costs 2100 gas for cold access under EIP-2929), TLOAD cannot be used for gas griefing through cold state reads. There is no state trie lookup — transient storage exists only in ephemeral memory during transaction execution. Loops over TLOAD are limited only by the block gas limit, making TLOAD one of the cheapest state-access opcodes and providing no DoS amplification.
P2: Consensus Safety (Low)
TLOAD is deterministic — it reads from the transient storage map, which is initialized to all-zeros at the start of each transaction and modified only by TSTORE. All conformant client implementations agree on the result. TLOAD was extensively tested across Geth, Nethermind, Besu, and Erigon before the Dencun activation. No consensus bugs have been attributed to TLOAD.
P3: TLOAD Breaks the 2300-Gas Reentrancy Assumption (Medium)
While TLOAD itself is a read-only operation (100 gas, no state modification), its companion TSTORE can execute with less than 2300 gas remaining. This means that a callee receiving ETH via Solidity’s transfer() or Vyper’s send() (2300 gas stipend) can execute TSTORE to modify its own transient storage, then a later TLOAD in the caller reads the modified value. This breaks the decade-old assumption that transfer()/send() prevent meaningful state changes in the callee.
ChainSecurity demonstrated this in November 2023 with concrete exploit scenarios: a temporary WETH approval pattern and an ETH locker contract, both vulnerable to reentrancy through low-gas TSTORE followed by TLOAD reads in subsequent execution frames. While TLOAD is the read side, the exploitable behavior requires both TSTORE (write at < 2300 gas) and TLOAD (read in a later frame).
P4: TLOAD Across Hard Forks (Low)
TLOAD was introduced in EIP-1153 (Dencun/Cancun, March 2024). No subsequent EIPs have modified its semantics or gas cost. Pre-Dencun, opcode 0x5C was an invalid opcode that caused an immediate revert. Contracts that check for TLOAD availability via INVALID catch must handle the pre-Dencun case on L2 chains or alternative EVMs that may not have adopted EIP-1153.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
| Reading an unset key | Returns 0 (all transient slots initialize to zero each transaction) | Identical to SLOAD on an unset persistent slot; code that checks tload(key) != 0 as a flag will see “not set” for any key not written in the current tx |
| Persistence within tx across calls | Value written by TSTORE in call frame A is visible to TLOAD in call frame B (same contract, same tx) | Stale transient values from earlier calls can pollute later calls; root cause of SIR.trading exploit |
| Clearing at transaction end | All transient storage is discarded after each transaction — every key resets to 0 | Flags, locks, or accumulators in transient storage do not survive across transactions; no explicit cleanup needed between txs |
| DELEGATECALL context | TLOAD reads the calling contract’s transient storage (same as SLOAD in delegatecall) | Multiple implementation contracts sharing a proxy’s transient key space can collide on slot indices; no established namespacing standard |
| STATICCALL context | TLOAD is permitted in STATICCALL (read-only); TSTORE is prohibited (would revert) | TLOAD in view/pure functions works normally; no state modification risk |
| Subcall reverts | Revert rolls back all TSTORE writes made within the reverted subcall; TLOAD in the parent sees pre-subcall values | Transient reentrancy locks set in a reverted subcall are silently removed; parent execution continues without the lock |
| TLOAD after SELFDESTRUCT (same tx) | TLOAD still reads from the contract’s transient storage within the same tx (transient storage is not affected by SELFDESTRUCT) | Edge case for contracts using SELFDESTRUCT + CREATE2 patterns; transient state persists until tx end |
| Interaction with SLOAD on same slot index | TLOAD and SLOAD with the same key read from different address spaces (transient vs. persistent) | No collision, but developers may confuse which storage type they’re reading, especially during SSTORE-to-TSTORE refactoring |
| Gas remaining < 100 | TLOAD reverts (out of gas) | Standard OOG behavior; no special threshold like SSTORE’s 2300-gas stipend requirement |
Real-World Exploits
Exploit 1: SIR.trading (Synthetics Implemented Right) — $355K via Stale Transient Storage (March 2025)
Root cause: Transient storage slot reused without clearing, enabling an attacker to bypass callback verification by substituting a pool address with a controlled amount value.
Details: SIR.trading’s vault contract used transient storage slot 0x1 to store a Uniswap V3 pool address before performing a swap in its mint function. The uniswapV3SwapCallback function read slot 0x1 to verify that msg.sender matched the stored pool address before transferring tokens. After the callback completed, the same slot 0x1 was overwritten with the swap amount to return data to the mint function — but the pool address was never explicitly cleared.
The attacker exploited this by first calling mint normally, which wrote the amount to slot 0x1. Then, within the same transaction, the attacker called uniswapV3SwapCallback directly. The callback loaded slot 0x1, which now contained the amount (not a pool address). The attacker had brute-forced a contract address (0x00000000001271551295307acc16ba1e7e0d4281) whose 160-bit representation matched the stored amount. The attacker deployed this contract via CREATE2 and called the callback from it, passing the msg.sender == pool verification. The callback transferred vault tokens to the attacker.
TLOAD’s role: TLOAD read a stale transient value (the amount) that should have been a pool address. The transient storage slot was not cleared between the swap callback and the subsequent direct callback invocation. The transaction-wide lifetime of transient storage — the defining property of EIP-1153 — was the root enabler.
Impact: ~$355,000 drained (17,814 USDC, 1.4 WBTC, 119.8 WETH), later converted to 193 WETH and laundered through Railgun. SIR.trading had launched just 38 days prior.
References:
- Verichains: EIP-1153 Transient Storage — Save Gas, Lose Bag
- Blockscope Research: SIR Protocol Exploit
- SlowMist: Fatal Residue — An On-Chain Heist Triggered by Transient Storage
Exploit 2: ChainSecurity TSTORE Low-Gas Reentrancy — Demonstrated Vulnerability Class (November 2023, Pre-Dencun)
Root cause: TSTORE executes with less than 2300 gas remaining, breaking the assumption that Solidity’s transfer() and Vyper’s send() prevent reentrancy.
Details: ChainSecurity published a detailed pre-Dencun analysis demonstrating that TSTORE’s lack of a minimum gas requirement (unlike SSTORE’s 2300-gas floor from EIP-2200) creates a new reentrancy vector. They provided three concrete exploit scenarios:
-
temporaryApprove WETH drain. A WETH contract implementing
temporaryApprove()using transient storage. ThewithdrawAllTempFrom()function reads the temporary allowance, sends ETH, then writes the balance to persistent storage. During the ETH transfer (2300 gas stipend), the recipient executes TSTORE to zero out the temporary allowance. The storage write after the transfer reads the now-zeroed transient allowance, leaving the attacker’s balance intact while the ETH is drained. -
ETHLocker callback reentrancy. An ETH vault using transient storage for deferred liquidity checks. During a
batch()callback, a low-gas reentrant call modifies transient balances, breaking the vault’s invariant enforcement. -
Pre-existing contract trust model broken. Contracts like EthsMarketV2 and Disperse (>150,000 calls) use
transfer()to send ETH. Pre-EIP-1153, these were reentrancy-safe. Post-EIP-1153, any new contract using transient storage that interacts with these existing contracts must account for the new reentrancy vector.
TLOAD’s role: In each scenario, TLOAD reads transient values that were modified by TSTORE during a low-gas callback. The read returns attacker-controlled data that the original caller trusts for authorization or accounting decisions.
Impact: No mainnet exploitation of this specific vector has been publicly reported as of March 2026, but the vulnerability class is real and affects any contract using transient storage in conjunction with transfer()/send() patterns.
References:
Attack Scenarios
Scenario A: Stale Transient Storage Callback Bypass (SIR.trading Pattern)
contract VulnerableVault {
// Transient slot 0x1 used for BOTH pool address and amount
function mint(address token, uint256 amount) external {
address pool = getUniswapPool(token);
// Store pool address in transient storage for callback verification
assembly { tstore(0x1, pool) }
// Perform swap -- pool calls back uniswapV3SwapCallback
IUniswapV3Pool(pool).swap(/* ... */);
// After callback, slot 0x1 is OVERWRITTEN with the amount
uint256 returnedAmount;
assembly { returnedAmount := tload(0x1) }
// ... use returnedAmount to mint tokens ...
// BUG: slot 0x1 still contains the amount, not cleared
}
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata data
) external {
// Read pool address from transient storage
address expectedPool;
assembly { expectedPool := tload(0x1) }
// Verify caller is the pool
require(msg.sender == expectedPool, "not pool");
// Transfer tokens to pool
IERC20(token).transfer(msg.sender, uint256(amount0Delta));
// Store amount in transient slot 0x1 for mint to read
assembly { tstore(0x1, amount0Delta) }
}
}
// Attack: after a normal mint() call (which leaves the amount in slot 0x1),
// the attacker calls uniswapV3SwapCallback directly from a contract
// whose address numerically equals the stale amount in slot 0x1.
// The require(msg.sender == expectedPool) passes because
// msg.sender == address(amount), draining the vault.Scenario B: Transient Reentrancy Lock Bypassed via Subcall Revert
contract VulnerablePool {
mapping(address => uint256) public balances;
modifier transientLock() {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
_;
assembly { tstore(0, 0) }
}
function withdraw(uint256 amount) external transientLock {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success,) = msg.sender.call{value: amount}("");
require(success);
}
}
// If withdraw is called from WITHIN a subcall that later reverts,
// the transient lock (tstore(0, 1)) is rolled back by the revert.
// The parent call can then call withdraw again -- the lock is gone.
//
// Example: Contract A calls a helper that internally calls
// pool.withdraw() then reverts. The lock is rolled back.
// A then calls pool.withdraw() directly -- no lock is set.Scenario C: Low-Gas TSTORE Reentrancy via transfer()
# Vulnerable: WETH with temporary approval using transient storage
tempAllowance: transient(HashMap[address, HashMap[address, uint256]])
balanceOf: public(HashMap[address, uint256])
@external
def temporaryApprove(spender: address, amount: uint256):
self.tempAllowance[msg.sender][spender] = amount
@external
def withdrawTempFrom(src: address, dst: address):
allowance: uint256 = self.tempAllowance[src][dst]
assert allowance > 0, "no allowance"
# Send ETH -- only 2300 gas, should be "safe"
send(dst, allowance)
# Read transient allowance and update persistent balance
# BUG: dst's receive() called temporaryApprove(dst, 0) via TSTORE
# (possible at <2300 gas because TSTORE has no gas floor)
# Now tempAllowance[src][dst] == 0
self.balanceOf[src] -= self.tempAllowance[src][dst] # subtracts 0!
self.tempAllowance[src][dst] = 0Scenario D: Transient Storage Slot Collision in Diamond Proxy
// Facet A: uses transient slot 0 for reentrancy lock
contract FacetA {
function riskyOperation() external {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
// ... perform operation ...
assembly { tstore(0, 0) }
}
}
// Facet B: uses transient slot 0 for a temporary flag
contract FacetB {
function setFlag() external {
assembly { tstore(0, 1) }
}
function checkFlag() external view returns (bool) {
bool flag;
assembly { flag := tload(0) }
return flag;
}
}
// Both facets are delegatecalled by the same Diamond proxy.
// They share the proxy's transient storage.
// If setFlag() is called first, riskyOperation() will revert
// because tload(0) returns 1 (the flag, not a reentrancy lock).
// Conversely, if riskyOperation() doesn't clear slot 0 (bug),
// checkFlag() returns true when no flag was set.Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Stale transient storage | Always clear transient storage slots after use | assembly { tstore(key, 0) } immediately after the value is consumed; treat transient slots like borrowed resources that must be returned |
| T1: Slot reuse for different types | Use unique transient slots for each purpose | Assign distinct constant keys per variable (e.g., POOL_SLOT = keccak256("pool"), AMOUNT_SLOT = keccak256("amount")); never multiplex slot semantics |
| T2: Reentrancy lock cleared by revert | Understand revert rollback semantics for transient storage | Transient locks in subcalls are rolled back on revert; place the lock in the outermost call frame, not in subcalls that may revert |
| T2: Lock not cleared on revert path | Use try/catch with explicit lock cleanup | Wrap external calls in try/catch; clear the transient lock in both success and catch paths |
| T3: Cross-contract reentrancy | Use a shared singleton lock contract | Deploy a dedicated lock contract; all protocol contracts call lock.acquire() / lock.release() using persistent or transient storage in the lock contract itself |
| T4: Delegatecall slot collision | Namespace transient storage slots per facet | Use keccak256(abi.encode(facetId, slotName)) as the transient key; adopt EIP-7201-style namespacing for transient storage |
| T5: Accidental SSTORE→TSTORE refactor | Audit all TSTORE/TLOAD usage for cross-tx persistence requirements | Grep for tstore/tload in assembly; verify each usage only needs intra-transaction lifetime; add comments documenting lifetime expectations |
| General: Low-gas reentrancy via transfer() | Stop using transfer() and send() for ETH transfers | Use call{value: amount}("") with reentrancy guards; the 2300-gas stipend is no longer a reliable reentrancy barrier post-EIP-1153 |
| General: Testing blind spot | Test multi-call-within-single-tx scenarios | Use Foundry’s vm.prank + sequential calls in the same test transaction; verify transient storage is clean between logical operations |
Compiler/EIP-Based Protections
- Solidity >= 0.8.24: Supports TLOAD/TSTORE via inline assembly (
tload(key),tstore(key, value)). The compiler emits a warning when TSTORE is used, advising developers to exercise “utmost care” for composability. No high-level language support yet (notransientkeyword in Solidity). - Vyper >= 0.3.8: Native
transient()type support (e.g.,myVar: transient(uint256)). Compiles to TLOAD/TSTORE directly. - EIP-1153 Security Considerations: The EIP itself warns: “smart contract developers should be careful to only leave transient storage slots with nonzero values when those slots are intended to be used by future calls within the same transaction.”
- OpenZeppelin ReentrancyGuardTransient: OpenZeppelin provides
ReentrancyGuardTransient(v5.1+), which uses transient storage for the reentrancy lock, saving ~15,000 gas compared to the SSTORE-basedReentrancyGuard. This is the recommended approach for transient reentrancy locks rather than hand-rolling assembly.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | High | SIR.trading ($355K, March 2025) |
| T2 | Smart Contract | High | Medium | ChainSecurity demonstrated exploit scenarios (November 2023) |
| T3 | Smart Contract | High | Medium | Theoretical; flash loan + transient storage interaction patterns |
| T4 | Smart Contract | High | Medium | No public exploit; Diamond/proxy pattern adoption growing |
| T5 | Smart Contract | Medium | Low | No public exploit; detectable in testing if multi-tx tests exist |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Low | N/A | — |
| P3 | Protocol | Medium | Medium | ChainSecurity pre-Dencun analysis; affects all transfer()/send() users |
| P4 | Protocol | Low | Low | — |
Related Opcodes
| Opcode | Relationship |
|---|---|
| TSTORE (0x5D) | Write counterpart to TLOAD. TSTORE writes a 32-byte value to transient storage at a given key. Same 100 gas cost, same transaction-wide lifetime, same delegatecall context behavior. TSTORE is prohibited in STATICCALL; TLOAD is permitted. The critical security difference: TSTORE has no minimum gas requirement (unlike SSTORE’s 2300-gas floor), enabling low-gas reentrancy. |
| SLOAD (0x54) | Persistent storage read. SLOAD reads from permanent contract storage that survives across transactions. TLOAD is the transient equivalent — same addressing model, same delegatecall context, but values are discarded at transaction end. SLOAD has cold/warm gas pricing (2100/100); TLOAD is always 100 gas. |
| SSTORE (0x55) | Persistent storage write. SSTORE is TSTORE’s persistent counterpart. Key differences: SSTORE has cold/warm/dirty pricing (up to 20,000 gas), a 2300-gas minimum requirement (EIP-2200), and gas refunds for clearing slots. TSTORE is always 100 gas, has no gas floor, and offers no refunds (nothing to refund — data is discarded). |