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 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 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 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 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 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
490fn 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
502fn 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
511fn 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
557async 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}