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