tycho-execution 0.408.0

Provides tools for encoding and executing swaps against Tycho router and protocol executors.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
use std::{collections::HashMap, str::FromStr};

use tycho_common::{models::Chain, Bytes};

use crate::encoding::{
    errors::EncodingError,
    evm::{
        constants::{
            DEFAULT_EXECUTORS_JSON, FALLBACK_KEY, FALLBACK_PREFIX, PRICE_LEVEL_STREAM_KEY,
            PRICE_LEVEL_STREAM_PREFIX, PROPAMM_FALLBACK_KEY, PROPAMM_FALLBACK_PREFIX,
            PROTOCOL_SPECIFIC_CONFIG, UNISWAP_V2_FORKS, UNISWAP_V3_FORKS,
        },
        swap_encoder::{
            aerodrome_v1::AerodromeV1SwapEncoder, balancer_v2::BalancerV2SwapEncoder,
            balancer_v3::BalancerV3SwapEncoder, bebop::BebopSwapEncoder, bopamm::BopAMMSwapEncoder,
            curve::CurveSwapEncoder, ekubo::EkuboSwapEncoder, ekubo_v3::EkuboV3SwapEncoder,
            erc_4626::ERC4626SwapEncoder, etherfi::EtherfiSwapEncoder,
            fallback::FallbackSwapEncoder, fermiswap::FermiSwapEncoder,
            fluid_v1::FluidV1SwapEncoder, hashflow::HashflowSwapEncoder,
            liquidity_party::LiquidityPartySwapEncoder, liquorice::LiquoriceSwapEncoder,
            lunarbase::LunarBaseSwapEncoder, maverick_v2::MaverickV2SwapEncoder,
            metric::MetricSwapEncoder, native::NativeSwapEncoder, native_wrap::WrapSwapEncoder,
            propamm::PropAMMSwapEncoder, ring_swap_v2::RingSwapV2SwapEncoder,
            rocketpool::RocketpoolSwapEncoder, sky::SkySwapEncoder,
            slipstreams::SlipstreamsSwapEncoder, uniswap_v2::UniswapV2SwapEncoder,
            uniswap_v3::UniswapV3SwapEncoder, uniswap_v4::UniswapV4SwapEncoder,
        },
    },
    swap_encoder::SwapEncoder,
};

/// Registry containing all supported `SwapEncoders`.
#[derive(Clone)]
pub struct SwapEncoderRegistry {
    chain: Chain,
    /// A hashmap containing the protocol system as a key and the `SwapEncoder` as a value.
    encoders: HashMap<String, Box<dyn SwapEncoder>>,
}

impl SwapEncoderRegistry {
    pub fn new(chain: Chain) -> Self {
        Self { chain, encoders: HashMap::new() }
    }

    /// Creates a new registry pre-populated with all default encoders for the given chain.
    pub fn new_with_defaults(chain: Chain) -> Result<Self, EncodingError> {
        Self::new(chain).add_default_encoders(None)
    }

    /// Populates the registry with the default `SwapEncoders` for the given blockchain by
    /// parsing the executors' addresses in the file at the given path.
    pub fn add_default_encoders(
        mut self,
        executors_addresses: Option<String>,
    ) -> Result<Self, EncodingError> {
        let config_str = if let Some(addresses) = executors_addresses {
            addresses
        } else {
            DEFAULT_EXECUTORS_JSON.to_string()
        };
        let config: HashMap<Chain, HashMap<String, String>> = serde_json::from_str(&config_str)?;
        let executors = config
            .get(&self.chain)
            .ok_or(EncodingError::FatalError("No executors found for chain".to_string()))?;

        let protocol_specific_config: HashMap<Chain, HashMap<String, HashMap<String, String>>> =
            serde_json::from_str(PROTOCOL_SPECIFIC_CONFIG)?;
        let protocol_specific_config = protocol_specific_config
            .get(&self.chain)
            .ok_or(EncodingError::FatalError(
                "No protocol specific config found for chain".to_string(),
            ))?;
        for (protocol, executor_address) in executors {
            let encoder = self.create_encoder(
                protocol,
                Bytes::from_str(executor_address).map_err(|_| {
                    EncodingError::FatalError(format!(
                        "Invalid executor address for protocol {}",
                        protocol
                    ))
                })?,
                protocol_specific_config
                    .get(protocol)
                    .cloned(),
            )?;
            self.encoders
                .insert(protocol.to_string(), encoder);
        }
        Ok(self)
    }

