DeFi Security AllianceRequest an audit
Menu

EVM patterns

Solidity Transient Storage Security: Lifetime, Ownership and Cleanup

Temporary state resets at transaction end and can still be shared by calls within that transaction. Transient storage security requires explicit owner mapping, successful-call cleanup and rollback tests for the actual execution graph.

Blue temporary latch in a white circuit with a return loop beside an independent circuit, illustrating transient state ownership.

Key facts

Original lifetime grid
32 constructed call-context, static, rollback, cleanup and observation cases
Same transaction
2 of 16 owner observations remain nonzero
Next transaction
16 of 16 owner observations are zero
Static writes
16 of 32 cases model a write exception
Limit
Source-derived model with zero initial stores, no EVM or gas benchmark

Identify the owner and lifetime of temporary state

A transient value disappears at transaction end, but can remain visible between successful calls within that transaction. Solidity transient storage security depends on both lifetime and ownership. A reentrancy lock that is never cleared can block a later legitimate call, while a delegated call can share the caller's transient store. Those are different boundaries and need different tests.

Transient storage is transaction-lifetime state accessed through the TLOAD and TSTORE opcodes introduced by EIP1153. It is not persistent disk-backed contract state. Nor is it private memory belonging to one call frame. Frames that share a storage owner can see the same transient slots during the transaction.

Ownership follows the execution context. A CALL uses the target contract's transient store. A DELEGATECALL uses the caller's store, as it does for persistent storage. A proxy and delegated implementation therefore need to reason about shared transient slots even when they appear as separate source contracts.

Temporary-state boundaries and the question each answers
BoundaryMeaningReview question
Transaction endTransient values are discardedDoes the design need state to survive later transactions?
Successful call returnWritten values can remain for later same-transaction callsShould the function clear its temporary state?
Reverted frameWrites since frame entry roll backWhat preexisting state does the outer frame retain?
Delegated contextThe caller owns the accessed storeWhich code shares the same slot?

The EIP1153 specification defines these execution rules. Our language discussion uses fixed Solidity v0.8.30 documentation, and our guard example uses OpenZeppelin Contracts v5.4.0. These are pinned reference versions, not a claim that either is the latest release.

The existing reentrancy guide explains the broader state-transition risk. Here the engineering job is to identify which call frames share temporary state, where cleanup occurs and how rollback affects observations. A lower-cost state primitive does not supply those design decisions.

Before using a transient variable, state its intended lifetime in the design. A per-call lock should end after successful work. A transaction-wide accumulator may intentionally persist between calls. A temporary approval needs an explicit permitted consumption path. The same automatic transaction reset supports these different designs but does not distinguish them.

Original call context and cleanup grid

Method

We built a finite model from the source rules for storage ownership, static writes, frame rollback and transaction reset. Two artificial contracts begin with zero in their modeled slots. One call attempts to write a value, may revert or clear after success, then an observation occurs in the same transaction or the next one.

Primary sources
EIP1153, fixed Solidity v0.8.30 transient-storage documentation and OpenZeppelin v5.4.0 transient guard, retrieved on .
Axes
CALL/DELEGATECALL ownership, inherited static/nonstatic context, explicit revert/success, clear/no clear and same/next transaction observation.
Population
32 Cartesian input cases; no excluded or missing rows.
Static interpretation
The static flag represents an inherited static context. It is independent of the two ownership choices; cleanup does not execute after the modeled write exception.
Artifacts
seo/research/integration-boundaries-2026-10-11/support-execution/finite-models.py and transient-storage-security-model-2026-10-11.json, with a complete CSV ledger. Retained repository artifacts are not public downloads.

Results

Complete source-derived transient-state model, measured
ObservationCountDenominator
Same-transaction owner store remains nonzero216 same-transaction cases
Next-transaction owner store is zero1616 next-transaction cases
Static write raises modeled exception1632 input cases

Surviving same-transaction values occur after nonstatic successful work without explicit cleanup, once for each ownership mode. A next-transaction observation is zero under every fixture. Both results can be true: transaction cleanup does not perform successful-call cleanup.

Our parent's store is nonzero in only 1 of the 16 same-transaction cases. That distinction follows ownership: the surviving direct-call value belongs to the callee, while the surviving delegated value belongs to the parent. An observer reading the wrong contract's store could incorrectly conclude that no temporary value remained.

Limits

  • No Solidity compiled and no EVM ran. The rows are a source-derived state model, not transaction observations.
  • Both stores begin at zero. The model does not independently measure restoration of a nonzero value present before frame entry.
  • Gas usage, nested grandchildren, deployed slot collisions and business invariants are outside the finite grid.

