Skip to main content

tycho_execution/encoding/evm/
constants.rs

1use std::{
2    collections::{HashMap, HashSet},
3    sync::LazyLock,
4    time::Duration,
5};
6
7use tycho_common::{models::Chain, Bytes};
8
9use crate::encoding::errors::EncodingError;
10
11pub(crate) const DEFAULT_EXECUTORS_JSON: &str =
12    include_str!("../../../config/executor_addresses.json");
13pub(crate) const DEFAULT_ROUTERS_JSON: &str = include_str!("../../../config/router_addresses.json");
14pub(crate) const PROTOCOL_SPECIFIC_CONFIG: &str =
15    include_str!("../../../config/protocol_specific_addresses.json");
16
17/// Default router addresses keyed by chain, parsed from `config/router_addresses.json`.
18pub static DEFAULT_ROUTER_ADDRESSES: LazyLock<HashMap<Chain, Bytes>> = LazyLock::new(|| {
19    serde_json::from_str(DEFAULT_ROUTERS_JSON).expect("valid router_addresses.json")
20});
21
22/// Returns the default Tycho router address for `chain`, or an error if none is configured.
23pub fn get_router_address(chain: &Chain) -> Result<&'static Bytes, EncodingError> {
24    DEFAULT_ROUTER_ADDRESSES
25        .get(chain)
26        .ok_or_else(|| {
27            EncodingError::FatalError(format!(
28                "No default router address found for chain {chain:?}"
29            ))
30        })
31}
32
33/// The address used by the TychoRouterV3 to represent native ETH.
34///
35/// Callers must use this address (not `address(0)`) for the `tokenIn` / `tokenOut`
36/// parameters when ABI-encoding router function calls that involve native ETH.
37/// The encoding pipeline's `EncodedSolution` only contains the inner swap bytes;
38/// the outer function arguments — including the token addresses — are the caller's
39/// responsibility.
40pub static ROUTER_ETH_ADDRESS: LazyLock<Bytes> = LazyLock::new(|| {
41    Bytes::from(alloy::primitives::hex!("EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").to_vec())
42});
43
44/// The number of blocks in the future for which to fetch Angstrom Attestations
45///
46/// It is important to note that fetching more blocks will send more attestations to the
47/// Tycho Router, resulting in a higher gas usage. Fetching fewer blocks may result in attestations
48/// expiring if the transaction is not sent fast enough.
49pub const ANGSTROM_DEFAULT_BLOCKS_IN_FUTURE: u64 = 5;
50
51/// The endpoint serving Angstrom pool unlock attestations.
52pub(crate) const ANGSTROM_DEFAULT_API_URL: &str =
53    "https://attestations.angstrom.xyz/getAttestations";
54
55/// The size of a single Angstrom attestation, without its block number prefix.
56///
57/// The Uniswap V4 executor rejects attestation data that is not a whole number of
58/// `8 + ANGSTROM_ATTESTATION_SIZE` byte entries.
59pub(crate) const ANGSTROM_ATTESTATION_SIZE: usize = 85;
60
61/// The shortest time Ethereum can take to produce a block, which both the refresh interval and
62/// the maximum window age derive from.
63///
64/// Ethereum proposes at most one block every 12 seconds, and a skipped proposal only makes the
65/// gap longer. Treating 12 seconds as one block therefore always overestimates how many blocks
66/// have elapsed, which is the safe direction for both constants below.
67const ETHEREUM_MIN_BLOCK_TIME_SECS: u64 = 12;
68
69/// How many times per block the background prefetcher refreshes the attestation window.
70///
71/// The window's contents only change when a block is produced, so refreshing more than once per
72/// block fetches nothing new. It is still more than once because the refresher has no block feed
73/// to align to: sampling twice a block bounds how long it keeps serving the previous block's
74/// window after a new one becomes available, without polling the API for the sake of it.
75const ANGSTROM_ATTESTATION_REFRESHES_PER_BLOCK: u64 = 2;
76
77/// How long the background prefetcher waits between Angstrom attestation refreshes.
78pub(crate) const ANGSTROM_ATTESTATION_REFRESH_INTERVAL: Duration =
79    Duration::from_secs(ETHEREUM_MIN_BLOCK_TIME_SECS / ANGSTROM_ATTESTATION_REFRESHES_PER_BLOCK);
80
81/// How many of the fetched window's blocks may elapse before the cache refetches while encoding.
82///
83/// A window fetched during block `N` covers `N` through `N + ANGSTROM_BLOCKS_IN_FUTURE`. Every
84/// block that elapses before encoding spends one of those: it removes a block the transaction
85/// could still have landed in, and adds an attestation the executor will skip. Keeping this at a
86/// single block preserves all but one block of the caller's slack, at the price of refetching
87/// inline sooner when the background refresh stalls.
88const ANGSTROM_ATTESTATION_MAX_AGE_BLOCKS: u64 = 1;
89
90/// How old a cached Angstrom attestation window may be before it is refetched while encoding.
91///
92/// Only reached when the background refresh has stopped keeping up: a healthy refresher replaces
93/// the window every `ANGSTROM_ATTESTATION_REFRESH_INTERVAL`.
94pub(crate) const ANGSTROM_ATTESTATION_MAX_AGE: Duration =
95    Duration::from_secs(ETHEREUM_MIN_BLOCK_TIME_SECS * ANGSTROM_ATTESTATION_MAX_AGE_BLOCKS);
96
97/// How long a single request to the Angstrom API may take before it is aborted.
98///
99/// Half the refresh interval, so one timed-out refresh cannot reach the encoding path: the next
100/// refresh still replaces the window within `ANGSTROM_ATTESTATION_MAX_AGE` of the previous one
101/// (3s aborted + 6s sleep + at most 3s for the retry). The slowest read measured against the
102/// live API was 902ms, including DNS and TLS on a cold connection.
103pub(crate) const ANGSTROM_API_TIMEOUT: Duration =
104    Duration::from_secs(ANGSTROM_ATTESTATION_REFRESH_INTERVAL.as_secs() / 2);
105
106/// These protocols support the optimization of grouping swaps.
107///
108/// This requires special encoding to send call data of multiple swaps to a single executor,
109/// as if it were a single swap. The protocol likely uses flash accounting to save gas on token
110/// transfers.
111pub static GROUPABLE_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
112    let mut set = HashSet::new();
113    set.insert("uniswap_v4");
114    set.insert("uniswap_v4_hooks");
115    set.insert("vm:balancer_v3");
116    set.insert("ekubo_v2");
117    set.insert("ekubo_v3");
118    set
119});
120
121/// These groupable protocols use simple concatenation instead of PLE when forming swap groups.
122pub static NON_PLE_ENCODED_PROTOCOLS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
123    let mut set = HashSet::new();
124    set.insert("ekubo_v2");
125    set.insert("ekubo_v3");
126    set
127});
128
129/// Protocol system prefix carried by components sourced from the pAMM price level stream. The
130/// venue suffix is either a configured name (e.g. `pricelevelstream:fermiswap`) or, for
131/// auto-detected pAMMs, the venue address (e.g. `pricelevelstream:0x5979…`); every such protocol
132/// maps to the generic `PropAMMSwapEncoder`.
133pub const PRICE_LEVEL_STREAM_PREFIX: &str = "pricelevelstream:";
134
135/// The executor-config key serving the whole price-level-stream protocol family: any
136/// `pricelevelstream:{venue}` protocol without an exact entry of its own falls back to this one,
137/// so a single configured executor address covers every pAMM, including auto-detected ones.
138pub const PRICE_LEVEL_STREAM_KEY: &str = "pricelevelstream";
139
140/// Protocol system prefix for pAMM components executed through the PropAMMRouter, so a stale maker
141/// quote retries on Uniswap V3 instead of reverting the route. Venue suffixes follow
142/// `PRICE_LEVEL_STREAM_PREFIX`; only whitelisted venues may use it. Calldata matches the direct
143/// path, so both prefixes share `PropAMMSwapEncoder` and differ only in the executor.
144pub const PROPAMM_FALLBACK_PREFIX: &str = "propammfallback:";
145
146/// The executor-config key serving the whole PropAMMRouter protocol family, mirroring
147/// `PRICE_LEVEL_STREAM_KEY`.
148pub const PROPAMM_FALLBACK_KEY: &str = "propammfallback";
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    /// `get_encoder` matches on the prefix but looks the executor up under the key, so the two
155    /// must name the same family.
156    #[test]
157    fn test_family_keys_and_prefixes_agree() {
158        assert_eq!(format!("{PRICE_LEVEL_STREAM_KEY}:"), PRICE_LEVEL_STREAM_PREFIX);
159        assert_eq!(format!("{PROPAMM_FALLBACK_KEY}:"), PROPAMM_FALLBACK_PREFIX);
160    }
161
162    /// The timings only keep inline fetches off the encoding path while a timed-out refresh plus
163    /// the retry that follows it still fit inside the maximum window age.
164    #[test]
165    fn test_one_timed_out_refresh_cannot_stale_the_window() {
166        let slowest_recovery =
167            ANGSTROM_API_TIMEOUT + ANGSTROM_ATTESTATION_REFRESH_INTERVAL + ANGSTROM_API_TIMEOUT;
168
169        assert!(
170            slowest_recovery <= ANGSTROM_ATTESTATION_MAX_AGE,
171            "a single timed-out refresh leaves the window stale for {slowest_recovery:?}, past \
172             the {ANGSTROM_ATTESTATION_MAX_AGE:?} maximum age"
173        );
174    }
175}