tycho_simulation/evm/override_stream/mod.rs
1//! Generic, protocol-agnostic core for injecting live per-block VM state overrides into pools.
2//!
3//! Some protocols (e.g. pAMMs such as FermiSwap) rely on off-chain oracle prices that are not part
4//! of the on-chain state Tycho indexes, and that change *within* a block. To simulate them
5//! accurately, a side channel publishes per-block VM storage overrides and an overridden block
6//! environment that must be reflected by the pool on every simulation — not just once per Tycho
7//! block.
8//!
9//! This module defines the generic plumbing:
10//! - [`OverrideSnapshot`](crate::evm::override_stream::OverrideSnapshot) — the resolved overrides
11//! for one protocol at a point in time.
12//! - [`StateOverrideProvider`](crate::evm::override_stream::StateOverrideProvider) — a source that
13//! maintains a *live* [`watch`](tokio::sync::watch) channel of snapshots per protocol (kept fresh
14//! in the background, e.g. from a WebSocket).
15//! - [`FailurePolicy`](crate::evm::override_stream::FailurePolicy) — the provider-set reaction of a
16//! pool to a simulation that fails while a snapshot's overrides are applied.
17//!
18//! Providers are registered per `protocol_system` (see
19//! [`ProtocolStreamBuilder::with_override_provider`](crate::evm::stream::ProtocolStreamBuilder::with_override_provider)).
20//! A pool subscribes to the provider registered for its protocol at creation time and reads the
21//! freshest snapshot at simulation time.
22//!
23//! It deliberately knows nothing about any specific venue or stream format; all such specifics live
24//! in the concrete provider implementations (see [`titan`](crate::evm::override_stream::titan)).
25
26pub mod titan;
27
28use std::{collections::HashMap, sync::Arc};
29
30use alloy::primitives::{Address, U256};
31use tokio::sync::watch;
32
33/// What a pool should do when a simulation fails while a snapshot's overrides are applied.
34///
35/// Set by the provider, which knows whether its overrides are authoritative or an advisory
36/// enhancement of the indexed state.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum FailurePolicy {
39 /// Propagate the failure to the caller.
40 #[default]
41 Error,
42 /// Retry the failed simulation without any overrides, on the plain indexed state.
43 ///
44 /// Suits providers whose overrides are only intermittently applicable — e.g. Titan pAMM
45 /// snapshots, which target the pending block and can make a pair transiently unquotable
46 /// (oracle lane not yet re-stamped for that block) even though the indexed state simulates
47 /// fine.
48 FallbackToIndexedState,
49}
50
51/// Resolved per-block VM overrides for a single protocol at a point in time.
52///
53/// `block_number` and `block_timestamp` are already resolved by the provider (e.g. Titan derives
54/// `block_timestamp` from the frame's beacon slot) so this core stays protocol-agnostic. Either
55/// field may be `None`, in which case the pool's existing block environment is left intact.
56///
57/// The struct is `#[non_exhaustive]` so fields can be added without breaking downstream
58/// providers; construct it outside this crate by mutating a [`Default::default`] value.
59#[derive(Clone, Default, Debug)]
60#[non_exhaustive]
61pub struct OverrideSnapshot {
62 /// The L1 block number these overrides apply to, if known.
63 pub block_number: Option<u64>,
64 /// The block timestamp to inject into the pool's simulation environment, if known.
65 pub block_timestamp: Option<u64>,
66 /// Storage overrides keyed by contract address, then by storage slot. `Arc`-wrapped so a pool
67 /// can read the latest snapshot on every simulation with a refcount bump rather than a deep
68 /// clone of the nested map.
69 pub storage: Arc<HashMap<Address, HashMap<U256, U256>>>,
70 /// Unix-seconds instant after which these overrides are stale and must not be used. Set by the
71 /// provider from its own freshness rules (e.g. Titan uses the quote timestamp plus one block
72 /// time). `None` means the snapshot never expires — e.g. the empty initial snapshot.
73 pub expires_at: Option<u64>,
74 /// How a pool should react when a simulation fails with these overrides applied.
75 pub failure_policy: FailurePolicy,
76}
77
78impl OverrideSnapshot {
79 /// Returns whether these overrides are stale at `now` (unix seconds).
80 ///
81 /// A snapshot without an [`expires_at`](Self::expires_at) never expires. Callers that read the
82 /// snapshot on every simulation should skip it entirely once this returns `true`, falling back
83 /// to whatever state they held before.
84 pub fn is_expired(&self, now: u64) -> bool {
85 self.expires_at
86 .is_some_and(|expires_at| now >= expires_at)
87 }
88}
89
90/// Supplies a *live* stream of resolved per-block VM overrides for one or more protocols.
91///
92/// Implementations maintain their snapshots in the background (e.g. from a WebSocket stream) and
93/// expose them through a [`watch`] channel per protocol. A single provider may serve several
94/// protocols sharing one underlying connection; the same provider can be registered for each of
95/// them (it is shared via `Arc`, not duplicated).
96pub trait StateOverrideProvider: Send + Sync {
97 /// Returns the live override channel for `protocol_system`, or `None` if unsupported.
98 ///
99 /// The returned [`watch::Receiver`] always reflects the latest snapshot; reading it
100 /// ([`watch::Receiver::borrow`]) never blocks, so a pool may read it on every simulation.
101 fn subscribe(&self, protocol_system: &str) -> Option<watch::Receiver<OverrideSnapshot>>;
102}
103
104/// The default registry of built-in override providers, keyed by `protocol_system`.
105///
106/// `protocols` yields the protocol systems the caller wants a built-in default for (i.e. registered
107/// exchanges minus any the consumer explicitly overrode — the builder computes that difference and
108/// forwards it as an owned iterator, avoiding clones). This is the single place that wires concrete
109/// providers (e.g. the Titan pAMM stream) into the otherwise protocol-agnostic stream builder,
110/// keeping all venue-specific knowledge out of both the builder and the generic override core.
111pub(crate) fn default_override_providers(
112 protocols: impl IntoIterator<Item = String>,
113) -> HashMap<String, Arc<dyn StateOverrideProvider>> {
114 let mut providers: HashMap<String, Arc<dyn StateOverrideProvider>> = HashMap::new();
115 providers.extend(titan::default_providers(protocols));
116 providers
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn snapshot_without_expiry_never_expires() {
125 let snapshot = OverrideSnapshot::default();
126 assert!(!snapshot.is_expired(u64::MAX));
127 }
128
129 #[test]
130 fn snapshot_expires_at_or_after_deadline() {
131 let snapshot = OverrideSnapshot { expires_at: Some(100), ..Default::default() };
132 assert!(!snapshot.is_expired(99));
133 assert!(snapshot.is_expired(100));
134 assert!(snapshot.is_expired(101));
135 }
136}