    /// Adds an encoder to the registry, replacing any existing encoder for the same protocol.
    pub fn register_encoder(mut self, protocol: &str, encoder: Box<dyn SwapEncoder>) -> Self {
        self.encoders
            .insert(protocol.to_string(), encoder);
        self
    }

    /// Returns the encoder registered for `protocol_system`.
    ///
    /// Price-level-stream protocols (`pricelevelstream:{protocol}`) without an exact entry fall
    /// back to the family entry registered under `pricelevelstream`, so a single configured
    /// executor address serves every pAMM — including auto-detected, address-named ones.
    /// `propammfallback:{protocol}` and `fallback:{protocol}` resolve the same way against
    /// `propammfallback` and `fallback`.
    #[allow(clippy::borrowed_box)]
    pub fn get_encoder(&self, protocol_system: &str) -> Option<&Box<dyn SwapEncoder>> {
        if let Some(encoder) = self.encoders.get(protocol_system) {
            return Some(encoder);
        }
        if protocol_system.starts_with(PRICE_LEVEL_STREAM_PREFIX) {
            return self
                .encoders
                .get(PRICE_LEVEL_STREAM_KEY);
        }
        if protocol_system.starts_with(PROPAMM_FALLBACK_PREFIX) {
            return self.encoders.get(PROPAMM_FALLBACK_KEY);
        }
        if protocol_system.starts_with(FALLBACK_PREFIX) {
            return self.encoders.get(FALLBACK_KEY);
        }
        None
    }

    /// The executor address of every encoder in this registry, keyed by protocol system.
    ///
    /// Several protocol systems may share one executor address, so the returned addresses are not
    /// necessarily distinct.
    pub fn executor_addresses(&self) -> HashMap<String, Bytes> {
        self.encoders
            .iter()
            .map(|(protocol, encoder)| (protocol.clone(), encoder.executor_address().clone()))
            .collect()
    }

