Skip to main content

tycho_simulation/price_level_stream/
fallback_router.rs

1//! Reads the venue whitelist of Titan's PropAMMRouter.
2//!
3//! Venues on the whitelist may be served under the `propammfallback:` protocol family, which
4//! executes their swaps through the router instead of the venue directly, so a stale maker
5//! quote falls back to a single-hop Uniswap V3 pool instead of reverting the route.
6
7use alloy::{
8    network::Ethereum,
9    primitives::{address, Address, TxKind},
10    providers::{Provider, ProviderBuilder, RootProvider},
11    rpc::types::TransactionRequest,
12    sol,
13    sol_types::SolCall,
14};
15use tycho_common::Bytes;
16
17/// Titan's PropAMMRouter deployment on Ethereum mainnet: written by LambdaClass, behind a UUPS
18/// proxy so upgrades keep the address.
19///
20/// Must match `tycho-execution`'s `PropAMMFallbackExecutor.PROPAMM_ROUTER`.
21/// <https://github.com/lambdaclass/propamm-router-contracts>
22pub const FALLBACK_ROUTER_ADDRESS: Address = address!("4DdF368080CD7946db5b459aD591c350158175e1");
23
24sol! {
25    /// The whitelist accessor of the PropAMMRouter. The swap surface the executor uses lives in
26    /// `tycho-execution`'s `IPropAMMRouter.sol`.
27    function getWhitelistedVenues() external view returns (address[] memory venues);
28}
29
30/// Error reading the PropAMMRouter's venue whitelist.
31#[derive(Debug, thiserror::Error)]
32pub enum FetchVenuesError {
33    /// The RPC URL could not be parsed.
34    #[error("invalid RPC URL {url:?}: {reason}")]
35    InvalidUrl {
36        /// The URL that failed to parse.
37        url: String,
38        /// The parse error.
39        reason: String,
40    },
41    /// The `eth_call` failed or returned undecodable data.
42    #[error("getWhitelistedVenues call to the PropAMMRouter failed: {reason}")]
43    Call {
44        /// Underlying transport or ABI decoding error.
45        reason: String,
46    },
47}
48
49/// Reads the router's whitelisted pAMM venues via `eth_call` on the node at `rpc_url`.
50///
51/// Read once at startup: the whitelist is governance-gated and changes rarely, and renaming a
52/// running component's protocol system would churn every consumer's component set.
53///
54/// # Errors
55///
56/// Returns [`FetchVenuesError::InvalidUrl`] if `rpc_url` does not parse, and
57/// [`FetchVenuesError::Call`] if the `eth_call` fails or returns undecodable data.
58pub async fn fetch_fallback_router_venues(rpc_url: &str) -> Result<Vec<Bytes>, FetchVenuesError> {
59    let url: reqwest::Url = rpc_url
60        .parse()
61        .map_err(|e| FetchVenuesError::InvalidUrl {
62            url: rpc_url.to_string(),
63            reason: format!("{e}"),
64        })?;
65    let provider: RootProvider<Ethereum> = ProviderBuilder::default().connect_http(url);
66    let response = provider
67        .call(TransactionRequest {
68            to: Some(TxKind::Call(FALLBACK_ROUTER_ADDRESS)),
69            input: getWhitelistedVenuesCall {}
70                .abi_encode()
71                .into(),
72            ..Default::default()
73        })
74        .await
75        .map_err(|e| FetchVenuesError::Call { reason: e.to_string() })?;
76    let venues = getWhitelistedVenuesCall::abi_decode_returns(&response).map_err(|e| {
77        FetchVenuesError::Call { reason: format!("failed to decode response: {e}") }
78    })?;
79    Ok(venues
80        .into_iter()
81        .map(|venue| Bytes::from(venue.as_slice().to_vec()))
82        .collect())
83}
84
85#[cfg(test)]
86mod tests {
87    use std::str::FromStr;
88
89    use super::*;
90
91    #[tokio::test]
92    #[ignore = "Requires RPC_URL to be set in environment variables or .env file"]
93    async fn test_fetch_fallback_router_venues_against_mainnet() {
94        let rpc_url = std::env::var("RPC_URL").expect("RPC_URL must be set for network tests");
95
96        let venues = fetch_fallback_router_venues(&rpc_url)
97            .await
98            .expect("whitelist read should succeed");
99
100        // FermiSwap is whitelisted on the live router.
101        let fermiswap =
102            Bytes::from_str("0x5979458912f80b96d30d4220af8e2e4925a33320").expect("valid address");
103        assert!(venues.contains(&fermiswap), "expected FermiSwap in {venues:?}");
104    }
105
106    #[tokio::test]
107    async fn test_fetch_fallback_router_venues_invalid_url() {
108        let result = fetch_fallback_router_venues("not a url").await;
109        assert!(matches!(result, Err(FetchVenuesError::InvalidUrl { .. })));
110    }
111
112    /// Reading the whitelist from a different router than the executor calls would let a venue
113    /// be served under `propammfallback:` that the executed router rejects.
114    #[test]
115    fn test_router_address_matches_the_executor() {
116        let executor = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
117            .join("../tycho-execution/contracts/src/executors/PropAMMFallbackExecutor.sol");
118        let source = std::fs::read_to_string(&executor)
119            .unwrap_or_else(|e| panic!("failed to read {}: {e}", executor.display()));
120
121        let address = FALLBACK_ROUTER_ADDRESS.to_string();
122        assert!(source.contains(&address), "PropAMMFallbackExecutor.sol does not use {address}");
123    }
124}