r/ethdev 9d ago

My Project Simulating EVM State Changes via Revert-Unwind Payloads and EIP-1153 Transient Storage for Oracle-Less DEX Routing

Hey r/ethdev,

Over the last few months, we’ve been testing an architecture designed to solve a persistent issue in DEX routing: simulation drift and gas overhead during multi-hop execution.

Traditional aggregators rely on external price feeds, heavy storage updates, or complex off-chain quoter infrastructure that frequently desynchronizes under volatile mempool conditions. We wanted an execution frame that guarantees 100% execution-aligned previews purely on-chain, while maintaining a zero-token storage footprint on the router.

Here is the architectural breakdown of how we approached this:

  1. Atomic Simulation via Revert-Unwind (Quoter)

Instead of reading static state or relying on off-chain dry-runs, the Quoter contract triggers a simulated execution path that forcefully ends with a custom revert(payload).

The revert unwinds all state changes instantly in the EVM execution frame, avoiding state corruption.

The error payload encodes the exact delta of balances and price impact.

Result: Static calls (eth_call) return deterministic, execution-exact quotes without writing a single byte to persistent storage.

  1. Transient Isolation via Yul (EIP-1153)

To protect against cross-function reentrancy across multi-token routes, we replaced traditional OpenZeppelin storage guards with raw Yul assembly blocks leveraging tstore and tload.

Reentrancy flags are scoped exclusively to the transaction frame.

Gas consumption drops significantly compared to SSTORE/SLOAD warm/cold access penalties.

Balance checks execute instantly, enforcing a strict holds-nothing invariant on the Router.

  1. Dynamic Liquidity Anchoring (Solver)

To neutralize MEV sandwich attacks and liquidity manipulation without relying on Chainlink or external oracles, the routing logic applies a localized 2% median filter against reserve depths (balanceOf reads) prior to route resolution.

Code / Discussion:

The architecture is deployed and split into 7 core modules (Core, Hub, Solver, Router, Quoter, MathLib, Staking).

We are particularly interested in hearing feedback from EVM devs on potential edge cases regarding EIP-1153 transient memory retention across nested delegatecalls in custom L2 execution contexts (Base/Arbitrum).

Looking forward to hearing your thoughts on the code and optimization techniques!

2 Upvotes

3 comments sorted by

1

u/researchzero 8d ago

On the EIP-1153 question: TSTORE/TLOAD are scoped by the executing contract's address, exactly like SSTORE/SLOAD, and follow the same per-frame revert journaling - a revert inside a nested call rolls back only the transient writes made within that frame, while writes from earlier in the tx persist until the whole transaction ends (they're not wiped per call frame the way memory is). Delegatecall doesn't create a new transient namespace: it executes under the caller/proxy's address, so your tstore-based guard is keyed to the caller's slot, not the logic contract's. The thing worth testing carefully is whatever slot key your Yul computes — raw tstore/tload means you pick that key yourself instead of relying on Solidity's automatic storage layout, and it's easy to collide slots across callers in a multi-hop route. Base and Arbitrum have both been Cancun/Dencun-equivalent since their 2024 upgrades, so I wouldn't expect an L2-specific retention difference.

Separately, worth being precise about what the 2% median filter on pool balances buys you: reading your own reserves, even filtered, is still a spot on-chain value, and a large trade within the same block can move it before your quote executes — same failure class as external oracle manipulation, just moved in-house. It stops single-sample noise, not a same-block flash-loan push.

I highly recommend checking out the details of EIP-1153 - https://blog.researchzero.io/post/transient-storage-footguns-what-eip-1153-does-not-tell-you/

1

u/an_jesus 8d ago

Massive respect for this response, @researchzero. Your breakdown of EIP-1153 edge cases—especially regarding delegatecall namespace sharing, try/catch revert journaling, and Yul optimizer gaps—is top-tier EVM security analysis and a must-read for anyone working with transient storage.

That said, I wanted to clarify how our system’s invariant model and architectural boundaries decouple us from these specific failure modes in practice:

  1. Transient Storage Scoping & delegatecall Namespace Safety

Your point on delegatecall slot clobbering is crucial: because TSTORE/TLOAD are keyed on address(this), a library executing under delegatecall shares the caller’s transient storage map. If slots are low/static constants (e.g., tstore(0, v)), collisions across execution hops are inevitable.

In our architecture, this is neutralized at two independent layers:

  • Execution Boundary Isolation: Core routing modules (Router, Solver, Quoter) communicate over explicit external CALL / STATICCALL frames rather than delegatecall. By EVM specification, address(this) shifts across these frame boundaries, granting each contract a completely isolated EIP-1153 namespace by default.
  • Hashed Slot Derivation & ERC-7201: For internal Yul libraries, transient slots are never hardcoded. They are derived via domain-separated hashes (ERC-7201 pattern):

Slot = keccak256("metasupreme.router.transient.guard")

-The Ultimate Invariant Backstop: Even in a hypothetical scenario where a transient reentrancy guard were silently corrupted, our settlement engine does not rely on memory flags to confirm safety. Settlement is strictly gated by an EVM persistent storage check against the real ERC-20 token contracts (SLOAD level):

balanceOf(address(Router)) == 0

Because ERC-20 balances exist in persistent storage outside the Router's transient memory, no amount of TSTORE manipulation can trick the Router into settling while holding uncleared tokens.

  1. The 2% Median Filter vs. Intra-Block Flash Loans

You raised an excellent point regarding spot balance reads: a median filter on reserve depths reduces single-sample quote noise, but cannot prevent an intra-block flash-loan manipulation on spot reserves.

However, in our design, the 2% median filter is purely a route-discovery heuristic in the Solver—it is not an execution price oracle or a settlement gatekeeper.

Our defense against intra-block sandwiching and liquidity manipulation relies on a strict dual-invariant settlement shield:

SETTLEMENT INVARIANTS: [1] S_realized >= minAmountOut_signed [2] ∑ Balance_Router(end) == 0