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