evm-amm-state 0.2.0

EVM-backed AMM state loading, cache synchronization, and pool simulation models
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
# evm-amm-state

[![crates.io](https://img.shields.io/crates/v/evm-amm-state.svg)](https://crates.io/crates/evm-amm-state)
[![docs.rs](https://img.shields.io/docsrs/evm-amm-state)](https://docs.rs/evm-amm-state)
[![CI](https://github.com/KaiCode2/evm-amm-state/actions/workflows/ci.yml/badge.svg)](https://github.com/KaiCode2/evm-amm-state/actions/workflows/ci.yml)
[![license](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)
[![MSRV](https://img.shields.io/badge/MSRV-1.88-informational)](https://github.com/KaiCode2/evm-amm-state/blob/main/Cargo.toml)

`evm-amm-state` is a real-time AMM state engine built on a forked-EVM state
cache ([`evm-fork-cache`]). It tracks a working set of pools, **cold-starts**
their on-chain state into the cache, and keeps them current **from chain log
events**: events that carry absolute state (Uniswap V2 / Solidly `Sync`) are
applied as exact writes with **no RPC at all**, Uniswap V3 `Mint`/`Burn` and
Balancer vault `Swap`s are **event-sourced** onto warm tick / cash slots the
same way, and only genuinely cold slots and delta-only events (Curve, Balancer
joins/exits) turn into a bounded, hash-pinned storage **resync** (block trace
first, then bulk-storage / point-read fallback). Once a pool's quote read-set is warmed and current, swap
**simulations run fully offline** against the live-synced state.

The defining design choice: **no reimplemented AMM math.** Every quote runs the
protocol's *canonical* on-chain quote entrypoint inside a local revm against the
warmed cache — the pool's own `get_dy` / `getAmountOut`, or the protocol's
official router/quoter (Uniswap `QuoterV2` / `Router02`) — then decodes the
result. There is no `LocalAMM`/`amm-math` formula layer to drift from the real
contracts.

[`evm-fork-cache`]: https://github.com/KaiCode2/evm-fork-cache

## Installation

```bash
cargo add evm-amm-state
```

All five protocol adapters are enabled by default; trim to what you use with
feature flags:

```toml
[dependencies]
evm-amm-state = { version = "0.1", default-features = false, features = [
    "uniswap-v3",
    "curve",
    "live-runtime", # optional Tokio cache actor + Alloy subscriber driver
] }
```

Requires Rust **1.88+** (the declared MSRV, checked in CI). The two public
dependencies whose types appear in this crate's API are re-exported at the
crate root — import `evm_amm_state::evm_fork_cache` and
`evm_amm_state::alloy_primitives` instead of pinning them yourself, and the
versions always match. `evm-fork-cache` is a 0.x companion released in
lockstep: a breaking bump there is a breaking bump here.

## The pipeline

Each protocol is a single [`AmmAdapter`] implementation; the
[`AdapterRegistry`] dispatches by pool key.

| Stage | What it does |
| --- | --- |
| **Register** | Describe a pool: a [`PoolKey`] + [`ProtocolMetadata`] tokens, fee, storage layout / coins, . |
| **Cold-start** | `registry.cold_start(pool, cache, policy)` warms the pool's read-set into the [`EvmCache`] from forked storage. Named-slot protocols (Uniswap V2/V3, Solidly) warm known slots; layout-free protocols (Balancer, Curve) **discover → verify** the exact slots a quote call SLOADs. Known AMM runtime bytecodes can be seeded first and verified once by `evm-fork-cache` against on-chain `EXTCODEHASH`. |
| **Subscribe** | `adapter.event_sources(pool)` lists the log topics to subscribe to over a `wss://` endpoint. |
| **React** | Decoded logs flow through [`AmmSyncEngine`] / [`AmmReactiveHandler`] + the `evm_fork_cache` reactive runtime. Exact-write protocols update cached state with **no RPC**; protocols whose logs do not carry final storage values emit hash-pinned resync requests that `evm-fork-cache` resolves from block traces, bulk storage, or point-read fallback. |
| **Simulate** | `adapter.simulate_swap(pool, cache, token_in, token_out, amount_in, &config)` executes the pool's own quote against the cached state and returns a [`SwapQuote`] — fully offline. |

[`AmmAdapter`]: src/adapters/traits.rs
[`AdapterRegistry`]: src/adapters/registry.rs
[`AmmSyncEngine`]: src/adapters/sync_manager.rs
[`AmmReactiveHandler`]: src/adapters/reactive.rs
[`PoolKey`]: src/adapters/types.rs
[`ProtocolMetadata`]: src/adapters/types.rs
[`EvmCache`]: https://github.com/KaiCode2/evm-fork-cache
[`SwapQuote`]: src/adapters/sim.rs

## Supported protocols

| Protocol | Feature | Quote entrypoint | Cold-start | Reactive |
| --- | --- | --- | --- | --- |
| Uniswap V2 | `uniswap-v2` | `Router02.getAmountsOut` | named slots | `Sync` → exact masked write |
| Uniswap V3 family (V3, PancakeSwap V3, Slipstream) | `uniswap-v3` (`pancake-v3`, `slipstream`) | `QuoterV2.quoteExactInputSingle` | slot0 + liquidity + multi-word tick scan (per-pool radius), or the one-shot full-range program sync (`v3_sync`) | `Swap` → slot0/liquidity; `Mint`/`Burn` → exact tick + global-liquidity writes where warm, resync only cold ticks |
| Balancer V2 | `balancer-v2` | `Vault.queryBatchSwap` | discover → verify (`getPoolTokens`), verify-only once known | `Swap` → exact 112-bit cash writes where warm, resync fallback; `PoolBalanceChanged` → resync |
| Solidly V2 (Aerodrome / Velodrome) | `solidly-v2` | pool `getAmountOut` | named slots (config layout) | `Sync` → two exact slot writes |
| **Curve** (StableSwap, StableSwap-NG, CryptoSwap v2, Tricrypto-NG) | `curve` | pool `get_dy` | discover → verify (`get_dy` read-set) | `TokenExchange` + liquidity events → slot resync |

All protocol adapters are on by default; `pancake-v3` and `slipstream` are
thin aliases of `uniswap-v3` (one V3-family adapter serves all three). See
[`docs/protocol-support-matrix.md`](docs/protocol-support-matrix.md) for the
per-protocol capability matrix (offline-after-cold-start, exact-write vs resync,
discovery, and known limitations), and [`docs/curve-adapter.md`](docs/curve-adapter.md)
for the Curve adapter in depth.

> **Slipstream quoting caveat.** Slipstream / Aerodrome CL ships as
> discovery + cold-start: its own quoter ABI differs (int24 tickSpacing), so
> discovered registrations leave `fee` unset and `simulate_swap` returns
> `MissingMetadata` until you supply a Uniswap-compatible quoter + fee — see
> the [support matrix]docs/protocol-support-matrix.md.

> **Solidly offline caveat.** Solidly's `getAmountOut` reads more than the
> reserves its cold-start warms — the pool's `stable` flag and token `decimals`,
> plus an external `IPoolFactory(factory).getFee()` STATICCALL (which needs the
> factory's code and fee slots). With a live-backed cache these fetch lazily on
> the first quote; for fully-offline Solidly quotes, keep a backend attached or
> pre-warm that read-set. Uniswap V2/V3 and Curve cold-starts already cover their
> quote read-set (V3 within its warmed tick window).

### Verified Pool Bytecode Seeding

Known pool runtime bytecodes live in [`src/adapters/bytecodes`](src/adapters/bytecodes)
as embedded binary artifacts. Before cold-start, an adapter's `code_seeds` returns
canonical `AdapterCodeSeed`s built by plain functions: `uniswap_v2_pair_code_seed`
returns the shared pair runtime and its precomputed code hash, and
`v3_code_seed_from_metadata` renders the V3 pool template from a pool's immutables.
No per-pool bytecode fields are attached to metadata.

When the cache has an account-fields fetcher available (and seeding is enabled),
`AdapterRegistry::cold_start` writes these seeds into `EvmCache` with
`seed_account_code`; `evm-fork-cache` verifies the code hash once against the
chain. Seeding is a pure optimization: a pool's own runtime code is otherwise
lazily fetched at first simulate. A code-hash mismatch (or an unverifiable seed)
is purged and the pool falls back to lazily fetching its real code — it stays
`Ready`, is never left `Degraded`, and no repair is raised. Verification results
are surfaced on the cold-start report's `code_seeds` field. Disable seeding
entirely with `AdapterRegistry::with_code_seeding(false)`.

Uniswap V3 has an embedded pool template and an explicit `uniswap_v3_code_seed`
helper for callers that already know the pool immutables. Factory-discovered
Uniswap V3 registrations carry the factory immutable in metadata, allowing
automatic V3 seeding without assuming a chain-global factory address. Bytecode
seeding covers Uniswap V2 and the V3 family from embedded/rendered templates.
Balancer and Curve pools have no shared template, so by default they fetch their
runtime code lazily on first simulate — but **Curve accepts an optional
caller-supplied seed** via `CurveMetadata::with_code_seed(runtime)` for callers
that already know a pool's Vyper runtime (verified once against on-chain code,
same purge-on-mismatch contract). Since seeding is a pure optimization over that
lazy fetch, this is only a latency difference, never a correctness one.

### Factory-backed Discovery

`PoolDiscovery` can build cold-start-ready registrations from configured
factories instead of requiring callers to paste pool addresses. `FactoryConfig`
is empty by default: callers opt in with explicit factory addresses, e.g.
`FactoryConfig::default().with_uniswap_v3_factory(factory)`, so chain- and
fork-specific deployments never inherit an assumed factory.

Discovery ships for every protocol whose pools resolve through the pinned
cache as a derived factory storage slot, batched by default:

| Protocol | Mechanism |
| --- | --- |
| Uniswap V3 / Sushi V3 / Pancake V3 | fee-keyed `getPool[t0][t1][fee]` (via `ClFactorySpec`) |
| Slipstream / Aerodrome CL | tickSpacing-keyed `getPool[t0][t1][tickSpacing]` (via `ClFactorySpec`) |
| Uniswap V2 | `getPair[t0][t1]` |
| Solidly V2 (Aerodrome / Velodrome) | `getPool[t0][t1][bool stable]` (stable + volatile) |

V3-discovered registrations carry `V3Metadata.factory`, which gives bytecode
seeding the factory immutable it needs to render and verify the expected pool
runtime. Multiple factories of the same protocol coexist (keyed by
`(protocol, factory_address)`).

A few AMM shapes are deliberately left to the integrator in 0.1.0 — **Algebra**-style
CL forks (Camelot, QuickSwap: a different pool engine, not just a discovery
config), **Curve** (needs the Vyper MetaRegistry view call, not wired this
release — its adapter still simulates explicitly-registered pools), and
**Balancer V2** discovery (no on-chain token→pool index, so it needs an async log
scan). These have first-class, stable escape hatches:
`register_adapter(Arc<dyn AmmAdapter>)` adds a novel simulation engine and
`PoolDiscovery::with_factory(Box<dyn PoolFactory>)` adds a novel discovery
mechanism — a new AMM never requires forking the crate. See
[`docs/pool-discovery.md`](docs/pool-discovery.md) for the full coverage table,
adding your own CL fork, and this boundary in depth.

The whole surface is one method — `discovery.find(cache, query)` — over one
fluent `PoolQuery`:

```rust,ignore
// one token pair, across every matching factory (V2, V3, forks)
discovery.find(&mut cache, PoolQuery::pair(weth, usdc))?;

// every pool joining any pair of a token basket — the C(n, 2) combinations
discovery.find(&mut cache, PoolQuery::basket([weth, usdc, dai, wbtc]))?;

// an explicit set of pairs
discovery.find(&mut cache, PoolQuery::pairs([(weth, usdc), (weth, dai)]))?;

// scope any of the above to one protocol
discovery.find(&mut cache, PoolQuery::pair(weth, usdc).on(ProtocolId::UniswapV3))?;

// mix protocols across pairs in ONE batched read — e.g. some pairs only on V2,
// others only on V3 — with find_many
discovery.find_many(&mut cache, [
    PoolQuery::pairs([(weth, usdc)]).on(ProtocolId::UniswapV2),
    PoolQuery::basket([weth, dai, wbtc]).on(ProtocolId::UniswapV3),
])?;
```

An unscoped query spans **every** matching factory and resolves the whole thing
— all pairs, all factories, all V3 fee tiers — in a **single batched read**
(`AdapterCache::read_storage_slots`, one bulk `eth_call` on `EvmCache`), so
request count scales with the number of factories, not with pairs, fee tiers, or
basket size (a 5-token mainnet basket resolves 49 pools in one round-trip, ~20×
faster than per-pair scans; see
[`examples/token_basket_bench.rs`](examples/token_basket_bench.rs)). `.on(p)`
filters to protocol `p` and errors `DiscoveryError::MissingFactory(p)` when no
factory is registered for it (an unscoped query never does). External factories
that only implement `find_pools` (no `candidate_reads`) keep working through a
per-pair fallback.

### Fast multi-pool bootstrap

`AdapterRegistry::cold_start_many(pools, cache, provider, policy)` warms many
pools at once: it seeds + verifies all one-shot-eligible pools' code in one
account-fields call, hydrates them through a single bundled `run_storage_programs`
`eth_call` (V3 full-sync / V2 flat-slot / Balancer or **Curve** discovered
read-set), and finalizes them `Ready`, falling back per pool to the conservative
per-pool `cold_start` for anything without a one-shot program or whose hydration
fails. `supports_one_shot_hydration` reports which pools take the fast path — a
Curve pool qualifies once its `discovered_slots` read-set is known (from a prior
discovery, a trace, or a registry), joining V2/V3 in the same bundled call. Combined with token-basket discovery,
the happy path is `find(PoolQuery::basket(..)) → cold_start_many → register`,
with request count driven by bootstrap phases rather than pool count.

### Extending with a new AMM

You can add a brand-new AMM from *outside* the crate — no fork, no `src/` edit —
via the `Custom` protocol id / pool key / metadata hatches and a minimal
[`AmmAdapter`] (only `protocol()` + `simulate_swap()`). See the runnable,
self-contained demo [`examples/custom_adapter.rs`](examples/custom_adapter.rs)
(`cargo run --example custom_adapter`) and the guide
[`docs/writing-an-adapter.md`](docs/writing-an-adapter.md).

The V3 cold-start tick-scan radius is per-pool configurable via
`V3Metadata.warm_word_radius` (default ±2 tick-bitmap words).

## Quickstart

Register a pool, cold-start it into a forked cache, and simulate a swap entirely
offline once warmed:

```rust,no_run
use std::sync::Arc;

use alloy_eips::{BlockId, BlockNumberOrTag};
use alloy_network::AnyNetwork;
use alloy_primitives::{U256, address};
use alloy_provider::{Provider, RootProvider};
use evm_fork_cache::cache::EvmCache;
use evm_amm_state::adapters::{
    AdapterRegistry, AmmAdapter, ColdStartPolicy, PoolKey, PoolRegistration,
    ProtocolMetadata, SimConfig, ConcentratedLiquidityAdapter, V3Metadata,
};
use evm_amm_state::adapters::storage::V3StorageLayout;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let provider = Arc::new(RootProvider::<AnyNetwork>::connect("https://eth.llamarpc.com").await?);
    let block = provider.get_block_number().await?;
    let mut cache = EvmCache::at_block(provider, BlockId::Number(BlockNumberOrTag::Number(block))).await;

    let usdc = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
    let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
    let pool = address!("88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"); // USDC/WETH 0.05%

    let mut registry = AdapterRegistry::new();
    registry.register_adapter(Arc::new(ConcentratedLiquidityAdapter::default()))?;

    let mut reg = PoolRegistration::new(PoolKey::UniswapV3(pool))
        .with_state_address(pool)
        .with_metadata(ProtocolMetadata::UniswapV3(
            V3Metadata::default()
                .with_token0(usdc)
                .with_token1(weth)
                .with_fee(500)
                .with_tick_spacing(10)
                .with_storage_layout(V3StorageLayout::uniswap(10)),
        ));
    registry.cold_start(&mut reg, &mut cache, ColdStartPolicy::Eager)?;

    // Offline from here: no RPC needed to quote.
    let out = ConcentratedLiquidityAdapter::default().simulate_swap(
        &reg, &mut cache, usdc, weth, U256::from(1_000_000_u64), &SimConfig::default(),
    )?;
    println!("1 USDC -> {} WETH (raw)", out.amount_out);
    Ok(())
}
```

The full **cold-start → WebSocket subscribe → react → simulate** loop is in
[`examples/adapter_pipeline.rs`](examples/adapter_pipeline.rs):

```bash
ETH_WS_URL=wss://your-node cargo run --example adapter_pipeline
# or derive wss:// from an https endpoint:
E2E_RPC_URL=https://your-archive-node cargo run --example adapter_pipeline
```

Live sync should use [`AmmSyncEngine`] or, if wiring the upstream runtime
manually, `ReactiveRuntime::ingest_batch_with_resync`. Plain `ingest_batch`
decodes and applies direct writes only; it reports Balancer/Curve/V3 repair
requests without executing the trace/storage resync phase. The boundary and
fallback policy are documented in
[`docs/trace-backed-sync.md`](docs/trace-backed-sync.md).

The tracked working set changes transactionally without rebuilding the runtime.
`AmmSyncEngine::add_pools` / `remove_pools` return generation-scoped
`AmmLifecycleReport`s while preserving journals and unrelated pending work;
`register_pools` / `unregister_pools` remain compatibility projections. Pool
handlers use an exact upstream route index, so direct/indexed dispatch does not
scan unrelated pools. `remove_pools_evicting` is explicit and ownership-aware:
it purges only state exclusive to removed pools, sparing co-tenants such as the
Balancer vault, and fences retained rollback history so a late reorg cannot
restore an explicitly evicted generation. Teardown batches exact resync-ID
cancellation into one pending-queue pass. Adapter families have matching add,
default-rejecting remove, and explicit cascade APIs. See
[`docs/live-runtime-lifecycle.md`](docs/live-runtime-lifecycle.md).

With the opt-in `live-runtime` feature, `AmmRuntime` owns the thread-local cache
inside a Tokio `LocalSet` and publishes immutable `Send + Sync` snapshots to
search workers. Startup is anchored by a sealed full header; zero-log blocks,
degradation, and reorg replacements remain version-coherent. The attached Alloy
driver stages generation-owned subscriptions before topology publication and
hash-pins a bounded union of `eth_getLogs` filters at every header. Reliable
changes, latest snapshot/status watches, lossy diagnostics, bounded queues,
typed backpressure, and prompt shutdown are described in
[`docs/live-runtime-cache-actor.md`](docs/live-runtime-cache-actor.md).

```mermaid
flowchart LR
    A["AMM log"] --> B["Adapter decode"]
    B --> C{"Event carries final values?"}
    C -->|yes| D["StateUpdate directly into EvmCache"]
    C -->|no| E["ResyncRequest for known slots"]
    E --> F["evm-fork-cache: block trace first"]
    F --> G["bulk storage / point fallback if unresolved"]
    D --> H["post-block cached state"]
    G --> H
```

### Arbitrage examples

Two end-to-end examples show the canonical use case — pricing swaps across many
pools offline to find and size a dislocation. Both warm real pools from an
archive node, then quote entirely offline:

```bash
# Cross-DEX: quote USDC/WETH across Uniswap V2, V3, and Curve; find the best
# buy + sell venue and price the round trip.
E2E_RPC_URL=<archive-url> cargo run --example arbitrage_cross_dex
# Triangular: walk USDC -> USDT (Curve 3pool) -> WETH (Curve tricrypto2)
# -> USDC (Uniswap V3) and price the cycle.
E2E_RPC_URL=<archive-url> cargo run --example arbitrage_triangular
```

See [`examples/arbitrage_cross_dex.rs`](examples/arbitrage_cross_dex.rs) and
[`examples/arbitrage_triangular.rs`](examples/arbitrage_triangular.rs).

### One-shot V3 full-pool sync

The [`v3_sync`](src/adapters/v3_sync.rs) module generates a ~360-byte EVM
program (tick spacing and storage layout baked in as immediates) that is
injected over a pool's code via an `eth_call` state override
([`evm-fork-cache`'s bulk-storage transport]) and walks the **entire** tick
bitmap *inside the EVM* — returning statics, every initialized tick's four
info words, and the whole observation ring in **one call with zero calldata**.
Live-measured on the USDC/WETH 0.05% pool: 1,563 ticks + 723 observations →
7,674 slots injected in ~140 ms for 26 CU (vs ~130k CU as per-slot point
reads — 7,674 × 17 CU), after
which a hard multi-tick-crossing quote runs in **~5 ms with zero lazy
fetches** (vs ~2 s paging ticks over RPC on a windowed cache). A
calldata-driven **partial** variant refreshes selected bitmap-word ranges
(the planner-window shape, or dense spacing-1 pools in chunks).

```bash
E2E_RPC_URL=<https endpoint> cargo run --release --example v3_full_sync
# live parity suite (state, quote, and partial-window equivalence):
E2E_RPC_URL=<archive-url> cargo test --test v3_full_sync_rpc -- --ignored
```

[`evm-fork-cache`'s bulk-storage transport]: https://github.com/KaiCode2/evm-fork-cache/blob/main/docs/bulk-storage-extraction.md

### One-shot flat-slot sync

The [`storage_sync`](src/adapters/storage_sync.rs) module covers adapters whose
hot state is already known as a slot list:

- **Uniswap V2**: canonical `token0`, `token1`, and packed reserves slots.
- **Solidly V2**: config-supplied reserve/token slot layout.
- **Balancer V2**: vault balance slots discovered by cold-start or trace data.
- **Curve**: pool read-set slots discovered by cold-start or trace data.

Small fixed layouts use generated no-calldata bytecode with slots baked in.
Larger discovered read-sets use a reusable calldata-driven loader. Both execute
through the same `eth_call` code-override transport as V3 and inject the returned
storage words into `EvmCache`.

One-shot syncs (V3 and flat-slot) **warm the cache only** — pool status and
metadata still come from `registry.cold_start`, and the Balancer/Curve loaders
require a previously discovered read-set.

```bash
E2E_RPC_URL=<https endpoint> SYNC_BENCH_ITERS=7 cargo run --release --example sync_latency
```

Live paid Alchemy RPC measurements on July 2, 2026, showed Balancer and Curve
refreshes dropping from discover→verify cold-starts around 330–360 ms to
one-shot storage programs around 75–76 ms once their read-set metadata is known.
The same run measured Uniswap V3 full-pool loading at 124.8 ms, while loading
7,670 slots. See
[`docs/benchmarks.md`](docs/benchmarks.md) for the full table and caveats.

The progressive live-runtime roadmap, including lifecycle/version/backfill
semantics and the stage-by-stage implementation gates, is documented in
[`docs/live-runtime-master-plan.md`](docs/live-runtime-master-plan.md). The
corresponding reproducible measurement contract and commands live in
[`docs/live-runtime-baselines.md`](docs/live-runtime-baselines.md). The public
identity, lifecycle, state-point, change-set, observer-event, and typed
compatibility-report contract is summarized in
[`docs/live-runtime-domain.md`](docs/live-runtime-domain.md). Stage 2's
generation-scoped pool handlers, shared-emitter routing, state/job/resync
ownership indexes, isolation guarantees, and routing benchmarks are documented
in [`docs/live-runtime-ownership.md`](docs/live-runtime-ownership.md).
Stage 4's cache actor, complete-block envelope, subscriber transactions,
publication channels, and responsiveness gates are documented in
[`docs/live-runtime-cache-actor.md`](docs/live-runtime-cache-actor.md).
Stage 5's non-blocking cold-start worker, exact-hash prepared artifacts,
generation-safe cancellation, resumable round progress, priority scheduling,
independent pool publication, and Stage 6's background discovery/repair,
capacity-one handoffs, deferred warming, and dynamic adapter/factory ownership
are documented in
[`docs/live-runtime-progressive-cold-start.md`](docs/live-runtime-progressive-cold-start.md).

Stage 9 adds `AmmRegistrationArchive`, a versioned chain-scoped sidecar for
built-in pool registrations and reusable Curve/Balancer read-set hints. Restored
queueable records are always `Pending` and must pass the current runtime's
hash-pinned cold-start verification before publication; generic EVM state stays
in `evm-fork-cache`. The deterministic, bounded file is written by same-directory
temporary file plus atomic rename. See
[`docs/warm-resume.md`](docs/warm-resume.md).

The archive's atomic rename protects that file alone. Applications that resume
ready pools from a persisted `EvmCache` must commit cache files, registration
archive, and hash-certified checkpoint as one application-level generation.
Publish a small manifest only after every generation file is flushed and synced;
otherwise a crash can pair a newer cache with an older valid checkpoint.

For event-time repair specifically, [`examples/trace_resync_latency.rs`](examples/trace_resync_latency.rs)
compares a real Curve event through `AmmSyncEngine` with trace-only resync versus
forced storage-fetch fallback:

```bash
E2E_RPC_URL=<https endpoint> TRACE_RESYNC_ITERS=3 cargo run --release --example trace_resync_latency
```

On the same paid Alchemy endpoint, a single Curve 3pool event measured 155.7 ms
through trace-only resync versus 26.7 ms through direct storage fallback; the
trace path is expected to amortize across many stale slots in the same block.

## Performance

Once warmed, a quote is a fully-offline revm execution of the pool's own quote
entrypoint: **~8–9 µs** for constant-product pools (Uniswap V2, Solidly) and
**~40–85 µs** for the heavier Curve / Balancer / Uniswap V3 quotes, on an M1 Pro.
Applying a `Sync` event is **~250 ns**. That is `eth_call`-grade correctness
(it runs the real bytecode) at roughly **1000× lower latency than an RPC
`eth_call`**, with no reimplemented math to drift. Full methodology, the
per-protocol table, and a comparison to other approaches are in
[`docs/benchmarks.md`](docs/benchmarks.md); reproduce with
`E2E_RPC_URL=<archive-url> cargo bench --bench swap_sim`.

## Crate boundaries

This crate owns generic AMM state loading, event-driven synchronization, and
offline swap simulation. It deliberately does **not** contain transaction
signing/broadcasting, strategy scheduling, or multi-leg arbitrage routing (the
legacy routing layer was removed; it is rebuildable on top of `simulate_swap` —
the arbitrage examples above show exactly that).
Standard view interfaces are declared locally with `alloy_sol_types::sol!`, so
the crate builds from source with no generated bindings crate.

## Examples

**Start here — zero setup, no RPC:** `cargo run --example custom_adapter`
(defines a novel AMM outside the crate, registers it, quotes both directions).
Everything else is env-gated and prints a skip message when unset:

| Example | Shows | Needs |
| --- | --- | --- |
| [`custom_adapter`]examples/custom_adapter.rs | third-party adapter, register → quote ||
| [`adapter_pipeline`]examples/adapter_pipeline.rs | register → cold-start → WS react → quote | `ETH_WS_URL` or `E2E_RPC_URL` |
| [`factory_discovery_live`]examples/factory_discovery_live.rs | discovery → cold-start → reactive | `E2E_RPC_URL` |
| [`declarative_discovery`]examples/declarative_discovery.rs | token-basket `PoolQuery``cold_start_many` | `E2E_RPC_URL` |
| [`token_basket_bench`]examples/token_basket_bench.rs | batched vs per-pair discovery timing | `E2E_RPC_URL` |
| [`v3_full_sync`]examples/v3_full_sync.rs | one-shot full-pool V3 sync + quote parity | `E2E_RPC_URL` |
| [`verified_bytecode_seed`]examples/verified_bytecode_seed.rs | seeding + on-chain code-hash verification | `E2E_RPC_URL` |
| [`sync_latency`]examples/sync_latency.rs | prior vs one-shot sync latency per protocol | `E2E_RPC_URL` (public fallback) |
| [`curve_cold_start_phases`]examples/curve_cold_start_phases.rs | Curve discovery vs verify-only vs bundled | `E2E_RPC_URL` (public fallback) |
| [`trace_resync_latency`]examples/trace_resync_latency.rs | event-time trace resync vs storage fallback | `E2E_RPC_URL` |
| [`arbitrage_cross_dex`]examples/arbitrage_cross_dex.rs | offline cross-DEX round-trip pricing | `E2E_RPC_URL` (archive) |
| [`arbitrage_triangular`]examples/arbitrage_triangular.rs | offline triangular cycle pricing | `E2E_RPC_URL` (archive) |

## Testing

```bash
cargo test                       # unit + offline integration tests
cargo test --no-default-features # protocol-neutral core
```

These run from a clone of the repository. The published crate **excludes the
integration test suite** (`tests/`) to stay lean, so `cargo test` on a crates.io
download exercises only the inline unit tests — clone the repo for the full
suite.

Network-dependent tests are env-gated and `#[ignore]`d. With an archive RPC they
pin a block, cold-start a real pool, and assert `simulate_swap` **equals the
on-chain quote** at the same block (`eth_call`), plus a live WebSocket soak that
keeps state in sync from events only:

```bash
# RPC parity (mainnet pools; Base for Solidly via host-swap):
E2E_RPC_URL=<archive-url> cargo test --test adapter_swap_sim_rpc -- --ignored
# Live WS soak (Uniswap V2, and Curve across all dialects):
E2E_RPC_URL=<archive-url> cargo test --test reactive_ws_e2e --test reactive_curve_ws_e2e -- --ignored --nocapture
```

## License

Licensed under either of

- Apache License, Version 2.0 ([LICENSE-APACHE]LICENSE-APACHE)
- MIT license ([LICENSE-MIT]LICENSE-MIT)

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.