Skip to main content

eth_prices/router/
auto.rs

1use std::collections::HashSet;
2
3use alloy::primitives::{Address, U256, address, aliases::U24};
4use futures::future::join_all;
5
6use crate::{
7    Result,
8    asset::identity::AssetIdentifier,
9    network::NetworkId,
10    provider::RpcProvider,
11    quoter::{
12        AnyQuoter,
13        erc4626::{ERC4626, ERC4626Quoter},
14        uniswap_v2::{UniswapV2Quoter, factory::UniswapV2Factory, pair::UniswapV2Pair},
15        uniswap_v3::{UniswapV3Quoter, factory::UniswapV3Factory, pool::UniswapV3Pool},
16    },
17    router::Router,
18};
19
20const UNISWAP_V2_FACTORY: Address = address!("0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f");
21const UNISWAP_V3_FACTORY: Address = address!("0x1F98431c8aD98523631AE4a59f267346ea31F984");
22const DEFAULT_V3_FEES: &[u32] = &[100, 500, 3000, 10000];
23const MAX_CONFIDENCE: u64 = 100;
24
25#[derive(Debug, Clone)]
26#[allow(dead_code)]
27enum PoolKind {
28    V2,
29    V3(u32),
30}
31
32#[derive(Debug, Clone)]
33#[allow(dead_code)]
34struct DiscoveredPool {
35    pool_address: Address,
36    token0: Address,
37    token1: Address,
38    score: U256,
39    kind: PoolKind,
40}
41
42#[derive(Debug, Clone)]
43pub struct AutoRouter {
44    provider: RpcProvider,
45    tokens: Vec<AssetIdentifier>,
46    network_id: Option<NetworkId>,
47    uniswap_v2_factory: Option<Address>,
48    uniswap_v3_factory: Option<Address>,
49    uniswap_v3_fees: Vec<u32>,
50    min_liquidity: Option<U256>,
51    discover_v2: bool,
52    discover_v3: bool,
53    discover_erc4626: bool,
54}
55
56impl AutoRouter {
57    pub fn new(provider: RpcProvider, tokens: Vec<AssetIdentifier>) -> Self {
58        Self {
59            provider,
60            tokens,
61            network_id: None,
62            uniswap_v2_factory: None,
63            uniswap_v3_factory: None,
64            uniswap_v3_fees: DEFAULT_V3_FEES.to_vec(),
65            min_liquidity: Some(U256::from(1)),
66            discover_v2: true,
67            discover_v3: true,
68            discover_erc4626: true,
69        }
70    }
71
72    pub fn with_network_id(mut self, network_id: NetworkId) -> Self {
73        self.network_id = Some(network_id);
74        self
75    }
76
77    pub fn with_uniswap_v2_factory(mut self, address: Address) -> Self {
78        self.uniswap_v2_factory = Some(address);
79        self
80    }
81
82    pub fn with_uniswap_v3_factory(mut self, address: Address) -> Self {
83        self.uniswap_v3_factory = Some(address);
84        self
85    }
86
87    pub fn with_uniswap_v3_fees(mut self, fees: Vec<u32>) -> Self {
88        self.uniswap_v3_fees = fees;
89        self
90    }
91
92    pub fn with_min_liquidity(mut self, min: U256) -> Self {
93        self.min_liquidity = Some(min);
94        self
95    }
96
97    pub fn discover_uniswap_v2(mut self, enable: bool) -> Self {
98        self.discover_v2 = enable;
99        self
100    }
101
102    pub fn discover_uniswap_v3(mut self, enable: bool) -> Self {
103        self.discover_v3 = enable;
104        self
105    }
106
107    pub fn discover_erc4626(mut self, enable: bool) -> Self {
108        self.discover_erc4626 = enable;
109        self
110    }
111
112    pub async fn build(self) -> Result<Router> {
113        let network_id = match self.network_id {
114            Some(ref id) => id.clone(),
115            None => NetworkId::from_provider(&self.provider).await?,
116        };
117
118        let mut all_quoters: Vec<AnyQuoter> = Vec::new();
119
120        // 1. ERC4626 discovery first — collect underlying tokens
121        let mut extra_addresses: Vec<Address> = Vec::new();
122        if self.discover_erc4626 {
123            let (erc4626_quoters, underlying) = self.discover_erc4626_quoters(&network_id).await;
124            all_quoters.extend(erc4626_quoters);
125            extra_addresses = underlying;
126        }
127
128        // 2. Build expanded address set (input tokens + ERC4626 underlyings)
129        let mut all_addresses: Vec<Address> = self.erc20_addresses();
130        let existing: HashSet<Address> = all_addresses.iter().copied().collect();
131        for addr in extra_addresses {
132            if !existing.contains(&addr) {
133                all_addresses.push(addr);
134            }
135        }
136
137        // 3. V2 discovery with expanded set
138        let mut v2_pools: Vec<DiscoveredPool> = Vec::new();
139        if self.discover_v2 {
140            v2_pools = Self::discover_v2_pools_inner(
141                &self.provider,
142                &all_addresses,
143                self.uniswap_v2_factory,
144            )
145            .await;
146            v2_pools = Self::filter_pools(Self::deduplicate_pools(v2_pools), &self.min_liquidity);
147        }
148
149        // 4. V3 discovery with expanded set
150        let mut v3_pools: Vec<DiscoveredPool> = Vec::new();
151        if self.discover_v3 {
152            v3_pools = Self::discover_v3_pools_inner(
153                &self.provider,
154                &all_addresses,
155                self.uniswap_v3_factory,
156                &self.uniswap_v3_fees,
157            )
158            .await;
159            v3_pools = Self::filter_pools(Self::deduplicate_pools(v3_pools), &self.min_liquidity);
160        }
161
162        // 5. Build quoters — V2 first so the Router's .find() prefers them
163        //    for direct routes (V2 spot prices are generally more reliable for
164        //    thin pools). V3 quoters remain as fallback for multi-hop paths.
165        for pool in v2_pools {
166            let quoter = UniswapV2Quoter {
167                network_id: network_id.clone(),
168                pair_address: pool.pool_address,
169                token0: pool.token0,
170                token1: pool.token1,
171            };
172            let confidence = pool_confidence_v2(pool.score);
173            all_quoters.push(AnyQuoter::from(quoter).with_confidence(confidence));
174        }
175
176        for pool in v3_pools {
177            let quoter = UniswapV3Quoter {
178                network_id: network_id.clone(),
179                pool_address: pool.pool_address,
180                token0: pool.token0,
181                token1: pool.token1,
182            };
183            let confidence = pool_confidence_v3(pool.score);
184            all_quoters.push(AnyQuoter::from(quoter).with_confidence(confidence));
185        }
186
187        if all_quoters.is_empty() {
188            return Err(crate::error::EthPricesError::AutoRouterNoPools);
189        }
190
191        Ok(Router::from_iter(all_quoters))
192    }
193
194    fn erc20_addresses(&self) -> Vec<Address> {
195        self.tokens
196            .iter()
197            .filter_map(|t| match t {
198                AssetIdentifier::ERC20 { address } => Some(*address),
199                _ => None,
200            })
201            .collect()
202    }
203
204    fn sorted_pair(a: Address, b: Address) -> (Address, Address) {
205        if a < b { (a, b) } else { (b, a) }
206    }
207
208    fn deduplicate_pools(pools: Vec<DiscoveredPool>) -> Vec<DiscoveredPool> {
209        let mut best: std::collections::HashMap<(Address, Address), DiscoveredPool> =
210            std::collections::HashMap::new();
211
212        for pool in pools {
213            let key = Self::sorted_pair(pool.token0, pool.token1);
214            match best.get(&key) {
215                Some(existing) if existing.score >= pool.score => continue,
216                _ => {
217                    best.insert(key, pool);
218                }
219            }
220        }
221
222        best.into_values().collect()
223    }
224
225    fn filter_pools(
226        pools: Vec<DiscoveredPool>,
227        min_liquidity: &Option<U256>,
228    ) -> Vec<DiscoveredPool> {
229        match min_liquidity {
230            Some(min) => pools.into_iter().filter(|p| p.score >= *min).collect(),
231            None => pools,
232        }
233    }
234
235    async fn discover_v2_pools_inner(
236        provider: &RpcProvider,
237        addresses: &[Address],
238        factory_opt: Option<Address>,
239    ) -> Vec<DiscoveredPool> {
240        let factory = factory_opt.unwrap_or(UNISWAP_V2_FACTORY);
241
242        let mut pairs = Vec::new();
243        for i in 0..addresses.len() {
244            for j in (i + 1)..addresses.len() {
245                pairs.push((addresses[i], addresses[j]));
246            }
247        }
248
249        let results: Vec<_> = join_all(pairs.into_iter().map(|(a, b)| {
250            let provider = provider.clone();
251            async move { discover_single_v2_pool(&provider, factory, a, b).await }
252        }))
253        .await;
254
255        let pools: Vec<DiscoveredPool> = results.into_iter().flatten().collect();
256
257        let liq_futures: Vec<_> = pools
258            .iter()
259            .map(|pool| {
260                let provider = provider.clone();
261                async move {
262                    let pair = UniswapV2Pair::new(pool.pool_address, &provider);
263                    match pair.getReserves().call().await {
264                        Ok(reserves) => {
265                            let reserve0 = U256::from(reserves.reserve0);
266                            let reserve1 = U256::from(reserves.reserve1);
267                            Some(std::cmp::min(reserve0, reserve1))
268                        }
269                        Err(_) => None,
270                    }
271                }
272            })
273            .collect();
274
275        let scores: Vec<Option<U256>> = join_all(liq_futures).await;
276
277        pools
278            .into_iter()
279            .zip(scores)
280            .filter_map(|(mut pool, score)| {
281                pool.score = score?;
282                Some(pool)
283            })
284            .collect()
285    }
286
287    async fn discover_v3_pools_inner(
288        provider: &RpcProvider,
289        addresses: &[Address],
290        factory_opt: Option<Address>,
291        fees: &[u32],
292    ) -> Vec<DiscoveredPool> {
293        let factory = factory_opt.unwrap_or(UNISWAP_V3_FACTORY);
294
295        if addresses.len() < 2 {
296            return Vec::new();
297        }
298
299        let mut queries = Vec::new();
300        for i in 0..addresses.len() {
301            for j in (i + 1)..addresses.len() {
302                let a = addresses[i];
303                let b = addresses[j];
304                for &fee in fees {
305                    queries.push((a, b, fee));
306                }
307            }
308        }
309
310        join_all(queries.into_iter().map(|(a, b, fee)| {
311            let provider = provider.clone();
312            async move { discover_single_v3_pool(&provider, factory, a, b, fee).await }
313        }))
314        .await
315        .into_iter()
316        .flatten()
317        .collect()
318    }
319
320    async fn discover_erc4626_quoters(
321        &self,
322        network_id: &NetworkId,
323    ) -> (Vec<AnyQuoter>, Vec<Address>) {
324        let addresses = self.erc20_addresses();
325
326        let results: Vec<_> = join_all(addresses.into_iter().map(|addr| {
327            let provider = self.provider.clone();
328            let net_id = network_id.clone();
329            async move {
330                match ERC4626::new(addr, &provider).asset().call().await {
331                    Ok(underlying) => {
332                        let quoter = ERC4626Quoter {
333                            network_id: net_id,
334                            vault_address: AssetIdentifier::ERC20 { address: addr },
335                            token_address: AssetIdentifier::ERC20 {
336                                address: underlying,
337                            },
338                        };
339                        Some((AnyQuoter::from(quoter).with_confidence(50), underlying))
340                    }
341                    Err(_) => None,
342                }
343            }
344        }))
345        .await;
346
347        let (quoters, underlying): (Vec<_>, Vec<_>) = results.into_iter().flatten().unzip();
348
349        (quoters, underlying)
350    }
351}
352
353fn pool_confidence_v2(score: U256) -> u64 {
354    if score.is_zero() {
355        return 0;
356    }
357    let divisor = U256::from(1_000_000_000u64);
358    let scaled = score / divisor;
359    if scaled >= U256::from(MAX_CONFIDENCE) {
360        MAX_CONFIDENCE
361    } else {
362        scaled.as_limbs()[0]
363    }
364}
365
366fn pool_confidence_v3(score: U256) -> u64 {
367    if score.is_zero() {
368        return 0;
369    }
370    let divisor = U256::from(10_000_000_000_000_000u64);
371    let scaled = score / divisor;
372    if scaled >= U256::from(MAX_CONFIDENCE) {
373        MAX_CONFIDENCE
374    } else {
375        scaled.as_limbs()[0]
376    }
377}
378
379async fn discover_single_v2_pool(
380    provider: &RpcProvider,
381    factory: Address,
382    token_a: Address,
383    token_b: Address,
384) -> Option<DiscoveredPool> {
385    let v2_factory = UniswapV2Factory::new(factory, provider);
386    let pair = v2_factory.getPair(token_a, token_b).call().await.ok()?;
387    if pair.is_zero() {
388        return None;
389    }
390
391    let pair_contract = UniswapV2Pair::new(pair, provider);
392    let token0 = pair_contract.token0().call().await.ok()?;
393    let token1 = pair_contract.token1().call().await.ok()?;
394
395    Some(DiscoveredPool {
396        pool_address: pair,
397        token0,
398        token1,
399        score: U256::ZERO,
400        kind: PoolKind::V2,
401    })
402}
403
404async fn discover_single_v3_pool(
405    provider: &RpcProvider,
406    factory: Address,
407    token_a: Address,
408    token_b: Address,
409    fee: u32,
410) -> Option<DiscoveredPool> {
411    let v3_factory = UniswapV3Factory::new(factory, provider);
412    let pool = v3_factory
413        .getPool(token_a, token_b, U24::from(fee))
414        .call()
415        .await
416        .ok()?;
417    if pool.is_zero() {
418        return None;
419    }
420
421    let pool_contract = UniswapV3Pool::new(pool, provider);
422    let token0 = pool_contract.token0().call().await.ok()?;
423    let token1 = pool_contract.token1().call().await.ok()?;
424    let liq: u128 = pool_contract.liquidity().call().await.ok()?;
425
426    Some(DiscoveredPool {
427        pool_address: pool,
428        token0,
429        token1,
430        score: U256::from(liq),
431        kind: PoolKind::V3(fee),
432    })
433}