    fn create_encoder(
        &self,
        protocol_system: &str,
        executor_address: Bytes,
        config: Option<HashMap<String, String>>,
    ) -> Result<Box<dyn SwapEncoder>, EncodingError> {
        match protocol_system {
            p if UNISWAP_V2_FORKS.contains(&p) => {
                Ok(Box::new(UniswapV2SwapEncoder::new(executor_address, self.chain, config)?))
            }
            "ring_swap_v2" => {
                Ok(Box::new(RingSwapV2SwapEncoder::new(executor_address, self.chain, config)?))
            }
            "aerodrome_v1" => {
                Ok(Box::new(AerodromeV1SwapEncoder::new(executor_address, self.chain, config)?))
            }
            "vm:balancer_v2" => {
                Ok(Box::new(BalancerV2SwapEncoder::new(executor_address, self.chain, config)?))
            }
            p if UNISWAP_V3_FORKS.contains(&p) => {
                Ok(Box::new(UniswapV3SwapEncoder::new(executor_address, self.chain, config)?))
            }
            "uniswap_v4" => {
                Ok(Box::new(UniswapV4SwapEncoder::new(executor_address, self.chain, config)?))
            }
            "ekubo_v2" => {
                Ok(Box::new(EkuboSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "ekubo_v3" => {
                Ok(Box::new(EkuboV3SwapEncoder::new(executor_address, self.chain, config)?))
            }
            "vm:bopamm" => {
                Ok(Box::new(BopAMMSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "vm:curve" => {
                Ok(Box::new(CurveSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "vm:maverick_v2" => {
                Ok(Box::new(MaverickV2SwapEncoder::new(executor_address, self.chain, config)?))
            }
            "vm:balancer_v3" => {
                Ok(Box::new(BalancerV3SwapEncoder::new(executor_address, self.chain, config)?))
            }
            "rfq:bebop" => {
                Ok(Box::new(BebopSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "rfq:hashflow" => {
                Ok(Box::new(HashflowSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "rfq:liquorice" => {
                Ok(Box::new(LiquoriceSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "rfq:metric" => {
                Ok(Box::new(MetricSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "rfq:native" => {
                Ok(Box::new(NativeSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "fluid_v1" => {
                Ok(Box::new(FluidV1SwapEncoder::new(executor_address, self.chain, config)?))
            }
            "vm:fermiswap" => {
                Ok(Box::new(FermiSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "vm:liquidityparty" => {
                Ok(Box::new(LiquidityPartySwapEncoder::new(executor_address, self.chain, config)?))
            }
            "aerodrome_slipstreams" => {
                Ok(Box::new(SlipstreamsSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "rocketpool" => {
                Ok(Box::new(RocketpoolSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "sky" => Ok(Box::new(SkySwapEncoder::new(executor_address, self.chain, config)?)),
            "erc4626" => {
                Ok(Box::new(ERC4626SwapEncoder::new(executor_address, self.chain, config)?))
            }
            "lunarbase" => {
                Ok(Box::new(LunarBaseSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "velodrome_slipstreams" => {
                Ok(Box::new(SlipstreamsSwapEncoder::new(executor_address, self.chain, config)?))
            }
            // UP on Robinhood Chain deploys the Slipstream contracts verbatim, and its pools price
            // swaps through a dynamic fee module, so it encodes like the other Slipstream forks.
            "up_v3" => {
                Ok(Box::new(SlipstreamsSwapEncoder::new(executor_address, self.chain, config)?))
            }
            // Ramses V3 reuses the standard Uniswap V3 executor unchanged, encoded via the
            // Slipstreams encoder. Three things make this sound:
            //   1. ABI match: the Ramses pool exposes the identical
            //      `swap(address,bool,int256,uint160,bytes)` and calls `uniswapV3SwapCallback`,
            //      which the router's selector-agnostic fallback routes back to the executor.
            //   2. The executor's `_decodeData` reads only the pool address (bytes 43..63) and the
            //      zero-for-one flag (byte 63): it calls `pool.swap` on that address without
            //      recomputing it, and never touches the 3-byte slot at bytes 40..43. So it is
            //      irrelevant both that Ramses keys pools by tick spacing rather than fee, and that
            //      the Slipstreams encoder packs `tick_spacing` into that slot (where Uniswap V3
            //      packs the fee).
            //   3. The SlipstreamsExecutor contract is byte-for-byte identical to the
            //      UniswapV3Executor, so the encoder choice does not imply a different on-chain
            //      executor.
            "ramses_v3" => {
                Ok(Box::new(SlipstreamsSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "native_wrapper" => {
                Ok(Box::new(WrapSwapEncoder::new(executor_address, self.chain, config)?))
            }
            "etherfi" => {
                Ok(Box::new(EtherfiSwapEncoder::new(executor_address, self.chain, config)?))
            }
            // All pAMMs following the standard IPropAMM interface share one generic encoder /
            // executor; the concrete protocol is identified by the component, not the encoder. The
            // bare family key serves every protocol via the `get_encoder` fallback;
            // protocol-specific `pricelevelstream:{protocol}` entries override it per
            // protocol. The PropAMMRouter path takes the same calldata, so it reuses
            // the same encoder and differs only in the executor address configured for
            // the family.
            pls if pls == PRICE_LEVEL_STREAM_KEY ||
                pls.starts_with(PRICE_LEVEL_STREAM_PREFIX) ||
                pls == PROPAMM_FALLBACK_KEY ||
                pls.starts_with(PROPAMM_FALLBACK_PREFIX) =>
            {
                Ok(Box::new(PropAMMSwapEncoder::new(executor_address, self.chain, config)?))
            }
            // The TychoFallbackRouter path carries the fallback protocol in the swap data, so it
            // needs its own encoder; the family resolves like the price-level-stream one.
            f if f == FALLBACK_KEY || f.starts_with(FALLBACK_PREFIX) => {
                Ok(Box::new(FallbackSwapEncoder::new(executor_address, self.chain, config)?))
            }
            _ => Err(EncodingError::FatalError(format!(
                "Unknown protocol system: {}",
                protocol_system
            ))),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A single `pricelevelstream` config entry serves the whole protocol family: the bare
    /// family key resolves as an exact entry, and every `pricelevelstream:{protocol}` protocol —
    /// including auto-detected, address-named protocols no config could enumerate — resolves to it
    /// through the fallback.
    #[test]
    fn test_price_level_stream_protocols_route_to_generic_encoder() {
        let executors = std::fs::read_to_string("config/test_executor_addresses.json").unwrap();
        let registry = SwapEncoderRegistry::new(Chain::Ethereum)
            .add_default_encoders(Some(executors))
            .unwrap();

        for protocol in [
            PRICE_LEVEL_STREAM_KEY,
            "pricelevelstream:fermiswap",
            "pricelevelstream:kipseli",
            "pricelevelstream:0x2222222222222222222222222222222222222222",
        ] {
            assert!(registry.get_encoder(protocol).is_some(), "no encoder resolved for {protocol}");
        }
        // The fallback is scoped to the price-level-stream prefix.
        assert!(registry
            .get_encoder("unknown_protocol")
            .is_none());
    }

    /// The PropAMMRouter family resolves the same way, and to a different executor than the direct
    /// path — same calldata, different call target.
    #[test]
    fn test_propamm_fallback_protocol_resolution() {
        let executors = std::fs::read_to_string("config/test_executor_addresses.json").unwrap();
        let registry = SwapEncoderRegistry::new(Chain::Ethereum)
            .add_default_encoders(Some(executors))
            .unwrap();

        for protocol in [
            PROPAMM_FALLBACK_KEY,
            "propammfallback:fermiswap",
            "propammfallback:0x5979458912f80b96d30d4220af8e2e4925a33320",
        ] {
            assert!(registry.get_encoder(protocol).is_some(), "no encoder resolved for {protocol}");
        }

        let direct = registry
            .get_encoder("pricelevelstream:fermiswap")
            .unwrap()
            .executor_address()
            .clone();
        let via_router = registry
            .get_encoder("propammfallback:fermiswap")
            .unwrap()
            .executor_address()
            .clone();
        assert_ne!(direct, via_router);
    }

    /// The TychoFallbackRouter family resolves like the other two pAMM families, against its own
    /// encoder. No `fallback` entry ships in the executor configs until the FallbackExecutor is
    /// deployed, so the test registers the family key itself.
    #[test]
    fn test_fallback_protocol_resolution() {
        let executor_address =
            Bytes::from_str("0x5c2f5a71f67c01775180adc06909288b4c329308").unwrap();
        let registry = SwapEncoderRegistry::new(Chain::Ethereum);
        let config = HashMap::from([(
            "angstrom_hook_address".to_string(),
            "0x0000000aa232009084Bd71A5797d089AA4Edfad4".to_string(),
        )]);
        let encoder = registry
            .create_encoder(FALLBACK_KEY, executor_address.clone(), Some(config))
            .unwrap();
        let registry = registry.register_encoder(FALLBACK_KEY, encoder);

        for protocol in [
            FALLBACK_KEY,
            "fallback:fermiswap",
            "fallback:0x5979458912f80b96d30d4220af8e2e4925a33320",
        ] {
            let resolved = registry
                .get_encoder(protocol)
                .unwrap_or_else(|| panic!("no encoder resolved for {protocol}"));
            assert_eq!(resolved.executor_address(), &executor_address);
        }
        // The family fallback is scoped to the prefix.
        assert!(registry
            .get_encoder("fallbackless_protocol")
            .is_none());
    }

    #[test]
    fn test_default_encoders_build_for_every_configured_chain() {
        let chains = [
            Chain::Ethereum,
            Chain::Base,
            Chain::Unichain,
            Chain::Arbitrum,
            Chain::Bsc,
            Chain::Polygon,
            Chain::Plasma,
            Chain::Robinhood,
        ];
        for chain in chains {
            let registry = SwapEncoderRegistry::new_with_defaults(chain).unwrap_or_else(|e| {
                panic!("default encoders failed to build for chain {chain}: {e}")
            });
            assert!(
                registry
                    .get_encoder("uniswap_v3")
                    .is_some(),
                "chain {chain} is missing the uniswap_v3 encoder"
            );
        }
    }

    #[test]
    fn test_executor_addresses_match_registered_encoders() {
        let registry = SwapEncoderRegistry::new_with_defaults(Chain::Ethereum).unwrap();

        let executor_addresses = registry.executor_addresses();

        assert!(!executor_addresses.is_empty());
        for (protocol, executor_address) in executor_addresses {
            let encoder = registry
                .get_encoder(&protocol)
                .unwrap_or_else(|| panic!("no encoder registered for {protocol}"));
            assert_eq!(encoder.executor_address(), &executor_address);
        }
    }

    /// The `fallback` section duplicates the `uniswap_v4` Angstrom hook address: the uniswap_v4
    /// encoder fetches attestations for that hook, the fallback encoder rejects it. A chain
    /// carrying both entries must keep them in lockstep, e.g. when Angstrom redeploys its hook.
    #[test]
    fn test_fallback_angstrom_hook_matches_uniswap_v4() {
        let config: HashMap<Chain, HashMap<String, HashMap<String, String>>> =
            serde_json::from_str(PROTOCOL_SPECIFIC_CONFIG).unwrap();
        for (chain, protocols) in config {
            let Some(fallback) = protocols.get(FALLBACK_KEY) else { continue };
            assert_eq!(
                fallback.get("angstrom_hook_address"),
                protocols
                    .get("uniswap_v4")
                    .and_then(|uniswap_v4| uniswap_v4.get("angstrom_hook_address")),
                "chain {chain}: the fallback and uniswap_v4 sections of \
                 protocol_specific_addresses.json must name the same Angstrom hook"
            );
        }
    }
}