use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use alloy_network::Ethereum;
use alloy_primitives::{Address, U256};
use evm_fork_cache::cache::EvmCache;
use evm_fork_cache::reactive::{
ReactiveBatchReport, ReactiveConfig, ReactiveError, ReactiveInputBatch, ReactiveReport,
ReactiveRuntime, RegisterError, ResyncFailure, ResyncTarget,
};
use super::{
AdapterCache, AdapterRegistry, AmmReactiveHandler, PoolKey, PoolRegistration, PoolStatus,
ProtocolId, ProtocolMetadata, RegistryError,
};
#[derive(Debug)]
#[non_exhaustive]
pub enum AmmSyncError {
Register(RegisterError),
Reactive(ReactiveError),
Registry(RegistryError),
}
impl fmt::Display for AmmSyncError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Register(err) => write!(f, "failed to register AMM sync handler: {err}"),
Self::Reactive(err) => write!(f, "AMM reactive ingest failed: {err}"),
Self::Registry(err) => write!(f, "AMM registry mutation failed: {err}"),
}
}
}
impl std::error::Error for AmmSyncError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Register(err) => Some(err),
Self::Reactive(err) => Some(err),
Self::Registry(err) => Some(err),
}
}
}
impl From<RegistryError> for AmmSyncError {
fn from(err: RegistryError) -> Self {
Self::Registry(err)
}
}
impl From<RegisterError> for AmmSyncError {
fn from(err: RegisterError) -> Self {
Self::Register(err)
}
}
impl From<ReactiveError> for AmmSyncError {
fn from(err: ReactiveError) -> Self {
Self::Reactive(err)
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct AmmSyncBatchReport {
pub reactive: ReactiveBatchReport<Ethereum>,
pub degraded_pools: Vec<PoolKey>,
pub recovered_pools: Vec<PoolKey>,
pub resync_state_updates: usize,
pub resync_failures: usize,
}
pub struct AmmSyncEngine {
registry: AdapterRegistry,
runtime: ReactiveRuntime<Ethereum>,
config: ReactiveConfig,
degraded_targets: HashMap<PoolKey, PendingTargets>,
}
impl AmmSyncEngine {
pub fn new(registry: AdapterRegistry) -> Result<Self, AmmSyncError> {
Self::with_config(registry, ReactiveConfig::default())
}
pub fn with_config(
registry: AdapterRegistry,
config: ReactiveConfig,
) -> Result<Self, AmmSyncError> {
let runtime = runtime_for(®istry, config.clone())?;
Ok(Self {
registry,
runtime,
config,
degraded_targets: HashMap::new(),
})
}
pub fn registry(&self) -> &AdapterRegistry {
&self.registry
}
pub fn runtime(&self) -> &ReactiveRuntime<Ethereum> {
&self.runtime
}
pub fn replace_registry(&mut self, registry: AdapterRegistry) -> Result<(), AmmSyncError> {
let runtime = runtime_for(®istry, self.config.clone())?;
self.registry = registry;
self.runtime = runtime;
if !self.degraded_targets.is_empty() {
let registry = &self.registry;
self.degraded_targets
.retain(|key, _| registry.pool(key).is_some());
}
Ok(())
}
pub fn register_pools(
&mut self,
pools: impl IntoIterator<Item = PoolRegistration>,
) -> Result<(), AmmSyncError> {
let mut registry = self.registry.clone();
for pool in pools {
registry.register_pool(pool)?;
}
self.replace_registry(registry)
}
pub fn unregister_pools(
&mut self,
keys: &[PoolKey],
) -> Result<Vec<PoolRegistration>, AmmSyncError> {
let mut registry = self.registry.clone();
let removed: Vec<PoolRegistration> = keys
.iter()
.filter_map(|key| registry.unregister_pool(key))
.collect();
if removed.is_empty() {
return Ok(removed);
}
self.replace_registry(registry)?;
Ok(removed)
}
pub fn unregister_pools_evicting(
&mut self,
keys: &[PoolKey],
cache: &mut dyn AdapterCache,
) -> Result<Vec<PoolRegistration>, AmmSyncError> {
let removed = self.unregister_pools(keys)?;
evict_exclusive_state(cache, &removed, &self.registry);
Ok(removed)
}
pub fn ingest_batch(
&mut self,
cache: &mut EvmCache,
batch: ReactiveInputBatch<Ethereum>,
) -> Result<AmmSyncBatchReport, AmmSyncError> {
let reactive = self.runtime.ingest_batch_with_resync(cache, batch)?;
let resync_state_updates = resync_state_update_count(&reactive);
let resync_failures = resync_failure_count(&reactive);
let degraded_pools = self.mark_failed_resync_pools(&reactive);
let recovered_pools = self.recover_resynced_pools(&reactive, °raded_pools);
Ok(AmmSyncBatchReport {
reactive,
degraded_pools,
recovered_pools,
resync_state_updates,
resync_failures,
})
}
fn mark_failed_resync_pools(&mut self, report: &ReactiveBatchReport<Ethereum>) -> Vec<PoolKey> {
let mut degraded = Vec::new();
for failure in resync_failures(report) {
for key in pools_for_failure(&self.registry, failure) {
if !degraded.contains(&key) {
degraded.push(key.clone());
}
let recorded = self
.registry
.pool(&key)
.map(|pool| PendingTargets::covered(&failure.target, pool));
if let Some(targets) = recorded {
self.degraded_targets
.entry(key.clone())
.or_default()
.merge(targets);
}
if let Some(pool) = self.registry.pool_mut(&key) {
pool.status = PoolStatus::Degraded;
}
}
}
degraded
}
fn recover_resynced_pools(
&mut self,
report: &ReactiveBatchReport<Ethereum>,
degraded_now: &[PoolKey],
) -> Vec<PoolKey> {
let refreshed: Vec<(Address, U256)> = resynced_slot_writes(report).collect();
if refreshed.is_empty() {
return Vec::new();
}
let targets = &self.degraded_targets;
let recovered: Vec<PoolKey> = self
.registry
.pools()
.filter(|pool| pool.status == PoolStatus::Degraded)
.filter(|pool| !degraded_now.contains(&pool.key))
.filter(|pool| should_recover(pool, targets.get(&pool.key), &refreshed))
.map(|pool| pool.key.clone())
.collect();
for key in &recovered {
if let Some(pool) = self.registry.pool_mut(key) {
pool.status = PoolStatus::Ready;
}
self.degraded_targets.remove(key);
}
recovered
}
}
fn runtime_for(
registry: &AdapterRegistry,
config: ReactiveConfig,
) -> Result<ReactiveRuntime<Ethereum>, RegisterError> {
let mut runtime = ReactiveRuntime::<Ethereum>::new(config);
runtime.register_handler(Arc::new(AmmReactiveHandler::new(registry.clone())))?;
Ok(runtime)
}
fn resync_state_update_count(report: &ReactiveBatchReport<Ethereum>) -> usize {
report
.reports
.iter()
.filter_map(|report| match report.as_ref() {
ReactiveReport::Resynced(report) => Some(report.state_updates.len()),
_ => None,
})
.sum()
}
fn resync_failure_count(report: &ReactiveBatchReport<Ethereum>) -> usize {
resync_failures(report).count()
}
fn resync_failures(report: &ReactiveBatchReport<Ethereum>) -> impl Iterator<Item = &ResyncFailure> {
report
.reports
.iter()
.flat_map(|report| match report.as_ref() {
ReactiveReport::Resynced(report) => report.failed.iter(),
_ => [].iter(),
})
}
fn resynced_slot_writes(
report: &ReactiveBatchReport<Ethereum>,
) -> impl Iterator<Item = (Address, U256)> + '_ {
report
.reports
.iter()
.flat_map(|report| match report.as_ref() {
ReactiveReport::Resynced(report) => report.state_updates.as_slice(),
_ => &[],
})
.filter_map(|update| match update {
evm_fork_cache::StateUpdate::Slot { address, slot, .. } => Some((*address, *slot)),
_ => None,
})
}
fn evict_exclusive_state(
cache: &mut dyn AdapterCache,
removed: &[PoolRegistration],
remaining: &AdapterRegistry,
) {
for pool in removed {
for address in pool_state_addresses(pool) {
let shared = remaining
.pools()
.any(|other| pool_state_addresses(other).contains(&address));
if !shared {
cache.purge_storage(address);
continue;
}
let exclusive: Vec<U256> = pool_covered_slots_at(pool, address)
.into_iter()
.filter(|slot| {
!remaining
.pools()
.any(|other| pool_covers_storage_slot(other, address, *slot))
})
.collect();
if !exclusive.is_empty() {
cache.purge_slots(address, &exclusive);
}
}
}
}
fn pool_state_addresses(pool: &PoolRegistration) -> Vec<Address> {
let mut addresses: Vec<Address> = pool.key.address().into_iter().collect();
for address in &pool.state_addresses {
if !addresses.contains(address) {
addresses.push(*address);
}
}
addresses
}
fn pool_covered_slots_at(pool: &PoolRegistration, address: Address) -> Vec<U256> {
match &pool.metadata {
ProtocolMetadata::BalancerV2(metadata)
if metadata
.vault
.or_else(|| pool.state_addresses.first().copied())
== Some(address) =>
{
metadata.balance_slots.clone()
}
ProtocolMetadata::Curve(metadata) if pool.key.address() == Some(address) => {
metadata.discovered_slots.clone()
}
_ => Vec::new(),
}
}
fn pools_for_failure(registry: &AdapterRegistry, failure: &ResyncFailure) -> Vec<PoolKey> {
registry
.pools()
.filter(|pool| pool_matches_resync_target(pool, &failure.target))
.map(|pool| pool.key.clone())
.collect()
}
fn pool_matches_resync_target(pool: &PoolRegistration, target: &ResyncTarget) -> bool {
match target {
ResyncTarget::StorageSlot { address, slot } => {
pool_covers_storage_slot(pool, *address, *slot)
}
ResyncTarget::StorageSlots { address, slots } => slots
.iter()
.any(|slot| pool_covers_storage_slot(pool, *address, *slot)),
ResyncTarget::Account { address, .. } => pool_owns_address(pool, *address),
}
}
#[derive(Clone, Debug, Default)]
struct PendingTargets {
slots: Vec<(Address, U256)>,
accounts: Vec<Address>,
}
impl PendingTargets {
fn covered(target: &ResyncTarget, pool: &PoolRegistration) -> Self {
let mut pending = Self::default();
match target {
ResyncTarget::StorageSlot { address, slot } => {
if pool_covers_storage_slot(pool, *address, *slot) {
pending.slots.push((*address, *slot));
}
}
ResyncTarget::StorageSlots { address, slots } => {
for slot in slots {
if pool_covers_storage_slot(pool, *address, *slot) {
pending.slots.push((*address, *slot));
}
}
}
ResyncTarget::Account { address, .. } => {
if pool_owns_address(pool, *address) {
pending.accounts.push(*address);
}
}
}
pending
}
fn merge(&mut self, other: Self) {
for slot in other.slots {
if !self.slots.contains(&slot) {
self.slots.push(slot);
}
}
for address in other.accounts {
if !self.accounts.contains(&address) {
self.accounts.push(address);
}
}
}
fn is_empty(&self) -> bool {
self.slots.is_empty() && self.accounts.is_empty()
}
fn satisfied_by(&self, refreshed: &[(Address, U256)]) -> bool {
let slots_ok = self.slots.iter().all(|target| refreshed.contains(target));
let accounts_ok = self
.accounts
.iter()
.all(|address| refreshed.iter().any(|(a, _)| a == address));
slots_ok && accounts_ok
}
}
fn should_recover(
pool: &PoolRegistration,
targets: Option<&PendingTargets>,
refreshed: &[(Address, U256)],
) -> bool {
match targets {
Some(pending) if !pending.is_empty() => pending.satisfied_by(refreshed),
_ => refreshed
.iter()
.any(|(address, slot)| pool_covers_storage_slot(pool, *address, *slot)),
}
}
fn pool_covers_storage_slot(pool: &PoolRegistration, address: Address, slot: U256) -> bool {
match pool.protocol() {
ProtocolId::BalancerV2 => {
let ProtocolMetadata::BalancerV2(metadata) = &pool.metadata else {
return false;
};
metadata
.vault
.or_else(|| pool.state_addresses.first().copied())
== Some(address)
&& metadata.balance_slots.contains(&slot)
}
ProtocolId::Curve => {
let ProtocolMetadata::Curve(metadata) = &pool.metadata else {
return false;
};
pool.key.address() == Some(address) && metadata.discovered_slots.contains(&slot)
}
ProtocolId::UniswapV3 | ProtocolId::PancakeV3 | ProtocolId::Slipstream => {
pool.key.address() == Some(address)
}
ProtocolId::UniswapV2 | ProtocolId::SolidlyV2 => pool.key.address() == Some(address),
#[cfg(feature = "experimental-protocols")]
ProtocolId::BalancerV3 | ProtocolId::Erc4626 | ProtocolId::UniswapV4 => {
pool_owns_address(pool, address)
}
ProtocolId::Custom(_) => pool_owns_address(pool, address),
}
}
fn pool_owns_address(pool: &PoolRegistration, address: Address) -> bool {
pool.key.address() == Some(address) || pool.state_addresses.contains(&address)
}
#[cfg(test)]
mod tests {
use super::*;
fn v3_pool(address: Address) -> PoolRegistration {
PoolRegistration::new(PoolKey::UniswapV3(address))
}
#[test]
fn tracked_target_recovers_only_on_that_slot() {
let address = Address::repeat_byte(0xaa);
let target = U256::from(1);
let unrelated = U256::from(2);
let pool = v3_pool(address);
let pending = PendingTargets {
slots: vec![(address, target)],
accounts: Vec::new(),
};
assert!(!should_recover(
&pool,
Some(&pending),
&[(address, unrelated)]
));
assert!(should_recover(&pool, Some(&pending), &[(address, target)]));
assert!(should_recover(
&pool,
Some(&pending),
&[(address, unrelated), (address, target)],
));
}
#[test]
fn untracked_degradation_falls_back_to_address_coverage() {
let address = Address::repeat_byte(0xbb);
let pool = v3_pool(address);
assert!(should_recover(&pool, None, &[(address, U256::from(9))]));
assert!(!should_recover(
&pool,
None,
&[(Address::repeat_byte(0xcc), U256::from(9))],
));
}
#[test]
fn empty_pending_is_treated_as_untracked() {
let address = Address::repeat_byte(0xdd);
let pool = v3_pool(address);
let pending = PendingTargets::default();
assert!(should_recover(
&pool,
Some(&pending),
&[(address, U256::from(3))]
));
}
#[test]
fn covered_records_only_slots_the_pool_covers() {
let address = Address::repeat_byte(0xee);
let elsewhere = Address::repeat_byte(0x01);
let pool = v3_pool(address);
let here = ResyncTarget::StorageSlots {
address,
slots: vec![U256::from(1), U256::from(2)],
};
let recorded = PendingTargets::covered(&here, &pool);
assert_eq!(
recorded.slots,
vec![(address, U256::from(1)), (address, U256::from(2))],
);
let other = ResyncTarget::StorageSlot {
address: elsewhere,
slot: U256::from(5),
};
assert!(PendingTargets::covered(&other, &pool).is_empty());
}
}