Abstract: transient storage makes reentrancy locks cheaper, not automatically safer

EIP-1153 adds `TSTORE` and `TLOAD`, giving EVM contracts a transaction-scoped key-value store. It is useful for reentrancy locks, temporary approvals, batching caches, and scratch state shared across internal execution steps. It also changes the audit model. Traditional `nonReentrant` patterns usually focus on a persistent storage lock. Yul and EVM bytecode can bypass Solidity's readable storage layout, compute raw slots manually, and keep transaction-local state visible across nested calls. The key conclusion is narrow: low-level reentrancy is not the old reentrancy bug written in assembly syntax. It is a state-machine problem involving control flow, slot namespaces, transient lifetime, delegatecall context, and cleanup paths. Defensive analysis has to place `SSTORE/SLOAD`, `TSTORE/TLOAD`, external `CALL`, `DELEGATECALL`, and Yul slot computation in the same control-flow graph.

Scope: defensive modeling, not exploit construction

This article covers three risk classes. The first is Yul or inline assembly directly writing storage slots that may overlap Solidity variables, proxy slots, library namespaces, or hand-written storage regions. The second is transient state poisoning: a transaction-local value is meant to be temporary, but a nested call or later entry point reads it under a different business assumption. The third is control-flow recovery after external calls. At the bytecode level, `JUMPDEST` and hand-written assembly returns can bypass cleanup code that looks obvious in high-level source.

The attacker model is defensive and abstract. The adversary can call public entry points, control a callback contract, influence call ordering inside the same transaction, or cause a normal function to be re-entered before the first execution path fully closes. This article does not provide reusable payloads, target selection methods, or live-contract exploitation steps. The point is to define what auditors and protocol engineers must check: which values survive for a transaction, which values survive for a call frame, which slots can be reused by low-level code, and which external calls transfer control to unknown logic.

EIP-1153 semantics: transient storage is not memory and not persistent storage

`TSTORE` and `TLOAD` provide low-cost storage that lasts for the current transaction. It is not memory. Memory belongs to a call context and is not naturally readable by another frame later in the same transaction. It is not ordinary storage either. Transient storage is discarded at the end of the transaction and does not become part of the long-term state trie.

A simplified transient reentrancy guard looks like this:

```text enter: require(TLOAD(lockSlot) == 0) TSTORE(lockSlot, 1) ... exit: TSTORE(lockSlot, 0) ```

The risk is not in that simplified shape. The risk is in the abnormal paths. Was the lock set before an external call? Is it cleared on failure, manual return, caught error, and early exit? Does a `DELEGATECALL` let library code write transient state in the caller's address context? Does another public entry point reuse the same lock slot? Are several locks encoded into one slot? A transient value has a short lifetime, but its visibility is wider than many engineers intuitively expect.

A useful safety invariant is:

`forall external_edge e: lock(owner, domain) == ENTERED => no_sensitive_state_delta_after_reentry(e)`

`owner` is the contract address or delegatecall context. `domain` is the function, asset, order, or route protected by the lock. A global lock is simple and strong but reduces concurrency. A fine-grained lock improves composability but increases slot computation and cleanup complexity. The design question is not whether transient storage can replace a persistent storage lock. The question is whether the lock domain and cleanup paths match the actual execution graph.

Revert behavior also needs care. Transient writes are subject to call-frame rollback, but which frames revert and which frames continue depends on error handling. A batch executor may catch one failed low-level call and continue. Assembly may manually inspect a call result. A `try/catch` branch may continue after a subcall failure. Auditors should inspect the transaction-level state machine, not only whether one internal function reverted.

Gas changes matter because they change design patterns. EIP-1153 makes temporary locks cheaper, so teams are more likely to store path phases, temporary allowances, route-local balances, and batching state in transient storage. Cost reduction encourages pattern expansion. The audit surface grows because every transient key can become an implicit input to a later call in the same transaction.

Yul slot overlapping: the compiler no longer protects the namespace

Solidity storage layout is deterministic for state variables, mappings, arrays, and structs. EIP-7201 discusses namespaced storage layout for modular systems. Yul can bypass those conventions. A raw `sstore(slot, value)` or `tstore(slot, value)` accepts a runtime-computed key. Once the slot is computed from a hand-written hash, offset, mask, pointer, or ABI-decoded value, two code paths may alias under some inputs.

Each storage access can be modeled as:

`Access = (kind, op, slot_expr, value_expr, context)`