This result supports a concrete acceptance test: where a lock is intended to permit sequential calls, test two successful calls in one transaction. A fresh-transaction test alone cannot reveal the missing cleanup illustrated by this model.

Clear locks at the composability boundary

A lock expresses a state transition: not entered, entered during protected work, then no longer entered. With transient storage, the final transition still matters when other work can occur in the same transaction. EIP1153 and the pinned compiler documentation both warn about leaving a lock set beyond its intended use.

The pinned ReentrancyGuardTransient sets its Boolean slot before the protected body and clears it afterward. That explicit cleanup is part of its composability behavior. Its documentation warns that one guarded function cannot directly call another guarded function. A design using internal helpers or separate external entrypoints needs to preserve the intended guard coverage.

Call cleanup occurs before automatic transaction resetA first protected call sets and then clears its owner slot so a later call in the same transaction can proceed. Transaction reset happens after those calls. Omitting the successful-call clear leaves the later call observing the set slot.Protected work then clearLater callTransaction resetA later call is earlier than transaction end
A per-call lock needs a successful-return cleanup policy even though its backing store is temporary.
Entry
Inspect the owner slot and reject an already-entered context where the policy requires it.
Protected work
Set the lock while external or internal work can expose the protected state.
Successful return
Clear a per-call lock before later same-transaction work is allowed.
Transaction reset
Discard remaining transient state after the transaction boundary.

Clearing every transient slot indiscriminately is not the rule. A transaction-wide accounting design may intentionally carry information between calls. The review should identify who writes the value, who may consume it and when its purpose ends. An explicit retained value can be correct; an unexplained retained lock can break the application's calling composition.

Do not confuse temporary approvals with locks. A one-time approval stored transiently may need consumption or invalidation before the transaction finishes, especially if another route can use it. Automatic reset limits its lifetime to the transaction but does not restrict the set of callers or actions within that lifetime.

The guard also protects only the routes where it is applied and the state it shares. An unguarded function can still observe an intermediate application state. Review the complete entrypoint graph and the invariants each path requires. A slot that correctly blocks one callback cannot certify unrelated state-changing routes.

Document the clearing path alongside the intended composition. A receiver used by an aggregator, a router executing several actions or a proxy hosting multiple delegated components can encounter sequential calls that an isolated function test never exercises. The positive sequential fixture is as valuable as the rejected recursive fixture.

Follow revert and static call behavior

A reverted frame rolls back transient writes made since that frame began, including writes made through its inner calls. It does not erase every transient value in the transaction. An outer frame may have written state before entering the failed call, then caught the failure and continued. The correct observation is the restored pre-entry value.

Our finite grid begins at zero, so rollback yields zero there. Testing restoration of a nonzero earlier value requires a different fixture. In an EVM test, set an outer value, enter an inner path that changes it and reverts, catch the failure and read the owner slot. The expected assertion comes from the source rollback rule; this article does not report that execution as performed.

Execution-context tests and their intended observations
TestExpected source-rule observationBoundary to preserve
Static transient readTLOAD is permittedReading does not authorize a later write
Static transient writeTSTORE raises an exceptionNo cleanup after the failed write is assumed
Caught inner revertPre-entry transient state is restoredEarlier outer writes remain
Fresh transactionTransient values begin at defaultPersistent application state is separate

A static context can be inherited through nested calls. The ownership mode and write permission are separate factors: delegated code can use the caller's store while still being forbidden to write because the surrounding execution is static. An ownership-only model would miss that distinction.

EIP1153 exempts TSTORE from the persistent SSTORE gas-stipend check. A security argument that depends on a low-gas callback being unable to change storage must therefore examine the actual primitive. Do not generalize a persistent-write restriction into a guarantee that all state-changing behavior is impossible under a familiar stipend.

EIP1153's opcode gas schedule is not the total cost of a protected function. A function also executes its application work, control flow and other calls. Our grid measured no gas and supplies no savings estimate. Performance choices need an actual benchmark under the deployed compiler and chain configuration.

Failure handling should record which boundary failed. A static write exception, a deliberate application revert and a rejected reentrant call can all abort work, but they support different conclusions. Keep accepted control fixtures so a broad revert cannot conceal an absent authorization or cleanup condition.

Use separate nonzero-baseline rollback fixtures for the two ownership modes. In a delegated call, record an earlier value in the parent's store, let the delegated frame replace it and deliberately revert that frame. After the parent catches the failure, its earlier value should be restored under the specification's rollback rule. This expected outcome was source-reviewed; it was not executed by our zero-initialized model.

For a direct call, establish a nonzero baseline in the callee's store and keep the parent's store separate. The callee changes its own value, then its frame reverts. The callee's earlier value should be restored while the parent's independent value follows the parent's own execution. A test reading only the parent would miss the callee's rollback boundary.

