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#[cfg(test)]
130mod tests {
131 use super::*;
132
133 /// The timings only keep inline fetches off the encoding path while a timed-out refresh plus
134 /// the retry that follows it still fit inside the maximum window age.
135 #[test]
136 fn test_one_timed_out_refresh_cannot_stale_the_window() {
137 let slowest_recovery =
138 ANGSTROM_API_TIMEOUT + ANGSTROM_ATTESTATION_REFRESH_INTERVAL + ANGSTROM_API_TIMEOUT;
139
140 assert!(
141 slowest_recovery <= ANGSTROM_ATTESTATION_MAX_AGE,
142 "a single timed-out refresh leaves the window stale for {slowest_recovery:?}, past \
143 the {ANGSTROM_ATTESTATION_MAX_AGE:?} maximum age"
144 );
145 }
146}