Skip to main content

tycho_execution/encoding/evm/swap_encoder/
swap_encoder_registry.rs

1use std::{collections::HashMap, str::FromStr};
2
3use tycho_common::{models::Chain, Bytes};
4
5use crate::encoding::{
6    errors::EncodingError,
7    evm::{
8        constants::{
9            DEFAULT_EXECUTORS_JSON, FALLBACK_KEY, FALLBACK_PREFIX, PRICE_LEVEL_STREAM_KEY,
10            PRICE_LEVEL_STREAM_PREFIX, PROPAMM_FALLBACK_KEY, PROPAMM_FALLBACK_PREFIX,
11            PROTOCOL_SPECIFIC_CONFIG, UNISWAP_V2_FORKS, UNISWAP_V3_FORKS,
12        },
13        swap_encoder::{
14            aerodrome_v1::AerodromeV1SwapEncoder, balancer_v2::BalancerV2SwapEncoder,
15            balancer_v3::BalancerV3SwapEncoder, bebop::BebopSwapEncoder, bopamm::BopAMMSwapEncoder,
16            curve::CurveSwapEncoder, ekubo::EkuboSwapEncoder, ekubo_v3::EkuboV3SwapEncoder,
17            erc_4626::ERC4626SwapEncoder, etherfi::EtherfiSwapEncoder,
18            fallback::FallbackSwapEncoder, fermiswap::FermiSwapEncoder,
19            fluid_v1::FluidV1SwapEncoder, hashflow::HashflowSwapEncoder,
20            liquidity_party::LiquidityPartySwapEncoder, liquorice::LiquoriceSwapEncoder,
21            lunarbase::LunarBaseSwapEncoder, maverick_v2::MaverickV2SwapEncoder,
22            metric::MetricSwapEncoder, native::NativeSwapEncoder, native_wrap::WrapSwapEncoder,
23            propamm::PropAMMSwapEncoder, ring_swap_v2::RingSwapV2SwapEncoder,
24            rocketpool::RocketpoolSwapEncoder, sky::SkySwapEncoder,
25            slipstreams::SlipstreamsSwapEncoder, uniswap_v2::UniswapV2SwapEncoder,
26            uniswap_v3::UniswapV3SwapEncoder, uniswap_v4::UniswapV4SwapEncoder,
27        },
28    },
29    swap_encoder::SwapEncoder,
30};
31
32/// Registry containing all supported `SwapEncoders`.
33#[derive(Clone)]
34pub struct SwapEncoderRegistry {
35    chain: Chain,
36    /// A hashmap containing the protocol system as a key and the `SwapEncoder` as a value.
37    encoders: HashMap<String, Box<dyn SwapEncoder>>,
38}
39
40impl SwapEncoderRegistry {
41    pub fn new(chain: Chain) -> Self {
42        Self { chain, encoders: HashMap::new() }
43    }
44
45    /// Creates a new registry pre-populated with all default encoders for the given chain.
46    pub fn new_with_defaults(chain: Chain) -> Result<Self, EncodingError> {
47        Self::new(chain).add_default_encoders(None)
48    }
49
50    /// Populates the registry with the default `SwapEncoders` for the given blockchain by
51    /// parsing the executors' addresses in the file at the given path.
52    pub fn add_default_encoders(
53        mut self,
54        executors_addresses: Option<String>,
55    ) -> Result<Self, EncodingError> {
56        let config_str = if let Some(addresses) = executors_addresses {
57            addresses
58        } else {
59            DEFAULT_EXECUTORS_JSON.to_string()
60        };
61        let config: HashMap<Chain, HashMap<String, String>> = serde_json::from_str(&config_str)?;
62        let executors = config
63            .get(&self.chain)
64            .ok_or(EncodingError::FatalError("No executors found for chain".to_string()))?;
65
66        let protocol_specific_config: HashMap<Chain, HashMap<String, HashMap<String, String>>> =
67            serde_json::from_str(PROTOCOL_SPECIFIC_CONFIG)?;
68        let protocol_specific_config = protocol_specific_config
69            .get(&self.chain)
70            .ok_or(EncodingError::FatalError(
71                "No protocol specific config found for chain".to_string(),
72            ))?;
73        for (protocol, executor_address) in executors {
74            let encoder = self.create_encoder(
75                protocol,
76                Bytes::from_str(executor_address).map_err(|_| {
77                    EncodingError::FatalError(format!(
78                        "Invalid executor address for protocol {}",
79                        protocol
80                    ))
81                })?,
82                protocol_specific_config
83                    .get(protocol)
84                    .cloned(),
85            )?;
86            self.encoders
87                .insert(protocol.to_string(), encoder);
88        }
89        Ok(self)
90    }
91
92    /// Adds an encoder to the registry, replacing any existing encoder for the same protocol.
93    pub fn register_encoder(mut self, protocol: &str, encoder: Box<dyn SwapEncoder>) -> Self {
94        self.encoders
95            .insert(protocol.to_string(), encoder);
96        self
97    }
98
99    /// Returns the encoder registered for `protocol_system`.
100    ///
101    /// Price-level-stream protocols (`pricelevelstream:{protocol}`) without an exact entry fall
102    /// back to the family entry registered under `pricelevelstream`, so a single configured
103    /// executor address serves every pAMM — including auto-detected, address-named ones.
104    /// `propammfallback:{protocol}` and `fallback:{protocol}` resolve the same way against
105    /// `propammfallback` and `fallback`.
106    #[allow(clippy::borrowed_box)]
107    pub fn get_encoder(&self, protocol_system: &str) -> Option<&Box<dyn SwapEncoder>> {
108        if let Some(encoder) = self.encoders.get(protocol_system) {
109            return Some(encoder);
110        }
111        if protocol_system.starts_with(PRICE_LEVEL_STREAM_PREFIX) {
112            return self
113                .encoders
114                .get(PRICE_LEVEL_STREAM_KEY);
115        }
116        if protocol_system.starts_with(PROPAMM_FALLBACK_PREFIX) {
117            return self.encoders.get(PROPAMM_FALLBACK_KEY);
118        }
119        if protocol_system.starts_with(FALLBACK_PREFIX) {
120            return self.encoders.get(FALLBACK_KEY);
121        }
122        None
123    }
124
125    /// The executor address of every encoder in this registry, keyed by protocol system.
126    ///
127    /// Several protocol systems may share one executor address, so the returned addresses are not
128    /// necessarily distinct.
129    pub fn executor_addresses(&self) -> HashMap<String, Bytes> {
130        self.encoders
131            .iter()
132            .map(|(protocol, encoder)| (protocol.clone(), encoder.executor_address().clone()))
133            .collect()
134    }
135
136    fn create_encoder(
137        &self,
138        protocol_system: &str,
139        executor_address: Bytes,
140        config: Option<HashMap<String, String>>,
141    ) -> Result<Box<dyn SwapEncoder>, EncodingError> {
142        match protocol_system {
143            p if UNISWAP_V2_FORKS.contains(&p) => {
144                Ok(Box::new(UniswapV2SwapEncoder::new(executor_address, self.chain, config)?))
145            }
146            "ring_swap_v2" => {
147                Ok(Box::new(RingSwapV2SwapEncoder::new(executor_address, self.chain, config)?))
148            }
149            "aerodrome_v1" => {
150                Ok(Box::new(AerodromeV1SwapEncoder::new(executor_address, self.chain, config)?))
151            }
152            "vm:balancer_v2" => {
153                Ok(Box::new(BalancerV2SwapEncoder::new(executor_address, self.chain, config)?))
154            }
155            p if UNISWAP_V3_FORKS.contains(&p) => {
156                Ok(Box::new(UniswapV3SwapEncoder::new(executor_address, self.chain, config)?))
157            }
158            "uniswap_v4" => {
159                Ok(Box::new(UniswapV4SwapEncoder::new(executor_address, self.chain, config)?))
160            }
161            "ekubo_v2" => {
162                Ok(Box::new(EkuboSwapEncoder::new(executor_address, self.chain, config)?))
163            }
164            "ekubo_v3" => {
165                Ok(Box::new(EkuboV3SwapEncoder::new(executor_address, self.chain, config)?))
166            }
167            "vm:bopamm" => {
168                Ok(Box::new(BopAMMSwapEncoder::new(executor_address, self.chain, config)?))
169            }
170            "vm:curve" => {
171                Ok(Box::new(CurveSwapEncoder::new(executor_address, self.chain, config)?))
172            }
173            "vm:maverick_v2" => {
174                Ok(Box::new(MaverickV2SwapEncoder::new(executor_address, self.chain, config)?))
175            }
176            "vm:balancer_v3" => {
177                Ok(Box::new(BalancerV3SwapEncoder::new(executor_address, self.chain, config)?))
178            }
179            "rfq:bebop" => {
180                Ok(Box::new(BebopSwapEncoder::new(executor_address, self.chain, config)?))
181            }
182            "rfq:hashflow" => {
183                Ok(Box::new(HashflowSwapEncoder::new(executor_address, self.chain, config)?))
184            }
185            "rfq:liquorice" => {
186                Ok(Box::new(LiquoriceSwapEncoder::new(executor_address, self.chain, config)?))
187            }
188            "rfq:metric" => {
189                Ok(Box::new(MetricSwapEncoder::new(executor_address, self.chain, config)?))
190            }
191            "rfq:native" => {
192                Ok(Box::new(NativeSwapEncoder::new(executor_address, self.chain, config)?))
193            }
194            "fluid_v1" => {
195                Ok(Box::new(FluidV1SwapEncoder::new(executor_address, self.chain, config)?))
196            }
197            "vm:fermiswap" => {
198                Ok(Box::new(FermiSwapEncoder::new(executor_address, self.chain, config)?))
199            }
200            "vm:liquidityparty" => {
201                Ok(Box::new(LiquidityPartySwapEncoder::new(executor_address, self.chain, config)?))
202            }
203            "aerodrome_slipstreams" => {
204                Ok(Box::new(SlipstreamsSwapEncoder::new(executor_address, self.chain, config)?))
205            }
206            "rocketpool" => {
207                Ok(Box::new(RocketpoolSwapEncoder::new(executor_address, self.chain, config)?))
208            }
209            "sky" => Ok(Box::new(SkySwapEncoder::new(executor_address, self.chain, config)?)),
210            "erc4626" => {
211                Ok(Box::new(ERC4626SwapEncoder::new(executor_address, self.chain, config)?))
212            }
213            "lunarbase" => {
214                Ok(Box::new(LunarBaseSwapEncoder::new(executor_address, self.chain, config)?))
215            }
216            "velodrome_slipstreams" => {
217                Ok(Box::new(SlipstreamsSwapEncoder::new(executor_address, self.chain, config)?))
218            }
219            // UP on Robinhood Chain deploys the Slipstream contracts verbatim, and its pools price
220            // swaps through a dynamic fee module, so it encodes like the other Slipstream forks.
221            "up_v3" => {
222                Ok(Box::new(SlipstreamsSwapEncoder::new(executor_address, self.chain, config)?))
223            }
224            // Ramses V3 reuses the standard Uniswap V3 executor unchanged, encoded via the
225            // Slipstreams encoder. Three things make this sound:
226            //   1. ABI match: the Ramses pool exposes the identical
227            //      `swap(address,bool,int256,uint160,bytes)` and calls `uniswapV3SwapCallback`,
228            //      which the router's selector-agnostic fallback routes back to the executor.
229            //   2. The executor's `_decodeData` reads only the pool address (bytes 43..63) and the
230            //      zero-for-one flag (byte 63): it calls `pool.swap` on that address without
231            //      recomputing it, and never touches the 3-byte slot at bytes 40..43. So it is
232            //      irrelevant both that Ramses keys pools by tick spacing rather than fee, and that
233            //      the Slipstreams encoder packs `tick_spacing` into that slot (where Uniswap V3
234            //      packs the fee).
235            //   3. The SlipstreamsExecutor contract is byte-for-byte identical to the
236            //      UniswapV3Executor, so the encoder choice does not imply a different on-chain
237            //      executor.
238            "ramses_v3" => {
239                Ok(Box::new(SlipstreamsSwapEncoder::new(executor_address, self.chain, config)?))
240            }
241            "native_wrapper" => {
242                Ok(Box::new(WrapSwapEncoder::new(executor_address, self.chain, config)?))
243            }
244            "etherfi" => {
245                Ok(Box::new(EtherfiSwapEncoder::new(executor_address, self.chain, config)?))
246            }
247            // All pAMMs following the standard IPropAMM interface share one generic encoder /
248            // executor; the concrete protocol is identified by the component, not the encoder. The
249            // bare family key serves every protocol via the `get_encoder` fallback;
250            // protocol-specific `pricelevelstream:{protocol}` entries override it per
251            // protocol. The PropAMMRouter path takes the same calldata, so it reuses
252            // the same encoder and differs only in the executor address configured for
253            // the family.
254            pls if pls == PRICE_LEVEL_STREAM_KEY ||
255                pls.starts_with(PRICE_LEVEL_STREAM_PREFIX) ||
256                pls == PROPAMM_FALLBACK_KEY ||
257                pls.starts_with(PROPAMM_FALLBACK_PREFIX) =>
258            {
259                Ok(Box::new(PropAMMSwapEncoder::new(executor_address, self.chain, config)?))
260            }
261            // The TychoFallbackRouter path carries the fallback protocol in the swap data, so it
262            // needs its own encoder; the family resolves like the price-level-stream one.
263            f if f == FALLBACK_KEY || f.starts_with(FALLBACK_PREFIX) => {
264                Ok(Box::new(FallbackSwapEncoder::new(executor_address, self.chain, config)?))
265            }
266            _ => Err(EncodingError::FatalError(format!(
267                "Unknown protocol system: {}",
268                protocol_system
269            ))),
270        }
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    /// A single `pricelevelstream` config entry serves the whole protocol family: the bare
279    /// family key resolves as an exact entry, and every `pricelevelstream:{protocol}` protocol —
280    /// including auto-detected, address-named protocols no config could enumerate — resolves to it
281    /// through the fallback.
282    #[test]
283    fn test_price_level_stream_protocols_route_to_generic_encoder() {
284        let executors = std::fs::read_to_string("config/test_executor_addresses.json").unwrap();
285        let registry = SwapEncoderRegistry::new(Chain::Ethereum)
286            .add_default_encoders(Some(executors))
287            .unwrap();
288
289        for protocol in [
290            PRICE_LEVEL_STREAM_KEY,
291            "pricelevelstream:fermiswap",
292            "pricelevelstream:kipseli",
293            "pricelevelstream:0x2222222222222222222222222222222222222222",
294        ] {
295            assert!(registry.get_encoder(protocol).is_some(), "no encoder resolved for {protocol}");
296        }
297        // The fallback is scoped to the price-level-stream prefix.
298        assert!(registry
299            .get_encoder("unknown_protocol")
300            .is_none());
301    }
302
303    /// The PropAMMRouter family resolves the same way, and to a different executor than the direct
304    /// path — same calldata, different call target.
305    #[test]
306    fn test_propamm_fallback_protocol_resolution() {
307        let executors = std::fs::read_to_string("config/test_executor_addresses.json").unwrap();
308        let registry = SwapEncoderRegistry::new(Chain::Ethereum)
309            .add_default_encoders(Some(executors))
310            .unwrap();
311
312        for protocol in [
313            PROPAMM_FALLBACK_KEY,
314            "propammfallback:fermiswap",
315            "propammfallback:0x5979458912f80b96d30d4220af8e2e4925a33320",
316        ] {
317            assert!(registry.get_encoder(protocol).is_some(), "no encoder resolved for {protocol}");
318        }
319
320        let direct = registry
321            .get_encoder("pricelevelstream:fermiswap")
322            .unwrap()
323            .executor_address()
324            .clone();
325        let via_router = registry
326            .get_encoder("propammfallback:fermiswap")
327            .unwrap()
328            .executor_address()
329            .clone();
330        assert_ne!(direct, via_router);
331    }
332
333    /// The TychoFallbackRouter family resolves like the other two pAMM families, against its own
334    /// encoder. No `fallback` entry ships in the executor configs until the FallbackExecutor is
335    /// deployed, so the test registers the family key itself.
336    #[test]
337    fn test_fallback_protocol_resolution() {
338        let executor_address =
339            Bytes::from_str("0x5c2f5a71f67c01775180adc06909288b4c329308").unwrap();
340        let registry = SwapEncoderRegistry::new(Chain::Ethereum);
341        let config = HashMap::from([(
342            "angstrom_hook_address".to_string(),
343            "0x0000000aa232009084Bd71A5797d089AA4Edfad4".to_string(),
344        )]);
345        let encoder = registry
346            .create_encoder(FALLBACK_KEY, executor_address.clone(), Some(config))
347            .unwrap();
348        let registry = registry.register_encoder(FALLBACK_KEY, encoder);
349
350        for protocol in [
351            FALLBACK_KEY,
352            "fallback:fermiswap",
353            "fallback:0x5979458912f80b96d30d4220af8e2e4925a33320",
354        ] {
355            let resolved = registry
356                .get_encoder(protocol)
357                .unwrap_or_else(|| panic!("no encoder resolved for {protocol}"));
358            assert_eq!(resolved.executor_address(), &executor_address);
359        }
360        // The family fallback is scoped to the prefix.
361        assert!(registry
362            .get_encoder("fallbackless_protocol")
363            .is_none());
364    }
365
366    #[test]
367    fn test_default_encoders_build_for_every_configured_chain() {
368        let chains = [
369            Chain::Ethereum,
370            Chain::Base,
371            Chain::Unichain,
372            Chain::Arbitrum,
373            Chain::Bsc,
374            Chain::Polygon,
375            Chain::Plasma,
376            Chain::Robinhood,
377        ];
378        for chain in chains {
379            let registry = SwapEncoderRegistry::new_with_defaults(chain).unwrap_or_else(|e| {
380                panic!("default encoders failed to build for chain {chain}: {e}")
381            });
382            assert!(
383                registry
384                    .get_encoder("uniswap_v3")
385                    .is_some(),
386                "chain {chain} is missing the uniswap_v3 encoder"
387            );
388        }
389    }
390
391    #[test]
392    fn test_executor_addresses_match_registered_encoders() {
393        let registry = SwapEncoderRegistry::new_with_defaults(Chain::Ethereum).unwrap();
394
395        let executor_addresses = registry.executor_addresses();
396
397        assert!(!executor_addresses.is_empty());
398        for (protocol, executor_address) in executor_addresses {
399            let encoder = registry
400                .get_encoder(&protocol)
401                .unwrap_or_else(|| panic!("no encoder registered for {protocol}"));
402            assert_eq!(encoder.executor_address(), &executor_address);
403        }
404    }
405
406    /// The `fallback` section duplicates the `uniswap_v4` Angstrom hook address: the uniswap_v4
407    /// encoder fetches attestations for that hook, the fallback encoder rejects it. A chain
408    /// carrying both entries must keep them in lockstep, e.g. when Angstrom redeploys its hook.
409    #[test]
410    fn test_fallback_angstrom_hook_matches_uniswap_v4() {
411        let config: HashMap<Chain, HashMap<String, HashMap<String, String>>> =
412            serde_json::from_str(PROTOCOL_SPECIFIC_CONFIG).unwrap();
413        for (chain, protocols) in config {
414            let Some(fallback) = protocols.get(FALLBACK_KEY) else { continue };
415            assert_eq!(
416                fallback.get("angstrom_hook_address"),
417                protocols
418                    .get("uniswap_v4")
419                    .and_then(|uniswap_v4| uniswap_v4.get("angstrom_hook_address")),
420                "chain {chain}: the fallback and uniswap_v4 sections of \
421                 protocol_specific_addresses.json must name the same Angstrom hook"
422            );
423        }
424    }
425}