`kind` is persistent or transient. `op` is read or write. `slot_expr` is the symbolic slot expression. `context` includes function, call depth, delegatecall status, and whether the access occurs before or after an unknown external call. Slot overlapping is not simple string equality. It is a satisfiability question: can two slot expressions map to the same key under some input assignment?

```text for each write a in storage_accesses: for each read_or_write b after external_call: if may_alias(a.slot_expr, b.slot_expr) and domains_conflict(a, b): report SlotOverlapAfterExternalCall ```

Static analyzers such as Slither and Mythril provide useful foundations: control-flow graphs, call graphs, data flow, taint tracking, and symbolic execution. Low-level Yul requires additional normalization. An analyzer needs to recognize `keccak256(ptr, len)` mapping slot construction, track `mstore` writes into memory ranges, normalize `add(base, offset)`, `and(mask)`, `shl`, and `shr`, and distinguish compiler-generated layout from hand-written namespaces. Without this normalization, an audit may miss slots that look different but can alias.

Memory pointer pollution is another low-level source of aliasing. Many assembly blocks build a mapping key in memory and then hash it. If the memory region is partially overwritten, reused after ABI decoding, or influenced by copied return data, the computed slot may depend on bytes the developer did not intend to include. The defensive rule is not to ban memory reuse. The rule is to fully overwrite the hashed region, use fixed lengths, domain-separate the preimage, and avoid deriving storage keys directly from unchecked return data.

Control-flow graph analysis: reentrancy hides after unknown calls

High-level source makes developers think in function order. EVM execution follows opcode control flow. `JUMPDEST` marks valid jump targets. External `CALL`, `STATICCALL`, and `DELEGATECALL` transfer control to code the current contract does not own. When control returns, execution resumes at the current frame. If the external target can trigger another entry into the same contract before returning, the analyzer must treat the external call as a possible recursive edge.

A defensive CFG state machine can be written as:

```text state = Clean on TSTORE(lock, 1): state = Locked on external_call_unknown while state == Locked: mark ReentryWindow on sensitive_sstore after ReentryWindow: require lock_domain_covers_write on function_exit: require all_transient_locks_cleared ```

This is not an exploit flow. It is an audit model. The important step is to include every recovery edge after an external call: success return, failed return, assembly-level `return`, `revert`, conditional jump, error branch, event branch, and batch continuation. Many locks that look clean in source cover only the main path. Yul early returns and manual jumps can bypass cleanup.

`DELEGATECALL` makes the problem larger. The callee executes in the caller's storage and transient-storage context. If a library treats a fixed slot as a temporary guard while the caller treats the same slot as another lock, balance marker, or phase flag, the modules can poison each other. Proxies, plugin routers, modular accounts, and shared libraries should document transient slot ownership as part of the interface, not as private implementation detail.

Transient state poisoning: dirty locks and dirty caches inside one transaction

Transient state poisoning occurs when a temporary value is valid for one logical stage but is read by a later nested call or second entry point under a different assumption. It may not look like the classic balance reentrancy pattern. It can be a temporary approval that remains visible, a route limit reused across paths, an order-phase flag left dirty, a fee discount cache carried forward, or a refund state inherited incorrectly.

A transaction can be abstracted as:

`Tx = [call_1, subcall_1.1, subcall_1.2, call_2, ...]`

If `call_1` writes `TSTORE(k, v)` and `subcall_1.2` or `call_2` reads `TLOAD(k)` without the same business preconditions, the system has a poisoning risk. The solution is not to ban all cross-call transient state. The solution is to domain-separate transient keys:

`key = H("allswap.lock", chainId, contract, functionSelector, asset, orderId)`

The more precise the domain, the lower the chance of accidental reuse. The more precise the domain, the more complex the cleanup and gas accounting. This is the actual trade-off.

Useful detection signals include: the same transient key is read and written by several public or external functions; a nonzero `TSTORE` is not followed by cleanup that dominates all normal exits; a sensitive `SSTORE` occurs after an unknown external call; a delegatecall target uses a fixed transient slot; assembly derives a slot from calldata or memory pointers; and one key is used as a lock, cache, and phase marker at the same time.

Defensive strategy: lock domains, cleanup paths, and audit rules

First, make lock domains explicit. A global lock is suitable for a single-vault settlement function. A route, asset, or order-level lock may be necessary for routers that need concurrency. Do not leave slot meaning in comments. Put slot constants, namespace hashes, and intended usage in auditable code. For proxy systems and libraries, slot conventions should be part of the interface.

