use alloy_primitives::{Address, B256, Bytes, U256};
use evm_fork_cache::CacheError as UpstreamCacheError;
use evm_fork_cache::bulk_storage::{StorageProgram, run_storage_programs};
use evm_fork_cache::cache::{CodeSeedState, EvmCache};
use evm_fork_cache::cold_start::ColdStartConfig;
use super::bytecode::AdapterCodeSeed;
use super::state::UpstreamStateView;
use super::storage_sync::{StorageSyncSpec, decode_storage_sync, storage_sync_spec_for_pool};
use super::{
AdapterRegistry, CallOutcome, ColdStartOutcome, ColdStartPolicy, ColdStartReport,
PoolRegistration, PoolStatus, SlotChange, StateView, UnsupportedReason,
};
#[cfg(feature = "uniswap-v3")]
use super::v3_sync::{V3SyncError, V3SyncSpec, decode_full_sync, full_sync_program};
#[non_exhaustive]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CodeSeedReport {
pub verified: Vec<Address>,
pub mismatched: Vec<CodeSeedMismatch>,
pub not_deployed: Vec<Address>,
pub codeless: Vec<Address>,
pub unverifiable: Vec<(Address, String)>,
}
impl From<evm_fork_cache::cache::CodeVerifyReport> for CodeSeedReport {
fn from(report: evm_fork_cache::cache::CodeVerifyReport) -> Self {
Self {
verified: report.verified,
mismatched: report.mismatched.into_iter().map(Into::into).collect(),
not_deployed: report.not_deployed,
codeless: report.codeless,
unverifiable: report.unverifiable,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CodeSeedMismatch {
pub address: Address,
pub expected: B256,
pub actual: B256,
}
impl CodeSeedMismatch {
pub fn new(address: Address, expected: B256, actual: B256) -> Self {
Self {
address,
expected,
actual,
}
}
}
impl From<evm_fork_cache::cache::CodeMismatch> for CodeSeedMismatch {
fn from(mismatch: evm_fork_cache::cache::CodeMismatch) -> Self {
Self {
address: mismatch.address,
expected: mismatch.expected,
actual: mismatch.actual,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct ColdStartPlan {
pub verify: Vec<(Address, U256)>,
pub probe: Vec<(Address, U256)>,
pub accounts: Vec<Address>,
pub discover: Vec<ColdStartCall>,
}
impl From<ColdStartPlan> for evm_fork_cache::cold_start::ColdStartPlan {
fn from(plan: ColdStartPlan) -> Self {
evm_fork_cache::cold_start::ColdStartPlan {
verify: plan.verify,
probe: plan.probe,
accounts: plan.accounts,
discover: plan.discover.into_iter().map(Into::into).collect(),
probe_roots: Vec::new(),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct ColdStartCall {
pub from: Address,
pub to: Address,
pub calldata: Bytes,
pub restrict_to: Option<Vec<Address>>,
}
impl ColdStartCall {
pub fn new(from: Address, to: Address, calldata: impl Into<Bytes>) -> Self {
Self {
from,
to,
calldata: calldata.into(),
restrict_to: None,
}
}
pub fn with_restrict_to(mut self, addresses: impl IntoIterator<Item = Address>) -> Self {
self.restrict_to = Some(addresses.into_iter().collect());
self
}
}
impl From<ColdStartCall> for evm_fork_cache::cold_start::ColdStartCall {
fn from(call: ColdStartCall) -> Self {
evm_fork_cache::cold_start::ColdStartCall {
from: call.from,
to: call.to,
calldata: call.calldata,
restrict_to: call.restrict_to,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct ColdStartResults {
pub verified: Vec<SlotChange>,
pub fetched: Vec<SlotOutcome>,
pub probed: Vec<SlotOutcome>,
pub discovered: Vec<ColdStartCallResult>,
}
impl From<evm_fork_cache::cold_start::ColdStartResults> for ColdStartResults {
fn from(results: evm_fork_cache::cold_start::ColdStartResults) -> Self {
Self {
verified: results.verified.into_iter().map(SlotChange::from).collect(),
fetched: results.fetched.into_iter().map(SlotOutcome::from).collect(),
probed: results.probed.into_iter().map(SlotOutcome::from).collect(),
discovered: results
.discovered
.into_iter()
.map(ColdStartCallResult::from)
.collect(),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct ColdStartCallResult {
pub result: CallOutcome,
pub access: StorageAccessList,
}
impl ColdStartCallResult {
pub fn new(result: CallOutcome, access: StorageAccessList) -> Self {
Self { result, access }
}
}
impl From<evm_fork_cache::cold_start::ColdStartCallResult> for ColdStartCallResult {
fn from(call: evm_fork_cache::cold_start::ColdStartCallResult) -> Self {
Self {
result: CallOutcome::from(call.result),
access: StorageAccessList::from(call.access),
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct StorageAccessList {
pub accounts: Vec<Address>,
pub slots: Vec<(Address, U256)>,
}
impl From<evm_fork_cache::access_set::StorageAccessList> for StorageAccessList {
fn from(access: evm_fork_cache::access_set::StorageAccessList) -> Self {
Self {
accounts: access.accounts.into_iter().collect(),
slots: access.slots.into_iter().collect(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SlotFetch {
Value(U256),
Zero,
FetchFailed {
reason: String,
},
NotAttempted,
}
impl From<evm_fork_cache::cold_start::SlotFetch> for SlotFetch {
fn from(fetch: evm_fork_cache::cold_start::SlotFetch) -> Self {
use evm_fork_cache::cold_start::SlotFetch as Upstream;
match fetch {
Upstream::Value(value) => SlotFetch::Value(value),
Upstream::Zero => SlotFetch::Zero,
Upstream::FetchFailed { reason } => SlotFetch::FetchFailed { reason },
Upstream::NotAttempted => SlotFetch::NotAttempted,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SlotOutcome {
pub address: Address,
pub slot: U256,
pub fetch: SlotFetch,
}
impl SlotOutcome {
pub fn new(address: Address, slot: U256, fetch: SlotFetch) -> Self {
Self {
address,
slot,
fetch,
}
}
}
impl From<evm_fork_cache::cold_start::SlotOutcome> for SlotOutcome {
fn from(outcome: evm_fork_cache::cold_start::SlotOutcome) -> Self {
Self {
address: outcome.address,
slot: outcome.slot,
fetch: outcome.fetch.into(),
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum ColdStartStep {
Done,
Continue(ColdStartPlan),
}
impl From<ColdStartStep> for evm_fork_cache::cold_start::ColdStartStep {
fn from(step: ColdStartStep) -> Self {
match step {
ColdStartStep::Done => evm_fork_cache::cold_start::ColdStartStep::Done,
ColdStartStep::Continue(plan) => {
evm_fork_cache::cold_start::ColdStartStep::Continue(plan.into())
}
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct ColdStartRunReport {
pub rounds: usize,
pub verified_slots: usize,
pub changed_slots: usize,
pub discovered_accounts: usize,
pub discovered_slots: usize,
pub failed_slots: usize,
}
impl From<evm_fork_cache::cold_start::ColdStartRunReport> for ColdStartRunReport {
fn from(report: evm_fork_cache::cold_start::ColdStartRunReport) -> Self {
Self {
rounds: report.rounds,
verified_slots: report.verified_slots,
changed_slots: report.changed_slots,
discovered_accounts: report.discovered_accounts,
discovered_slots: report.discovered_slots,
failed_slots: report.failed_slots,
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ColdStartError {
NoBatchFetcher,
NoAccountProofFetcher,
NoAccountFieldsFetcher,
RoundBudgetExceeded {
max_rounds: usize,
},
Fetch(Box<dyn std::error::Error + Send + Sync + 'static>),
}
impl std::fmt::Display for ColdStartError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoBatchFetcher => write!(f, "cold-start requires a storage batch fetcher"),
Self::NoAccountProofFetcher => {
write!(f, "cold-start requires an account proof fetcher")
}
Self::NoAccountFieldsFetcher => {
write!(
f,
"cold-start code-seed verification requires an account fields fetcher"
)
}
Self::RoundBudgetExceeded { max_rounds } => {
write!(f, "cold-start round budget exceeded ({max_rounds})")
}
Self::Fetch(err) => write!(f, "cold-start fetch error: {err}"),
}
}
}
impl std::error::Error for ColdStartError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Fetch(err) => Some(&**err as &(dyn std::error::Error + 'static)),
_ => None,
}
}
}
impl From<evm_fork_cache::cold_start::ColdStartError> for ColdStartError {
fn from(err: evm_fork_cache::cold_start::ColdStartError) -> Self {
use evm_fork_cache::cold_start::ColdStartError as Upstream;
match err {
Upstream::NoBatchFetcher => ColdStartError::NoBatchFetcher,
Upstream::NoAccountProofFetcher => ColdStartError::NoAccountProofFetcher,
Upstream::NoAccountFieldsFetcher => ColdStartError::NoAccountFieldsFetcher,
Upstream::RoundBudgetExceeded { max_rounds } => {
ColdStartError::RoundBudgetExceeded { max_rounds }
}
Upstream::Fetch(cause) => ColdStartError::Fetch(Box::new(cause)),
}
}
}
pub trait AdapterColdStartPlanner {
fn initial_plan(&mut self, state: &dyn StateView) -> ColdStartPlan;
fn on_results(&mut self, results: &ColdStartResults, state: &dyn StateView) -> ColdStartStep;
fn finish(
&mut self,
pool: &mut PoolRegistration,
report: &ColdStartRunReport,
) -> ColdStartOutcome;
}
struct Bridge<'a>(&'a mut dyn AdapterColdStartPlanner);
impl evm_fork_cache::cold_start::ColdStartPlanner for Bridge<'_> {
fn initial_plan(
&mut self,
state: &dyn evm_fork_cache::StateView,
) -> evm_fork_cache::cold_start::ColdStartPlan {
self.0.initial_plan(&UpstreamStateView(state)).into()
}
fn on_results(
&mut self,
results: &evm_fork_cache::cold_start::ColdStartResults,
state: &dyn evm_fork_cache::StateView,
) -> evm_fork_cache::cold_start::ColdStartStep {
let results = ColdStartResults::from(results.clone());
self.0
.on_results(&results, &UpstreamStateView(state))
.into()
}
}
enum HydrationKind {
#[cfg(feature = "uniswap-v3")]
V3 {
pool: Address,
spec: V3SyncSpec,
},
Flat {
spec: StorageSyncSpec,
},
}
impl HydrationKind {
fn program(&self) -> StorageProgram {
match self {
#[cfg(feature = "uniswap-v3")]
HydrationKind::V3 { pool, spec } => full_sync_program(*pool, spec),
HydrationKind::Flat { spec } => spec.program(),
}
}
fn apply(&self, cache: &mut EvmCache, output: &Bytes) -> Result<Hydrated, HydrationError> {
let entries: Vec<(Address, U256, U256)> = match self {
#[cfg(feature = "uniswap-v3")]
HydrationKind::V3 { pool, spec } => {
let snapshot = decode_full_sync(spec, output).map_err(HydrationError::V3)?;
snapshot
.storage_entries(spec)
.into_iter()
.map(|(slot, value)| (*pool, slot, value))
.collect()
}
HydrationKind::Flat { spec } => {
let snapshot = decode_storage_sync(spec, output).map_err(HydrationError::Flat)?;
snapshot.storage_entries()
}
};
Ok(inject_and_record(cache, entries))
}
}
struct Hydrated {
verified: Vec<(Address, U256)>,
changed: Vec<SlotChange>,
}
fn inject_and_record(cache: &mut EvmCache, entries: Vec<(Address, U256, U256)>) -> Hydrated {
let verified: Vec<(Address, U256)> = entries.iter().map(|(a, s, _)| (*a, *s)).collect();
let changed: Vec<SlotChange> = entries
.iter()
.filter_map(|(address, slot, value)| {
let prior = cache
.cached_storage_value(*address, *slot)
.unwrap_or(U256::ZERO);
(prior != *value).then(|| SlotChange::new(*address, *slot, prior, *value))
})
.collect();
cache.inject_storage_batch(&entries);
Hydrated { verified, changed }
}
fn hydration_kind(pool: &PoolRegistration) -> Option<HydrationKind> {
let address = pool.key.address()?;
#[cfg(feature = "uniswap-v3")]
if let Some(spec) = v3_sync_spec(pool) {
return Some(HydrationKind::V3 {
pool: address,
spec,
});
}
let _ = address;
storage_sync_spec_for_pool(pool)
.ok()
.map(|spec| HydrationKind::Flat { spec })
}
#[cfg(feature = "uniswap-v3")]
fn v3_sync_spec(pool: &PoolRegistration) -> Option<V3SyncSpec> {
use super::ProtocolMetadata;
let (metadata, canonical_uniswap) = match &pool.metadata {
ProtocolMetadata::UniswapV3(metadata) => (metadata, true),
ProtocolMetadata::PancakeV3(metadata) | ProtocolMetadata::Slipstream(metadata) => {
(metadata, false)
}
_ => return None,
};
let layout = metadata.storage_layout.filter(|l| l.tick_spacing > 0)?;
Some(if canonical_uniswap {
V3SyncSpec::uniswap(layout)
} else {
V3SyncSpec::core(layout)
})
}
pub fn supports_one_shot_hydration(pool: &PoolRegistration) -> bool {
hydration_kind(pool).is_some()
}
fn fast_metadata_complete(pool: &PoolRegistration) -> bool {
use super::ProtocolMetadata;
match &pool.metadata {
ProtocolMetadata::UniswapV2(m) => m.token0.is_some() && m.token1.is_some(),
ProtocolMetadata::UniswapV3(m)
| ProtocolMetadata::PancakeV3(m)
| ProtocolMetadata::Slipstream(m) => m.fee.is_some() && m.storage_layout.is_some(),
ProtocolMetadata::SolidlyV2(m) => {
m.token0.is_some()
&& m.token1.is_some()
&& m.stable.is_some()
&& m.storage_layout.is_some()
}
ProtocolMetadata::BalancerV2(m) => m.vault.is_some() && !m.balance_slots.is_empty(),
ProtocolMetadata::Curve(m) => !m.coins.is_empty() && !m.discovered_slots.is_empty(),
ProtocolMetadata::Unknown | ProtocolMetadata::Custom(_) => false,
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum HydrationError {
#[cfg(feature = "uniswap-v3")]
V3(V3SyncError),
Flat(super::storage_sync::StorageSyncError),
}
impl std::fmt::Display for HydrationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(feature = "uniswap-v3")]
Self::V3(_) => write!(f, "one-shot V3 hydration failed"),
Self::Flat(_) => write!(f, "one-shot flat-slot hydration failed"),
}
}
}
impl std::error::Error for HydrationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
#[cfg(feature = "uniswap-v3")]
Self::V3(err) => Some(err as &(dyn std::error::Error + 'static)),
Self::Flat(err) => Some(err as &(dyn std::error::Error + 'static)),
}
}
}
impl AdapterRegistry {
pub fn cold_start(
&self,
pool: &mut PoolRegistration,
cache: &mut EvmCache,
policy: ColdStartPolicy,
) -> Result<ColdStartOutcome, ColdStartError> {
let Some(adapter) = self.adapter(pool.protocol()) else {
return Ok(ColdStartOutcome::Unsupported(UnsupportedReason::Protocol(
pool.protocol(),
)));
};
let mut planner = match adapter.cold_start_planner(pool, policy) {
Ok(planner) => planner,
Err(reason) => return Ok(ColdStartOutcome::Unsupported(reason)),
};
let code_seed_report = if self.code_seeding && cache.account_fields_fetcher().is_some() {
match adapter.code_seeds(pool) {
Ok(seeds) if !seeds.is_empty() => Some(seed_and_verify(cache, seeds)),
_ => None,
}
} else {
None
};
let report = {
let mut bridge = Bridge(planner.as_mut());
cache
.run_cold_start(&mut bridge, ColdStartConfig::default())
.map_err(ColdStartError::from)?
};
let report = ColdStartRunReport::from(report);
let outcome = planner.finish(pool, &report);
Ok(attach_code_seeds(outcome, code_seed_report))
}
pub async fn cold_start_many<P>(
&self,
pools: &mut [PoolRegistration],
cache: &mut EvmCache,
provider: &P,
policy: ColdStartPolicy,
) -> Result<Vec<ColdStartOutcome>, ColdStartError>
where
P: alloy_provider::Provider<alloy_network::AnyNetwork>,
{
if pools.is_empty() {
return Ok(Vec::new());
}
let mut fast: Vec<(usize, HydrationKind)> = Vec::new();
let mut is_fallback = vec![true; pools.len()];
for (index, pool) in pools.iter().enumerate() {
if self.adapter(pool.protocol()).is_none() {
continue;
}
if let Some(kind) = hydration_kind(pool)
&& fast_metadata_complete(pool)
{
is_fallback[index] = false;
fast.push((index, kind));
}
}
if self.code_seeding && cache.account_fields_fetcher().is_some() {
let mut any_pending = false;
for (index, _) in &fast {
let pool = &pools[*index];
if let Some(adapter) = self.adapter(pool.protocol()) {
if let Ok(seeds) = adapter.code_seeds(pool) {
any_pending |= seed_batch(cache, seeds);
}
}
}
if any_pending {
let _ = verify_pending_seeds(cache);
}
}
let mut outcomes: Vec<Option<ColdStartOutcome>> = (0..pools.len()).map(|_| None).collect();
if !fast.is_empty() {
let programs: Vec<StorageProgram> =
fast.iter().map(|(_, kind)| kind.program()).collect();
let results = run_storage_programs(provider, cache.block(), &programs).await;
for ((index, kind), result) in fast.iter().zip(results) {
match result.map(|output| kind.apply(cache, &output)) {
Ok(Ok(hydrated)) => {
let pool = &mut pools[*index];
pool.status = PoolStatus::Ready;
let mut report = ColdStartReport::new(pool.key.clone(), policy);
report.status = PoolStatus::Ready;
report.verified_slots = hydrated.verified;
report.changed_slots = hydrated.changed;
outcomes[*index] = Some(ColdStartOutcome::Ready(report));
}
_ => is_fallback[*index] = true,
}
}
}
for (index, pool) in pools.iter_mut().enumerate() {
if is_fallback[index] {
outcomes[index] = Some(self.cold_start(pool, cache, policy)?);
}
}
if matches!(policy, ColdStartPolicy::Strict | ColdStartPolicy::Eager) {
for target in self.collect_quote_code_targets(pools) {
let _ = cache.ensure_account(target).await;
}
}
Ok(outcomes
.into_iter()
.map(|outcome| outcome.expect("every pool is fast-hydrated or fell back"))
.collect())
}
fn collect_quote_code_targets(&self, pools: &[PoolRegistration]) -> Vec<Address> {
let mut targets: Vec<Address> = Vec::new();
for pool in pools {
if self.adapter(pool.protocol()).is_none() {
continue;
}
for target in pool.quote_code_targets(&self.sim_config) {
if !targets.contains(&target) {
targets.push(target);
}
}
}
targets
}
}
fn attach_code_seeds(
outcome: ColdStartOutcome,
code_seed_report: Option<CodeSeedReport>,
) -> ColdStartOutcome {
match outcome {
ColdStartOutcome::Ready(mut report) => {
report.code_seeds = code_seed_report;
ColdStartOutcome::Ready(report)
}
ColdStartOutcome::ReadyWithDeferred(mut report, deferred) => {
report.code_seeds = code_seed_report;
ColdStartOutcome::ReadyWithDeferred(report, deferred)
}
ColdStartOutcome::NeedsRepair(mut report, repair) => {
report.code_seeds = code_seed_report;
ColdStartOutcome::NeedsRepair(report, repair)
}
ColdStartOutcome::Unsupported(reason) => ColdStartOutcome::Unsupported(reason),
}
}
fn seed_and_verify(cache: &mut EvmCache, seeds: Vec<AdapterCodeSeed>) -> CodeSeedReport {
let any_pending = seed_batch(cache, seeds);
if !any_pending {
return CodeSeedReport::default();
}
verify_pending_seeds(cache)
}
fn seed_batch(cache: &mut EvmCache, seeds: impl IntoIterator<Item = AdapterCodeSeed>) -> bool {
let mut any_pending = false;
for seed in seeds {
match cache.code_seed_state(&seed.address) {
Some(CodeSeedState::Verified { code_hash, .. }) if *code_hash == seed.code_hash => {
continue;
}
Some(CodeSeedState::Etched { .. }) => continue,
Some(CodeSeedState::Pending { code_hash }) if *code_hash == seed.code_hash => {
any_pending = true;
continue;
}
_ => {}
}
match cache.seed_account_code(seed.address, seed.runtime_bytecode) {
Ok(_) => {
if matches!(
cache.code_seed_state(&seed.address),
Some(CodeSeedState::Pending { .. })
) {
any_pending = true;
}
}
Err(UpstreamCacheError::CodeSeedConflict { .. })
| Err(UpstreamCacheError::CodeSeedEmpty { .. }) => {}
Err(_) => {}
}
}
any_pending
}
fn verify_pending_seeds(cache: &mut EvmCache) -> CodeSeedReport {
match cache.verify_code_seeds() {
Ok(report) => {
for (address, _reason) in &report.unverifiable {
cache.purge_account(*address);
}
CodeSeedReport::from(report)
}
Err(_) => {
for address in cache.pending_code_seeds() {
cache.purge_account(address);
}
CodeSeedReport::default()
}
}
}
#[cfg(all(test, feature = "uniswap-v3"))]
mod tests {
use super::*;
use crate::adapters::ConcentratedLiquidityAdapter;
use crate::adapters::storage::V3StorageLayout;
use crate::adapters::types::{
PoolKey, PoolRegistration, ProtocolMetadata, UniswapV2Metadata, V3Metadata,
};
use crate::adapters::v3_sync::V3SyncSpec;
use alloy_primitives::Address;
use std::sync::Arc;
#[test]
fn uniswap_v3_uses_the_canonical_full_spec() {
let layout = V3StorageLayout::uniswap(10);
let pool = PoolRegistration::new(PoolKey::UniswapV3(Address::repeat_byte(0x11)))
.with_metadata(ProtocolMetadata::UniswapV3(
V3Metadata::default()
.with_fee(500)
.with_storage_layout(layout),
));
assert_eq!(v3_sync_spec(&pool), Some(V3SyncSpec::uniswap(layout)));
}
#[test]
fn pancake_and_slipstream_use_the_core_spec_until_verified() {
let pancake_layout = V3StorageLayout::pancake(10);
let pancake = PoolRegistration::new(PoolKey::PancakeV3(Address::repeat_byte(0x22)))
.with_metadata(ProtocolMetadata::PancakeV3(
V3Metadata::default()
.with_fee(2500)
.with_storage_layout(pancake_layout),
));
assert_eq!(
v3_sync_spec(&pancake),
Some(V3SyncSpec::core(pancake_layout))
);
let slip_layout = V3StorageLayout::slipstream(100);
let slip = PoolRegistration::new(PoolKey::Slipstream(Address::repeat_byte(0x33)))
.with_metadata(ProtocolMetadata::Slipstream(
V3Metadata::default().with_storage_layout(slip_layout),
));
assert_eq!(v3_sync_spec(&slip), Some(V3SyncSpec::core(slip_layout)));
}
#[test]
fn non_positive_tick_spacing_is_not_fast_eligible() {
let layout = V3StorageLayout::uniswap(0);
let pool = PoolRegistration::new(PoolKey::UniswapV3(Address::repeat_byte(0x44)))
.with_metadata(ProtocolMetadata::UniswapV3(
V3Metadata::default()
.with_fee(500)
.with_storage_layout(layout),
));
assert_eq!(v3_sync_spec(&pool), None);
assert!(!supports_one_shot_hydration(&pool));
}
#[test]
fn collect_quote_code_targets_dedupes_and_skips_unadaptered_pools() {
let (q1, q2) = (Address::repeat_byte(0xd1), Address::repeat_byte(0xd2));
let mut registry = AdapterRegistry::new();
registry
.register_adapter(Arc::new(ConcentratedLiquidityAdapter::default()))
.expect("register the V3-family adapter");
let v3 = |tag: u8, quoter: Address| {
PoolRegistration::new(PoolKey::UniswapV3(Address::repeat_byte(tag))).with_metadata(
ProtocolMetadata::UniswapV3(V3Metadata::default().with_quoter(quoter)),
)
};
let pools = vec![
v3(0x01, q1),
v3(0x02, q1), v3(0x03, q2), PoolRegistration::new(PoolKey::UniswapV2(Address::repeat_byte(0x04)))
.with_metadata(ProtocolMetadata::UniswapV2(UniswapV2Metadata::default())),
];
assert_eq!(registry.collect_quote_code_targets(&pools), vec![q1, q2]);
}
}
#[cfg(test)]
mod hydration_tests {
use super::*;
use alloy_primitives::Address;
use alloy_provider::RootProvider;
use alloy_provider::network::AnyNetwork;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
use std::sync::Arc;
async fn mock_cache() -> EvmCache {
let provider = RootProvider::<AnyNetwork>::new(RpcClient::mocked(Asserter::new()));
EvmCache::new(Arc::new(provider)).await
}
#[tokio::test(flavor = "multi_thread")]
async fn inject_and_record_reports_loaded_and_changed_slots() {
let pool = Address::repeat_byte(0x51);
let unchanged = U256::from(1);
let changed = U256::from(2);
let fresh = U256::from(3);
let value = U256::from(0xabc_u64);
let mut cache = mock_cache().await;
cache.inject_storage_batch(&[
(pool, unchanged, value),
(pool, changed, U256::from(10_u64)),
]);
let entries = vec![
(pool, unchanged, value), (pool, changed, U256::from(20_u64)), (pool, fresh, value), ];
let hydrated = inject_and_record(&mut cache, entries);
assert_eq!(
hydrated.verified,
vec![(pool, unchanged), (pool, changed), (pool, fresh)]
);
assert_eq!(hydrated.changed.len(), 2);
assert!(hydrated.changed.iter().any(|c| c.slot == changed
&& c.old == U256::from(10_u64)
&& c.new == U256::from(20_u64)));
assert!(
hydrated
.changed
.iter()
.any(|c| c.slot == fresh && c.old == U256::ZERO && c.new == value)
);
assert_eq!(
cache.cached_storage_value(pool, changed),
Some(U256::from(20_u64))
);
assert_eq!(cache.cached_storage_value(pool, fresh), Some(value));
}
}