← Research

Transient Storage in the Wild

A measurement of EIP-1153 misuse across deployed contracts, and an open-source detector.

Michael Ross · Nexus Trinity · July 2026 · updated July 2026 with Arbitrum data and an EIP-7702 extension

The short version

  • Transient storage (TSTORE/TLOAD, EIP-1153) is cleared at the end of the transaction, not the call frame. That one fact has already cost real money — the SIR.trading hack (~$355k, March 2025) came from a transient slot that was reused and never cleared.
  • I built a detector for the misuse patterns and ran it across the Uniswap V4 ecosystem — first Ethereum, then extended to Arbitrum. 241 of 1,756 unique V4 hooks across both chains (13.7%) use transient storage.
  • Across those hooks and four audited reference protocols, on either chain: zero high-confidence misuse. The ecosystem largely gets this right. The one dangerous cross-callback pattern found — Bunni's rebalance hook — appears on both chains and is correctly guarded every time.
  • The contribution is the data and the tool, not the concept. The bug class is known; how common it is, and where, was not. The detector is MIT-licensed and reproducible.

The lesson that cost $355k

On 30 March 2025, SIR.trading lost about $355,000. The root cause was a transient storage slot that was used to pass a pool and an amount into a callback, reused for two different values, and never cleared. Because transient storage survives for the whole transaction, an attacker could make the callback read a slot the contract believed was fresh.

That is the whole hazard in one sentence: transient storage is not memory. Memory dies when the call frame returns. A transient slot set in one call is still there in the next call of the same transaction — including calls into, or callbacks from, untrusted code.

Why it's a footgun

EIP-1153 and the Solidity docs both warn about the transaction-scoped lifetime. ChainSecurity documented the low-gas reentrancy angle. ERC-7562 warns that in ERC-4337 bundles, transient storage leaks across UserOperations from unrelated senders. The class is well understood by the people who write these standards.

Dedaub also measured prevalence, earlier and broader: their October 2024 study scanned all deployed contracts as of block 20,129,223 and found ~250 with transient storage, broken down by pattern (reentrancy guards over 50%, auth checks 8.3%, cross-call context 6%) and by gas savings (91.6% average versus regular storage). That predates this piece by over a year and is worth reading for the adoption-rate angle. What it could not cover — because it had not happened yet — is the SIR.trading hack (March 2025), which is the actual reason misuse patterns, not just adoption numbers, matter. This piece narrows the lens to one ecosystem (Uniswap V4 hooks), goes after specific dangerous shapes rather than a use-case census, and is grounded in the one real incident the class has produced so far.

What still is not documented, even accounting for Dedaub's work, is misuse: of the contracts using transient storage today, how many do it in a shape that could actually be exploited? You cannot answer that from an adoption count. You answer it with a scanner built around the specific failure mode.

The detector

The tool works on Solidity source (no compiler required, so it runs on flattened Etherscan output) and on raw bytecode for a presence signal. It reports candidates in tiers — it triages, it does not pronounce verdicts:

TierNameWhat it flags
P4 · highUNCLEARED_AUTHa slot read in a require/auth check, never cleared — the SIR.trading shape
P5 · reviewCROSS_CALLBACK_VALUEa slot written in one function, read in another that moves value, uncleared
P3 · reviewSLOT_REUSEone slot written with two distinct non-boolean values
P1 · reviewUNCLEARED_SLOTa specific named slot written with no zero-clear found
P2 · reviewCALL_IN_WINDOWan external call while a transient slot is live

Two design decisions do most of the precision work. false and address(0) count as clears (a boolean lock cleared with false is cleared). Read-modify-write accumulators — a slot both loaded and stored in one function, the way a counter settles back to zero — are treated as self-managing and not reported. And flags only fire on specific slots (named constants or literals), never on generic accessor libraries where the slot is a function parameter.

It was built against real code, and real code found the bugs in it