Observe the correct owner through its getter or an intentionally exposed test method. TLOAD reads the current owner's store; it does not let one contract arbitrarily inspect another contract's transient slots. A direct call to the other contract can obtain an observation through that contract's code. The test harness must preserve this call relationship so its evidence matches the ownership claim.

The pinned compiler also treats reading transient state as non-pure and writing it as non-view. Include mutability expectations in the interface review. A high-level declaration restriction, a static execution restriction and an application lock policy are different reasons a proposed access may be unavailable. Diagnose the actual boundary instead of assigning every rejected write to the reentrancy guard.

Retain both rollback fixtures beside the sequential-success fixture. Their expected outcomes are complementary: rollback restores the earlier frame state, successful cleanup permits later work and transaction reset begins the next lifetime. Testing only one of those transitions leaves the others as assumptions.

Test proxy paths and sequential calls

The pinned Solidity documentation requires a Cancun-or-newer EVM target for transient storage. That compiler setting is a prerequisite, not evidence that a target network activated the opcodes. The pinned OpenZeppelin guard requires an EIP1153-enabled network. Verify the actual chain and build configuration before approving the deployment.

At Solidity v0.8.30, high-level transient state declarations are restricted to value types. Transient arrays, mappings, structures, local variables and parameters are not supported in that version. This restriction does not mean assembly cannot manage arbitrary transient slots. State the supported language version and access method rather than making a permanent claim about the language.

  1. Pin compiler, EVM target, network activation assumptions and the transient access implementation.
  2. Identify the storage owner on each direct and delegated path. List code that intentionally shares slots within that owner.
  3. Run an accepted protected call, then another accepted call in the same transaction. Assert cleanup before the second call.
  4. Exercise a same-owner recursive attempt and compare it with a direct call using a separate owner store.
  5. Test caught inner rollback with a nonzero pre-entry value, plus static read and write paths.
  6. Start a fresh transaction and verify transient reset while persistent business state retains its intended meaning.

Persistent and transient storage have independent address spaces in the cited language documentation. Reordering transient variables therefore does not change persistent layout. It can still change which transient field a slot refers to in shared execution, so delegated components and temporary-state conventions need a coordinated review.

Public transient variables receive getter functions in the pinned documentation. An external observation during a transaction can therefore participate in application behavior, while a separate later transaction sees the reset state. A getter's existence does not provide a historical record of the earlier temporary value.

Transient values follow the cited packing rules for persistent values. Custom assembly access must therefore match the intended slot and field representation when sharing state with high-level declarations. A successful whole-word write can alter more than the logical field an author intended to change. Review masks and offsets under the pinned layout before treating the access as an isolated temporary flag.

The upgrade review guide supplies the broader proxy release process. Include transient slot assumptions when an upgrade changes delegated components. A compatible persistent layout does not establish that temporary locks or approvals remain coordinated across the new call graph.

When evaluating a reviewer through the member directory or Pharos Production profile, name these execution tests in the scope. A gas optimization engagement that swaps storage primitives needs evidence for ownership, cleanup and rollback, alongside any performance measurement.

Choose transient storage from explicit assumptions

Choose the primitive after deciding whether the state belongs to a call, a transaction or a persistent application record. A transaction-lifetime lock can fit a protected workflow if its successful cleanup preserves composition. A durable nonce or configuration needs state that survives transaction boundaries. The primitive should follow the required lifetime.

Acceptance should preserve the owner map, intended clearing points, rollback assertions and network prerequisites. Document any value deliberately retained for a later call. If another component can read or write the same slot, name that relationship and test it; temporary lifetime does not create component isolation.

The model's counts show the boundary clearly: all next-transaction observations are zero, while successful same-transaction work can leave state behind. That does not prove a deployed lock is defective. It explains why a fresh-transaction test and a sequential-call test establish different properties.

The release decision belongs to the implemented call graph. Keep its assumptions explicit, then recheck them when a proxy implementation, router composition or compiler configuration changes. Automatic reset is one useful property of transient storage. Correct ownership and intentional cleanup still need engineering evidence.

Frequently asked questions

Can a transient variable be constant or immutable in the pinned compiler?

Solidity v0.8.30 documentation says the transient qualifier cannot be combined with constant or immutable. Check the documentation for the actual compiler version before selecting a declaration.

Can transient state variables be initialized at declaration?

Not in the pinned v0.8.30 documentation. They begin with their type's default value; intended transaction-time values must be written through the implementation's execution path.

Does moving a variable to transient storage eliminate its public naming conflict?

No. The cited documentation says transient and persistent variables have independent storage address spaces but still share a naming namespace. Review declaration and access changes separately.

Comments

0

    Leave a comment

    Share a question or observation about this article.

    10 to 3,000 characters.