Second, verify cleanup through the CFG. Every `TSTORE(k, nonzero)` should have cleanup that dominates normal exits. The audit should inspect compiled control flow, not only indentation. Early returns, low-level error handling, manual jumps, and assembly return paths matter.

Third, keep the Checks-Effects-Interactions rule, but refine it. The more accurate rule is: any temporary state set before an unknown external call must be assumed observable by a reentrant entry in the same transaction. Any sensitive persistent write after the call must prove that the lock domain covers every reentrant path.

Fourth, extend static analysis to assembly. Searching for Solidity-level `nonReentrant` is not enough. A useful analyzer marks `TLOAD`, `TSTORE`, `SLOAD`, `SSTORE`, `CALL`, `DELEGATECALL`, and `JUMPDEST` in the CFG, then tracks symbolic slot expressions. Human auditors should focus on inline assembly, proxy upgrades, libraries, fallback and receive handlers, token callbacks, cross-chain message executors, and batch routers.

Fifth, test same-transaction composition, not only single functions. Many transient-storage bugs appear only when a batch executor, multicall wrapper, token callback, refund entry point, and proxy path are composed. Defensive tests can use benign callback contracts to simulate reentry boundaries. The goal is to verify invariants, not to build attack payloads. Assertions should cover lock cleanup, order isolation, rollback consistency, and delegatecall namespace permissions.

Sixth, treat upgradeability as part of the threat model. A proxy upgrade can change transient slot meaning while routers, libraries, or frontends still assume the old convention. Upgrade reviews should compare `TLOAD/TSTORE` key spaces just as seriously as they compare storage layout.

Seventh, make audit reports state-specific. A useful report should say whether the window occurs before or after an external call, whether the affected key is persistent or transient, whether delegatecall namespaces are involved, whether funds, approvals, refunds, or fees are affected, and whether the fix is a new lock domain, a cleanup patch, a function split, or removal of unnecessary assembly.

Repair priority should follow the state object. A path that can repeatedly release funds after an external call is an immediate stop-ship issue. A transient key reused across order phases may require a lock-domain redesign and event-semantics review. A slot alias that exists only on an unreachable path may be handled with regression tests and static assertions. A tool warning that cannot prove safety but is manually justified should become a documented invariant, not a dismissed finding. This classification is more useful than a generic “high / medium / low” label because it ties the issue to funds, authorization, refunds, or accounting.

Runtime monitoring can also help without exposing exploit details. Useful production signals include repeated entry into the same route within one transaction, unusual rollback frequency, route state written and then cleared before settlement, repeated refund state transitions, and transient guard hit rates. None of these signals proves exploitation by itself. They are early-warning indicators that the contract is operating near a reentrancy boundary. Monitoring should preserve user privacy and avoid leaking strategy, but it should give incident responders enough context to distinguish normal batching from abnormal callback behavior.

One practical audit rule is to require a transient-key inventory. For every `TSTORE`, the inventory should list the key derivation, lock domain, reader functions, writer functions, cleanup paths, external-call windows, and delegatecall assumptions. For every `TLOAD`, the inventory should state which business precondition makes the read valid. If the team cannot write this inventory, the code is already too implicit for a high-value router.

AllSwap relevance: route safety depends on state closure, not only modifiers

AllSwap-style non-custodial cross-chain exchange flows involve quotes, locks, execution, failure refunds, and multi-asset paths. If an EVM contract optimizes routing with Yul or transient storage, the risk is not only that the contract is “reentrant.” The practical risk is that a route state becomes polluted and a later path believes an order is in a safe phase, an asset limit has been consumed, or a refund condition has been met.

A route system can separate state into three layers. `persistentSettlementState` is long-lived on-chain state. `transientExecutionState` is transaction-local execution state. `offchainRouteState` is what the quote and risk engine sees. The safety boundary is that these layers must not impersonate each other. A transient lock hit should not be interpreted off-chain as order completion. A persistent write should not expose a repeatable release path before the external callback window has closed.

A simple risk score is:

`contractRisk = assemblyWriteRisk + transientAliasRisk + externalCallbackRisk + refundStateComplexity`

If a route depends on a complex Yul router, token callbacks, proxy contracts, and multiple external calls, risk controls should increase audit depth, lower per-transaction limits, or require a stricter refund state machine. The user does not need to see these internals. The user needs clear states: locked, executing, rolling back, refunded, or settled.

For cross-chain execution, a swap can be modeled as `quoted -> locked -> executing -> callbackWindow -> settled/refunding`. The `callbackWindow` stage is often under-modeled. The contract has entered external code, some transient markers are set, and the final persistent state has not closed. If another entry point reads temporary state at that moment, an off-chain router may misread progress. Events should describe closed persistent states, not internal transient phases.