The honest part. The first version screamed false positives at Uniswap V4. It flagged V4's Lock library as “uncleared” because the lock is cleared with tstore(IS_UNLOCKED_SLOT, false) — and the detector only treated a literal 0 as a clear. false is zero. It flagged Balancer V3's generic typed-slot accessor library on every setter, because tstore(slot, value) with both as parameters looks like an uncleared write.

A detector that cries wolf at the reference implementations is worthless, so those were caught and fixed before any measurement. That iteration against live code is the point, not an embarrassment to hide.

Results

Reference implementations. Four heavily-audited transient-storage users — V4 PoolManager, Balancer V3 Vault, V4 PositionManager, Universal Router — produce zero high-confidence hits. The few review-tier slots (e.g. V4's RESERVES_OF_SLOT) are left un-zeroed by design: reads are gated behind a companion CURRENCY_SLOT that is reset, so a stale reserve is dead data. That is what the review tier is for — the tool makes a human look, and the human clears it.

Prevalence, per chain. Enumerating every Initialize event on each chain's V4 PoolManager — a different, real contract address per chain, not a CREATE2 singleton with one address everywhere — yields:

ChainHooks enumeratedUse transient storageVerified source
Ethereum mainnet1,021168 (16%)148
Arbitrum73573 (10%)24
Combined1,756241 (13.7%)172

The lower share on Arbitrum is consistent with it being a younger, still-growing hook population relative to Ethereum's.

The hunt. Re-scanning every retrievable hook source, on both chains, for the cross-callback value-flow pattern returns exactly one contract family — appearing three times: Bunni's rebalance hook, once on Ethereum and twice on Arbitrum (two separate deployments there). It stashes the output-token balance before a rebalance order and reads it back afterward — through transient storage, never cleared. It is the pattern. It is also not exploitable, in any of the three instances: the pre-hook requires a matching order hash, the post-hook requires msg.sender == floodPlain and is nonReentrant, and the two run paired within a single settlement. A well-guarded instance of a dangerous pattern — guarded identically on a second chain, which suggests it's guarded by design in the shared codebase, not by chain-specific luck.

So: no exploitable transient-storage bug in the V4 hook population on either chain measured. That is a real result. The ecosystem — reference contracts and the hooks built on them — largely handles transient storage carefully, and that carefulness travels with the code across chains.

A third chain, attempted and disclosed. Base was next on the list — same V4 hook architecture, a real deployed PoolManager. But Base is gated to a paid Etherscan API tier for exactly the endpoints this measurement leans on hardest (log search, raw JSON-RPC, contract-creation lookup); only verified-source lookup works on the free tier used here. Falling back to Base's own public RPC for log-scanning worked in principle — binary-searching eth_getCode pinned the PoolManager's exact deploy block with no key needed — but the public endpoint proved too unreliable to complete the ~2,400-call scan this required: intermittent server errors on arbitrary block ranges, not just near the chain tip, eventually wedging a connection entirely over a multi-hour attempt. Rather than publish a partial, silently-incomplete Base number, it is left out here entirely. A real limitation of doing this for free against a live chain, not a hidden gap.

What “done right” looks like

  • Guard the read, not just the slot. V4 leaves RESERVES_OF_SLOT un-zeroed but zeroes a companion CURRENCY_SLOT; a reserve is only trusted while the currency is synced. Cheaper than clearing every slot, and safe.
  • Pair and authorize the callback. Bunni's before/after hooks are individually access-controlled and only ever run as a matched pair inside one settlement, so the transient value can't be read stale or by an attacker.

Extension: EIP-7702 delegation targets

EIP-7702 (live since Pectra) lets an EOA point itself at a contract's code via a delegation designator. That makes 7702 delegation targets a new, security-critical category worth the same check: do they use transient storage, and is it guarded? This was a small spot-check against a handful of publicly-known implementations, not an exhaustive scan of the category.

Two results worth recording honestly. Ambire's and OpenFort's README-listed addresses carry no deployed code on mainnet — dead ends, not zero-usage measurements (OpenFort's real implementation turned out to only exist on Sepolia, found via its Foundry broadcast records rather than its README).

Ithaca's Porto account implementation, Base mainnet (0x664ab8c20b629422f5398e58ff8989e68b26a4e6), uses transient storage: one TSTORE, one TLOAD. Pulling the verified source showed this isn't Porto-specific logic — it's Solady's published EIP7702Proxy.sol, a generic relay-proxy pattern meant to be reused across different 7702 wallet implementations. Tracing the actual usage: the transient slot is a one-shot “please initialize” handshake between a delegatecalled implementation and the relay's fallback. Because it's set from inside a delegatecall (same address, same transient-storage context — not a real inter-contract call), and read-then-cleared in the same statement immediately after, there is no window for the classic set-external-call-stale-read shape this piece is built around. Correctly guarded.

Finding that took fixing a bug in this project's own tool first: the scanner always called Etherscan's code-lookup endpoint, which this API plan blocks for Base specifically — every Base lookup was silently coming back as a false 0-byte result until it was routed through an RPC fallback that already existed in the codebase but had never been wired up. Caught by cross-checking against a raw eth_getCode call and a known-good contract, rather than accepting a suspicious zero at face value — the same verify-before-reporting discipline the rest of this piece argues for.

Net result: one confirmed, correctly-guarded transient-storage use in a shared, widely reusable piece of 7702 wallet infrastructure. Not a vulnerability, and not (on this limited check) a pattern that needs fixing — but a real, live example worth having on record as this category grows.

Guidance

  • Clear every specific transient slot before the frame exits, or prove the read is guarded.
  • Never reuse one slot for two meanings.
  • Treat false / address(0) as the clear; treat a slot read in an auth check as sensitive.
  • In ERC-4337, assume the next UserOperation in the bundle shares your transient storage.
  • In EIP-7702 relay proxies, a transient flag set from inside a delegatecall to signal the caller is safe by construction (same address, same transient storage) — the risk model that matters is a real external call, not a delegatecall.

Taking it back to the EIP itself

This research is also the basis of an open pull request against the EIP-1153 specification itself, proposing to add the SIR.trading incident to its Security Considerations section as a concrete, real-world example of the hazard that section already warns about.

It's contested, and that's worth saying plainly rather than glossing over. A reviewer pushed back that SIR.trading's root cause — a transient slot reused for two unrelated values — isn't the same failure as the classic reentrancy-lock pattern the section was originally written around, and said they would not approve the edit as first framed. That's a fair distinction. The PR was revised to present it as a second, related example — slot aliasing, not reentrancy — rather than a restatement of the existing one.

One more thing worth being straight about: a search turned up no precedent for a Final EIP being amended after finalization to cite a named real-world incident. If this merges, it would be a first. It hasn't merged yet — review is still open, and that's the editors' call to make, not a claim to make ahead of it.

The tool & ethics

Open-source (MIT), ~300 lines, zero dependencies — no solc, no requests, no third-party SDK. The opcode walker, paren-depth body scanner, and Etherscan client are hand-written against the raw EVM and Solidity source, not bolted onto an existing framework. Ships with ground-truth fixtures, including a minimal reproduction of the SIR.trading bug class, and a self-test that runs the detector against them before it runs against anything real. Every corpus contract came from public, verified on-chain source; nothing live was probed or transacted against. The single P5 hit — Bunni's rebalance hook, above — was traced to its guard and confirmed non-exploitable. Had it not been, it would have gone to the team privately and been reproduced on a fork before any of this was published.

Reproduce the research

Verify it yourself

The detector, harvesting pipeline, and ground-truth fixtures are public on GitHub — MIT-licensed, Python standard library only. Clone it, run the self-test, and reproduce the numbers above.

Want this kind of look at your contracts?

This is the depth Nexus Trinity brings to a review — read the code that holds funds, measure it against reality, and prove what's wrong before someone else does.

Request a review
✉️Email us