Skip to main content

eth_prices/router/
auto.rs

1use std::collections::{HashMap, HashSet};
2
3use alloy::{
4    eips::BlockNumberOrTag,
5    primitives::{Address, U256, address, aliases::U24},
6    providers::Provider,
7};
8use futures::future::join_all;
9
10use crate::{
11    Result,
12    asset::{erc20::ERC20, identity::AssetIdentifier},
13    network::NetworkId,
14    provider::RpcProvider,
15    quoter::{
16        AnyQuoter,
17        erc4626::{ERC4626, ERC4626Quoter},
18        uniswap_v2::{UniswapV2Quoter, discovery::UniswapV2Factory, pair::UniswapV2Pair},
19        uniswap_v3::{UniswapV3Quoter, discovery::UniswapV3Factory, pool::UniswapV3Pool},
20    },
21    router::Router,
22};
23
24const UNISWAP_V2_FACTORY: Address = address!("0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f");
25const UNISWAP_V3_FACTORY: Address = address!("0x1F98431c8aD98523631AE4a59f267346ea31F984");
26const DEFAULT_V3_FEES: &[u32] = &[100, 500, 3000, 10000];
27const MAX_CONFIDENCE: u64 = 100;
28const FALLBACK_DECIMALS: u8 = 18;
29const FRESHNESS_HALF_LIFE_SECONDS: f64 = 86_400.0;
30
31#[derive(Debug, Clone)]
32#[allow(dead_code)]
33enum PoolKind {
34    V2,
35    V3(u32),
36}
37
38#[derive(Debug, Clone)]
39#[allow(dead_code)]
40struct DiscoveredPool {
41    pool_address: Address,
42    token0: Address,
43    token1: Address,
44    score: U256,
45    kind: PoolKind,
46    reserve0: U256,
47    reserve1: U256,
48    last_trade_timestamp: Option<u64>,
49}
50
51#[derive(Debug, Clone, serde::Serialize)]
52#[serde(rename_all = "camelCase")]
53pub struct DiscoveryFailure {
54    pub target: String,
55    pub message: String,
56}
57
58#[derive(Debug, Clone, serde::Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct DiscovererReport {
61    pub identity: String,
62    pub attempted: usize,
63    pub discovered: usize,
64    pub skipped: usize,
65    pub failures: Vec<DiscoveryFailure>,
66}
67
68#[derive(Debug, Clone, Default, serde::Serialize)]
69#[serde(rename_all = "camelCase")]
70pub struct DiscoveryReport {
71    pub discoverers: Vec<DiscovererReport>,
72}
73
74#[derive(Debug, Clone)]
75pub struct AutoRouter {
76    provider: RpcProvider,
77    tokens: Vec<AssetIdentifier>,
78    network_id: Option<NetworkId>,
79    uniswap_v2_factory: Option<Address>,
80    uniswap_v3_factory: Option<Address>,
81    uniswap_v3_fees: Vec<u32>,
82    min_liquidity: Option<U256>,
83    discover_v2: bool,
84    discover_v3: bool,
85    discover_erc4626: bool,
86}
87
88impl AutoRouter {
89    pub fn new(provider: RpcProvider, tokens: Vec<AssetIdentifier>) -> Self {
90        Self {
91            provider,
92            tokens,
93            network_id: None,
94            uniswap_v2_factory: None,
95            uniswap_v3_factory: None,
96            uniswap_v3_fees: DEFAULT_V3_FEES.to_vec(),
97            min_liquidity: Some(U256::from(1)),
98            discover_v2: true,
99            discover_v3: true,
100            discover_erc4626: true,
101        }
102    }
103
104    pub fn with_network_id(mut self, network_id: NetworkId) -> Self {
105        self.network_id = Some(network_id);
106        self
107    }
108
109    pub fn with_uniswap_v2_factory(mut self, address: Address) -> Self {
110        self.uniswap_v2_factory = Some(address);
111        self
112    }
113
114    pub fn with_uniswap_v3_factory(mut self, address: Address) -> Self {
115        self.uniswap_v3_factory = Some(address);
116        self
117    }
118
119    pub fn with_uniswap_v3_fees(mut self, fees: Vec<u32>) -> Self {
120        self.uniswap_v3_fees = fees;
121        self
122    }
123
124    pub fn with_min_liquidity(mut self, min: U256) -> Self {
125        self.min_liquidity = Some(min);
126        self
127    }
128
129    pub fn discover_uniswap_v2(mut self, enable: bool) -> Self {
130        self.discover_v2 = enable;
131        self
132    }
133
134    pub fn discover_uniswap_v3(mut self, enable: bool) -> Self {
135        self.discover_v3 = enable;
136        self
137    }
138
139    pub fn discover_erc4626(mut self, enable: bool) -> Self {
140        self.discover_erc4626 = enable;
141        self
142    }
143
144    pub async fn build(self) -> Result<Router> {
145        Ok(self.build_with_report().await?.0)
146    }
147
148    pub async fn build_with_report(self) -> Result<(Router, DiscoveryReport)> {
149        let network_id = match self.network_id {
150            Some(ref id) => id.clone(),
151            None => NetworkId::from_provider(&self.provider).await?,
152        };
153
154        let mut all_quoters: Vec<AnyQuoter> = Vec::new();
155        let mut report = DiscoveryReport::default();
156
157        // 1. ERC4626 discovery first — collect underlying tokens
158        let mut extra_addresses: Vec<Address> = Vec::new();
159        if self.discover_erc4626 {
160            let (erc4626_quoters, underlying, discoverer) =
161                self.discover_erc4626_quoters(&network_id).await;
162            all_quoters.extend(erc4626_quoters);
163            extra_addresses = underlying;
164            report.discoverers.push(discoverer);
165        }
166
167        // 2. Build expanded address set (input tokens + ERC4626 underlyings)
168        let mut all_addresses: Vec<Address> = self.erc20_addresses();
169        let existing: HashSet<Address> = all_addresses.iter().copied().collect();
170        for addr in extra_addresses {
171            if !existing.contains(&addr) {
172                all_addresses.push(addr);
173            }
174        }
175
176        // 3. V2 discovery with expanded set
177        let mut v2_pools: Vec<DiscoveredPool> = Vec::new();
178        if self.discover_v2 {
179            let factory = self.uniswap_v2_factory.unwrap_or(UNISWAP_V2_FACTORY);
180            let (pools, attempted, failures) =
181                Self::discover_v2_pools_inner(&self.provider, &all_addresses, factory).await;
182            v2_pools = Self::filter_pools(Self::deduplicate_pools(pools), &self.min_liquidity);
183            report.discoverers.push(DiscovererReport {
184                identity: format!("uniswap_v2:{factory}"),
185                attempted,
186                discovered: v2_pools.len(),
187                skipped: attempted.saturating_sub(v2_pools.len() + failures.len()),
188                failures,
189            });
190        }
191
192        // 4. V3 discovery with expanded set
193        let mut v3_pools: Vec<DiscoveredPool> = Vec::new();
194        if self.discover_v3 {
195            let factory = self.uniswap_v3_factory.unwrap_or(UNISWAP_V3_FACTORY);
196            let (pools, attempted, failures) = Self::discover_v3_pools_inner(
197                &self.provider,
198                &all_addresses,
199                factory,
200                &self.uniswap_v3_fees,
201            )
202            .await;
203            v3_pools = Self::filter_pools(Self::deduplicate_pools(pools), &self.min_liquidity);
204            report.discoverers.push(DiscovererReport {
205                identity: format!("uniswap_v3:{factory}"),
206                attempted,
207                discovered: v3_pools.len(),
208                skipped: attempted.saturating_sub(v3_pools.len() + failures.len()),
209                failures,
210            });
211        }
212
213        // 5. Score every kept pool: decimals-normalized liquidity decayed by
214        //    the age of the pool's last trade, matching the TypeScript router.
215        let (decimals, block_timestamp, v3_last_trades) = futures::join!(
216            fetch_decimals(&self.provider, &all_addresses),
217            fetch_block_timestamp(&self.provider),
218            fetch_v3_last_trades(&self.provider, &v3_pools),
219        );
220
221        for pool in v2_pools {
222            let confidence = pool_confidence(
223                v2_geometric_mean(&pool, &decimals),
224                block_timestamp,
225                pool.last_trade_timestamp,
226            );
227            let quoter = UniswapV2Quoter {
228                network_id: network_id.clone(),
229                pair_address: pool.pool_address,
230                token0: pool.token0,
231                token1: pool.token1,
232            };
233            all_quoters.push(AnyQuoter::from(quoter).with_confidence(confidence));
234        }
235
236        for pool in v3_pools {
237            let confidence = pool_confidence(
238                v3_geometric_mean(&pool, &decimals),
239                block_timestamp,
240                v3_last_trades.get(&pool.pool_address).copied(),
241            );
242            let quoter = UniswapV3Quoter {
243                network_id: network_id.clone(),
244                pool_address: pool.pool_address,
245                token0: pool.token0,
246                token1: pool.token1,
247            };
248            all_quoters.push(AnyQuoter::from(quoter).with_confidence(confidence));
249        }
250
251        if all_quoters.is_empty() {
252            return Err(crate::error::EthPricesError::AutoRouterNoPools);
253        }
254
255        Ok((Router::from_iter(all_quoters), report))
256    }
257
258    fn erc20_addresses(&self) -> Vec<Address> {
259        self.tokens
260            .iter()
261            .filter_map(|t| match t {
262                AssetIdentifier::ERC20 { address } => Some(*address),
263                _ => None,
264            })
265            .collect()
266    }
267
268    fn sorted_pair(a: Address, b: Address) -> (Address, Address) {
269        if a < b { (a, b) } else { (b, a) }
270    }
271
272    fn deduplicate_pools(pools: Vec<DiscoveredPool>) -> Vec<DiscoveredPool> {
273        let mut best: std::collections::HashMap<(Address, Address), DiscoveredPool> =
274            std::collections::HashMap::new();
275
276        for pool in pools {
277            let key = Self::sorted_pair(pool.token0, pool.token1);
278            match best.get(&key) {
279                Some(existing) if existing.score >= pool.score => continue,
280                _ => {
281                    best.insert(key, pool);
282                }
283            }
284        }
285
286        let mut pools: Vec<DiscoveredPool> = best.into_values().collect();
287        pools.sort_by_key(|pool| pool.pool_address);
288        pools
289    }
290
291    fn filter_pools(
292        pools: Vec<DiscoveredPool>,
293        min_liquidity: &Option<U256>,
294    ) -> Vec<DiscoveredPool> {
295        match min_liquidity {
296            Some(min) => pools.into_iter().filter(|p| p.score >= *min).collect(),
297            None => pools,
298        }
299    }
300
301    async fn discover_v2_pools_inner(
302        provider: &RpcProvider,
303        addresses: &[Address],
304        factory: Address,
305    ) -> (Vec<DiscoveredPool>, usize, Vec<DiscoveryFailure>) {
306        let mut pairs = Vec::new();
307        for i in 0..addresses.len() {
308            for j in (i + 1)..addresses.len() {
309                pairs.push((addresses[i], addresses[j]));
310            }
311        }
312        let attempted = pairs.len();
313
314        let results: Vec<_> = join_all(pairs.into_iter().map(|(a, b)| {
315            let provider = provider.clone();
316            async move { discover_single_v2_pool(&provider, factory, a, b).await }
317        }))
318        .await;
319
320        let mut pools: Vec<DiscoveredPool> = Vec::new();
321        let mut failures: Vec<DiscoveryFailure> = Vec::new();
322        for result in results {
323            match result {
324                Ok(Some(pool)) => pools.push(pool),
325                Ok(None) => {}
326                Err(failure) => failures.push(failure),
327            }
328        }
329
330        let liq_futures: Vec<_> = pools
331            .iter()
332            .map(|pool| {
333                let provider = provider.clone();
334                async move {
335                    let pair = UniswapV2Pair::new(pool.pool_address, &provider);
336                    pair.getReserves()
337                        .call()
338                        .await
339                        .map(|reserves| {
340                            (
341                                U256::from(reserves.reserve0),
342                                U256::from(reserves.reserve1),
343                                u64::from(reserves.blockTimestampLast),
344                            )
345                        })
346                        .map_err(|error| error.to_string())
347                }
348            })
349            .collect();
350
351        let scores = join_all(liq_futures).await;
352        let scored = pools
353            .into_iter()
354            .zip(scores)
355            .filter_map(|(mut pool, reserves)| match reserves {
356                Ok((reserve0, reserve1, last_trade)) => {
357                    pool.score = std::cmp::min(reserve0, reserve1);
358                    pool.reserve0 = reserve0;
359                    pool.reserve1 = reserve1;
360                    pool.last_trade_timestamp = Some(last_trade);
361                    Some(pool)
362                }
363                Err(message) => {
364                    failures.push(DiscoveryFailure {
365                        target: pool.pool_address.to_string(),
366                        message: format!("getReserves failed: {message}"),
367                    });
368                    None
369                }
370            })
371            .collect();
372
373        (scored, attempted, failures)
374    }
375
376    async fn discover_v3_pools_inner(
377        provider: &RpcProvider,
378        addresses: &[Address],
379        factory: Address,
380        fees: &[u32],
381    ) -> (Vec<DiscoveredPool>, usize, Vec<DiscoveryFailure>) {
382        if addresses.len() < 2 {
383            return (Vec::new(), 0, Vec::new());
384        }
385
386        let mut queries = Vec::new();
387        for i in 0..addresses.len() {
388            for j in (i + 1)..addresses.len() {
389                let a = addresses[i];
390                let b = addresses[j];
391                for &fee in fees {
392                    queries.push((a, b, fee));
393                }
394            }
395        }
396        let attempted = queries.len();
397
398        let results = join_all(queries.into_iter().map(|(a, b, fee)| {
399            let provider = provider.clone();
400            async move { discover_single_v3_pool(&provider, factory, a, b, fee).await }
401        }))
402        .await;
403
404        let mut pools: Vec<DiscoveredPool> = Vec::new();
405        let mut failures: Vec<DiscoveryFailure> = Vec::new();
406        for result in results {
407            match result {
408                Ok(Some(pool)) => pools.push(pool),
409                Ok(None) => {}
410                Err(failure) => failures.push(failure),
411            }
412        }
413
414        (pools, attempted, failures)
415    }
416
417    async fn discover_erc4626_quoters(
418        &self,
419        network_id: &NetworkId,
420    ) -> (Vec<AnyQuoter>, Vec<Address>, DiscovererReport) {
421        let addresses = self.erc20_addresses();
422        let attempted = addresses.len();
423
424        let results: Vec<_> = join_all(addresses.into_iter().map(|addr| {
425            let provider = self.provider.clone();
426            let net_id = network_id.clone();
427            async move {
428                match ERC4626::new(addr, &provider).asset().call().await {
429                    Ok(underlying) => {
430                        let quoter = ERC4626Quoter {
431                            network_id: net_id,
432                            vault_address: AssetIdentifier::ERC20 { address: addr },
433                            token_address: AssetIdentifier::ERC20 {
434                                address: underlying,
435                            },
436                        };
437                        Ok((AnyQuoter::from(quoter).with_confidence(50), underlying))
438                    }
439                    Err(error) => Err(DiscoveryFailure {
440                        target: addr.to_string(),
441                        message: error.to_string(),
442                    }),
443                }
444            }
445        }))
446        .await;
447
448        let mut quoters = Vec::new();
449        let mut underlying = Vec::new();
450        let mut failures = Vec::new();
451        for result in results {
452            match result {
453                Ok((quoter, asset)) => {
454                    quoters.push(quoter);
455                    underlying.push(asset);
456                }
457                Err(failure) => failures.push(failure),
458            }
459        }
460
461        let discovered = quoters.len();
462        (
463            quoters,
464            underlying,
465            DiscovererReport {
466                identity: format!("erc4626:{}", network_id.0),
467                attempted,
468                discovered,
469                skipped: failures.len(),
470                failures,
471            },
472        )
473    }
474}
475
476fn approx_whole_units(value: U256, decimals: u8) -> f64 {
477    value.to_string().parse::<f64>().unwrap_or(f64::INFINITY) / 10f64.powi(i32::from(decimals))
478}
479
480fn decimals_of(decimals: &HashMap<Address, u8>, token: Address) -> u8 {
481    decimals.get(&token).copied().unwrap_or(FALLBACK_DECIMALS)
482}
483
484fn v2_geometric_mean(pool: &DiscoveredPool, decimals: &HashMap<Address, u8>) -> f64 {
485    let units0 = approx_whole_units(pool.reserve0, decimals_of(decimals, pool.token0));
486    let units1 = approx_whole_units(pool.reserve1, decimals_of(decimals, pool.token1));
487    (units0 * units1).sqrt()
488}
489
490// V3 liquidity L is sqrt(x·y) over the virtual reserves, so normalizing by the
491// average token decimals yields the same whole-unit quantity scored for V2.
492fn v3_geometric_mean(pool: &DiscoveredPool, decimals: &HashMap<Address, u8>) -> f64 {
493    let scale = 10f64.powf(
494        f64::from(
495            u32::from(decimals_of(decimals, pool.token0))
496                + u32::from(decimals_of(decimals, pool.token1)),
497        ) / 2.0,
498    );
499    approx_whole_units(pool.score, 0) / scale
500}
501
502// Maps geometric-mean liquidity in whole token units onto 0..100, log-scaled
503// so pools rank by order of magnitude; saturates at one million whole units.
504fn liquidity_confidence(geometric_mean: f64) -> f64 {
505    if !geometric_mean.is_finite() || geometric_mean <= 0.0 {
506        return 0.0;
507    }
508    ((100.0 / 6.0) * (1.0 + geometric_mean).log10()).clamp(0.0, MAX_CONFIDENCE as f64)
509}
510
511// A pool that has not traded recently carries a spot price nobody has been
512// willing to arbitrage, so its validity decays with the age of the last trade.
513fn freshness_multiplier(age_seconds: f64) -> f64 {
514    if !age_seconds.is_finite() || age_seconds <= 0.0 {
515        return 1.0;
516    }
517    2f64.powf(-age_seconds / FRESHNESS_HALF_LIFE_SECONDS)
518}
519
520fn pool_confidence(
521    geometric_mean: f64,
522    block_timestamp: Option<u64>,
523    last_trade_timestamp: Option<u64>,
524) -> u64 {
525    let freshness = match (block_timestamp, last_trade_timestamp) {
526        (Some(now), Some(last_trade)) => {
527            freshness_multiplier(now.saturating_sub(last_trade) as f64)
528        }
529        _ => 1.0,
530    };
531    (liquidity_confidence(geometric_mean) * freshness).round() as u64
532}
533
534async fn fetch_decimals(provider: &RpcProvider, addresses: &[Address]) -> HashMap<Address, u8> {
535    join_all(addresses.iter().map(|address| {
536        let provider = provider.clone();
537        let address = *address;
538        async move {
539            let decimals = ERC20::new(address, &provider).decimals().call().await;
540            (address, decimals.unwrap_or(FALLBACK_DECIMALS))
541        }
542    }))
543    .await
544    .into_iter()
545    .collect()
546}
547
548async fn fetch_block_timestamp(provider: &RpcProvider) -> Option<u64> {
549    provider
550        .get_block_by_number(BlockNumberOrTag::Latest)
551        .await
552        .ok()
553        .flatten()
554        .map(|block| block.header.timestamp)
555}
556
557// The most recent oracle observation is written on the first swap of a block,
558// so its timestamp tells when the pool's spot price was last market-tested.
559async fn fetch_v3_last_trades(
560    provider: &RpcProvider,
561    pools: &[DiscoveredPool],
562) -> HashMap<Address, u64> {
563    join_all(pools.iter().map(|pool| {
564        let provider = provider.clone();
565        let pool_address = pool.pool_address;
566        async move {
567            let contract = UniswapV3Pool::new(pool_address, &provider);
568            let slot = contract.slot0().call().await.ok()?;
569            let observation = contract
570                .observations(U256::from(slot.observationIndex))
571                .call()
572                .await
573                .ok()?;
574            Some((pool_address, u64::from(observation.blockTimestamp)))
575        }
576    }))
577    .await
578    .into_iter()
579    .flatten()
580    .collect()
581}
582
583async fn discover_single_v2_pool(
584    provider: &RpcProvider,
585    factory: Address,
586    token_a: Address,
587    token_b: Address,
588) -> std::result::Result<Option<DiscoveredPool>, DiscoveryFailure> {
589    let failure = |target: String, message: String| DiscoveryFailure { target, message };
590    let v2_factory = UniswapV2Factory::new(factory, provider);
591    let pair = v2_factory
592        .getPair(token_a, token_b)
593        .call()
594        .await
595        .map_err(|error| {
596            failure(
597                format!("{token_a}/{token_b}"),
598                format!("getPair failed: {error}"),
599            )
600        })?;
601    if pair.is_zero() {
602        return Ok(None);
603    }
604
605    let pair_contract = UniswapV2Pair::new(pair, provider);
606    let token0 = pair_contract
607        .token0()
608        .call()
609        .await
610        .map_err(|error| failure(pair.to_string(), format!("token0 failed: {error}")))?;
611    let token1 = pair_contract
612        .token1()
613        .call()
614        .await
615        .map_err(|error| failure(pair.to_string(), format!("token1 failed: {error}")))?;
616
617    Ok(Some(DiscoveredPool {
618        pool_address: pair,
619        token0,
620        token1,
621        score: U256::ZERO,
622        kind: PoolKind::V2,
623        reserve0: U256::ZERO,
624        reserve1: U256::ZERO,
625        last_trade_timestamp: None,
626    }))
627}
628
629async fn discover_single_v3_pool(
630    provider: &RpcProvider,
631    factory: Address,
632    token_a: Address,
633    token_b: Address,
634    fee: u32,
635) -> std::result::Result<Option<DiscoveredPool>, DiscoveryFailure> {
636    let failure = |target: String, message: String| DiscoveryFailure { target, message };
637    let v3_factory = UniswapV3Factory::new(factory, provider);
638    let pool = v3_factory
639        .getPool(token_a, token_b, U24::from(fee))
640        .call()
641        .await
642        .map_err(|error| {
643            failure(
644                format!("{token_a}/{token_b}@{fee}"),
645                format!("getPool failed: {error}"),
646            )
647        })?;
648    if pool.is_zero() {
649        return Ok(None);
650    }
651
652    let pool_contract = UniswapV3Pool::new(pool, provider);
653    let token0 = pool_contract
654        .token0()
655        .call()
656        .await
657        .map_err(|error| failure(pool.to_string(), format!("token0 failed: {error}")))?;
658    let token1 = pool_contract
659        .token1()
660        .call()
661        .await
662        .map_err(|error| failure(pool.to_string(), format!("token1 failed: {error}")))?;
663    let liq: u128 = pool_contract
664        .liquidity()
665        .call()
666        .await
667        .map_err(|error| failure(pool.to_string(), format!("liquidity failed: {error}")))?;
668
669    Ok(Some(DiscoveredPool {
670        pool_address: pool,
671        token0,
672        token1,
673        score: U256::from(liq),
674        kind: PoolKind::V3(fee),
675        reserve0: U256::ZERO,
676        reserve1: U256::ZERO,
677        last_trade_timestamp: None,
678    }))
679}