Audit priority should follow the money path. A transient cache bug in a helper contract may only revert a transaction. The same bug in a router, vault, refund executor, or cross-chain message handler can affect fund release. Security scheduling should consider whether the contract is the last hop before asset movement, whether it accepts callbacks, whether it supports multicall, whether it uses Yul, and whether it composes modules through delegatecall.

There is also a product-level constraint. Users do not see `TSTORE` or `JUMPDEST`; they see a swap that is pending, completed, failed, or refunded. The on-chain contract must therefore emit events only after a state transition is closed enough for off-chain systems to consume. If a temporary execution phase emits a completion-like event before external callbacks finish, the route engine can become inconsistent even when the chain state eventually reverts or settles correctly. Event semantics are part of the security boundary.

For AllSwap-like infrastructure, a conservative integration rule is: transaction-local state can protect execution, but it should not define business truth outside the transaction. Business truth should come from persistent settlement state, verified message status, and refund state that survives transaction boundaries. This rule reduces some gas optimizations, but it keeps route accounting and user-visible state explainable under failure.

Open problems: audit tooling is still catching up to EIP-1153 semantics

First, transient storage namespaces are less mature than ordinary storage layout. Teams can hash namespaces, but cross-library and proxy conventions still depend heavily on engineering discipline.

Second, static analysis of Yul slot aliasing is hard. Conservative analysis produces noisy reports. Optimistic analysis misses real aliases. Better symbolic normalization and path-sensitive analysis are needed.

Third, `DELEGATECALL` plus transient storage makes state ownership non-obvious. Proxies, modular accounts, routers, and plugins all increase the number of components that may believe they own the same temporary namespace.

Fourth, runtime monitoring struggles to distinguish normal batching from abnormal reentry boundaries. Multiple entries in one transaction are not enough as a signal; monitoring needs state changes, external callbacks, and failure-path context.

Fifth, security education still overemphasizes high-level `nonReentrant`. EIP-1153 makes cheaper locks possible, but it also requires engineers to understand transaction-scoped lifetime, assembly control flow, and slot namespace design.

References

[1] EIP-1153: Transient storage opcodes, https://eips.ethereum.org/EIPS/eip-1153

[2] Solidity documentation, Transient Storage, https://docs.soliditylang.org/en/latest/contracts.html#transient-storage

[3] Solidity documentation, Inline Assembly, https://docs.soliditylang.org/en/latest/assembly.html

[4] Solidity documentation, Security Considerations: Reentrancy, https://docs.soliditylang.org/en/latest/security-considerations.html#reentrancy

[5] Solidity internals, Layout of State Variables in Storage, https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html

[6] OpenZeppelin Contracts, ReentrancyGuardTransient, https://docs.openzeppelin.com/contracts/5.x/api/utils#ReentrancyGuardTransient

[7] EIP-2200: Structured Definitions for Net Gas Metering, https://eips.ethereum.org/EIPS/eip-2200

[8] EIP-7201: Namespaced Storage Layout, https://eips.ethereum.org/EIPS/eip-7201

[9] Slither static analyzer, https://github.com/crytic/slither

[10] Mythril symbolic execution tool, https://github.com/ConsensysDiligence/mythril

[11] Ethereum Yellow Paper, https://ethereum.github.io/yellowpaper/paper.pdf

[12] evm.codes Cancun opcode reference, https://www.evm.codes/?fork=cancun

FAQ

Does EIP-1153 transient storage make reentrancy safer?

It makes transaction-scoped locks cheaper, but not automatically safer. If lock domains, slot namespaces, or cleanup paths are wrong, TSTORE and TLOAD can make same-transaction state poisoning harder to detect.

Why does Yul increase slot overlapping risk?

Yul can read and write runtime-computed slots directly, bypassing the readability of Solidity storage layout. Hand-written hashes, offsets, delegatecall namespaces, or pointer reuse can make different logic map to the same slot.

Is a traditional nonReentrant modifier enough for EIP-1153 risks?

It is still useful, but incomplete. Audits must confirm that locks cover external callbacks, abnormal exits, delegatecall paths, assembly returns, and the difference between persistent and transient lock state.

Why do cross-chain routers care about transient state poisoning?

Routers manage quotes, execution, refunds, and multi-asset paths. If temporary approvals, phase flags, or refund markers are read under the wrong transaction context, a later path may release or roll back funds incorrectly.

Sources & references