tycho_execution/encoding/evm/
tycho_encoders.rs

1use std::collections::HashSet;
2
3use alloy::signers::local::PrivateKeySigner;
4use tycho_common::{models::Chain, Bytes};
5
6use crate::encoding::{
7    errors::EncodingError,
8    evm::{
9        approvals::permit2::Permit2,
10        constants::{GROUPABLE_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS},
11        encoding_utils::encode_tycho_router_call,
12        group_swaps::group_swaps,
13        strategy_encoder::strategy_encoders::{
14            SequentialSwapStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder,
15        },
16        swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
17        utils::ple_encode,
18    },
19    models::{
20        EncodedSolution, EncodingContext, NativeAction, Solution, Transaction, TransferType,
21        UserTransferType,
22    },
23    strategy_encoder::StrategyEncoder,
24    tycho_encoder::TychoEncoder,
25};
26
27/// Encodes solutions to be used by the TychoRouter.
28///
29/// # Fields
30/// * `chain`: Chain to be used
31/// * `single_swap_strategy`: Encoder for single swaps
32/// * `sequential_swap_strategy`: Encoder for sequential swaps
33/// * `split_swap_strategy`: Encoder for split swaps
34/// * `router_address`: Address of the Tycho router contract
35/// * `user_transfer_type`: Type of user transfer
36/// * `permit2`: Optional Permit2 instance for permit transfers
37/// * `signer`: Optional signer (used only for permit2 and full calldata encoding)
38#[derive(Clone)]
39pub struct TychoRouterEncoder {
40    chain: Chain,
41    single_swap_strategy: SingleSwapStrategyEncoder,
42    sequential_swap_strategy: SequentialSwapStrategyEncoder,
43    split_swap_strategy: SplitSwapStrategyEncoder,
44    router_address: Bytes,
45    user_transfer_type: UserTransferType,
46    permit2: Option<Permit2>,
47    signer: Option<PrivateKeySigner>,
48}
49
50impl TychoRouterEncoder {
51    pub fn new(
52        chain: Chain,
53        swap_encoder_registry: SwapEncoderRegistry,
54        router_address: Bytes,
55        user_transfer_type: UserTransferType,
56        signer: Option<PrivateKeySigner>,
57        historical_trade: bool,
58    ) -> Result<Self, EncodingError> {
59        let permit2 = if user_transfer_type == UserTransferType::TransferFromPermit2 {
60            Some(Permit2::new()?)
61        } else {
62            None
63        };
64        Ok(TychoRouterEncoder {
65            single_swap_strategy: SingleSwapStrategyEncoder::new(
66                chain,
67                swap_encoder_registry.clone(),
68                user_transfer_type.clone(),
69                router_address.clone(),
70                historical_trade,
71            )?,
72            sequential_swap_strategy: SequentialSwapStrategyEncoder::new(
73                chain,
74                swap_encoder_registry.clone(),
75                user_transfer_type.clone(),
76                router_address.clone(),
77                historical_trade,
78            )?,
79            split_swap_strategy: SplitSwapStrategyEncoder::new(
80                chain,
81                swap_encoder_registry,
82                user_transfer_type.clone(),
83                router_address.clone(),
84                historical_trade,
85            )?,
86            router_address,
87            permit2,
88            signer,
89            chain,
90            user_transfer_type,
91        })
92    }
93
94    fn encode_solution(&self, solution: &Solution) -> Result<EncodedSolution, EncodingError> {
95        self.validate_solution(solution)?;
96        let protocols: HashSet<String> = solution
97            .swaps
98            .iter()
99            .map(|swap| swap.component.protocol_system.clone())
100            .collect();
101
102        let mut encoded_solution = if (solution.swaps.len() == 1) ||
103            ((protocols.len() == 1 &&
104                protocols
105                    .iter()
106                    .any(|p| GROUPABLE_PROTOCOLS.contains(&p.as_str()))) &&
107                solution
108                    .swaps
109                    .iter()
110                    .all(|swap| swap.split == 0.0))
111        {
112            self.single_swap_strategy
113                .encode_strategy(solution)?
114        } else if solution
115            .swaps
116            .iter()
117            .all(|swap| swap.split == 0.0)
118        {
119            self.sequential_swap_strategy
120                .encode_strategy(solution)?
121        } else {
122            self.split_swap_strategy
123                .encode_strategy(solution)?
124        };
125
126        if let Some(permit2) = &self.permit2 {
127            let permit = permit2.get_permit(
128                &self.router_address,
129                &solution.sender,
130                &solution.given_token,
131                &solution.given_amount,
132            )?;
133            encoded_solution.permit = Some(permit);
134        }
135        Ok(encoded_solution)
136    }
137}
138
139impl TychoEncoder for TychoRouterEncoder {
140    fn encode_solutions(
141        &self,
142        solutions: Vec<Solution>,
143    ) -> Result<Vec<EncodedSolution>, EncodingError> {
144        let mut result: Vec<EncodedSolution> = Vec::new();
145        for solution in solutions.iter() {
146            let encoded_solution = self.encode_solution(solution)?;
147            result.push(encoded_solution);
148        }
149        Ok(result)
150    }
151
152    fn encode_full_calldata(
153        &self,
154        solutions: Vec<Solution>,
155    ) -> Result<Vec<Transaction>, EncodingError> {
156        let mut transactions: Vec<Transaction> = Vec::new();
157        for solution in solutions.iter() {
158            let encoded_solution = self.encode_solution(solution)?;
159
160            let transaction = encode_tycho_router_call(
161                self.chain.id(),
162                encoded_solution,
163                solution,
164                &self.user_transfer_type,
165                &self.chain.native_token().address,
166                self.signer.clone(),
167            )?;
168
169            transactions.push(transaction);
170        }
171        Ok(transactions)
172    }
173
174    /// Raises an `EncodingError` if the solution is not considered valid.
175    ///
176    /// A solution is considered valid if all the following conditions are met:
177    /// * The solution is not exact out.
178    /// * The solution has at least one swap.
179    /// * If the solution is wrapping, the given token is the chain's native token and the first
180    ///   swap's input is the chain's wrapped token.
181    /// * If the solution is unwrapping, the checked token is the chain's native token and the last
182    ///   swap's output is the chain's wrapped token.
183    /// * The token cannot appear more than once in the solution unless it is the first and last
184    ///   token (i.e. a true cyclical swap).
185    fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> {
186        if solution.exact_out {
187            return Err(EncodingError::FatalError(
188                "Currently only exact input solutions are supported".to_string(),
189            ));
190        }
191        if solution.swaps.is_empty() {
192            return Err(EncodingError::FatalError("No swaps found in solution".to_string()));
193        }
194        let native_address = self.chain.native_token().address;
195        let wrapped_address = self
196            .chain
197            .wrapped_native_token()
198            .address;
199        if let Some(native_action) = &solution.native_action {
200            if native_action == &NativeAction::Wrap {
201                if solution.given_token != native_address {
202                    return Err(EncodingError::FatalError(
203                        "Native token must be the input token in order to wrap".to_string(),
204                    ));
205                }
206                if let Some(first_swap) = solution.swaps.first() {
207                    if first_swap.token_in != wrapped_address {
208                        return Err(EncodingError::FatalError(
209                            "Wrapped token must be the first swap's input in order to wrap"
210                                .to_string(),
211                        ));
212                    }
213                }
214            } else if native_action == &NativeAction::Unwrap {
215                if solution.checked_token != native_address {
216                    return Err(EncodingError::FatalError(
217                        "Native token must be the output token in order to unwrap".to_string(),
218                    ));
219                }
220                if let Some(last_swap) = solution.swaps.last() {
221                    if last_swap.token_out != wrapped_address {
222                        return Err(EncodingError::FatalError(
223                            "Wrapped token must be the last swap's output in order to unwrap"
224                                .to_string(),
225                        ));
226                    }
227                }
228            }
229        }
230
231        let mut solution_tokens = vec![];
232        let mut split_tokens_already_considered = HashSet::new();
233        for (i, swap) in solution.swaps.iter().enumerate() {
234            // so we don't count the split tokens more than once
235            if swap.split != 0.0 {
236                if !split_tokens_already_considered.contains(&swap.token_in) {
237                    solution_tokens.push(&swap.token_in);
238                    split_tokens_already_considered.insert(&swap.token_in);
239                }
240            } else {
241                // it might be the last swap of the split or a regular swap
242                if !split_tokens_already_considered.contains(&swap.token_in) {
243                    solution_tokens.push(&swap.token_in);
244                }
245            }
246            if i == solution.swaps.len() - 1 {
247                solution_tokens.push(&swap.token_out);
248            }
249        }
250
251        if solution_tokens.len() !=
252            solution_tokens
253                .iter()
254                .cloned()
255                .collect::<HashSet<&Bytes>>()
256                .len()
257        {
258            if let Some(last_swap) = solution.swaps.last() {
259                if solution.swaps[0].token_in != last_swap.token_out {
260                    return Err(EncodingError::FatalError(
261                        "Cyclical swaps are only allowed if they are the first and last token of a solution".to_string(),
262                    ));
263                } else {
264                    // it is a valid cyclical swap
265                    // we don't support any wrapping or unwrapping in this case
266                    if let Some(_native_action) = &solution.native_action {
267                        return Err(EncodingError::FatalError(
268                            "Wrapping/Unwrapping is not available in cyclical swaps".to_string(),
269                        ));
270                    }
271                }
272            }
273        }
274        Ok(())
275    }
276}
277
278/// Represents an encoder for one swap to be executed directly against an Executor.
279///
280/// This is useful when you want to bypass the Tycho Router, use your own Router contract and
281/// just need the calldata for a particular swap.
282///
283/// # Fields
284/// * `swap_encoder_registry`: Registry of swap encoders
285#[derive(Clone)]
286pub struct TychoExecutorEncoder {
287    swap_encoder_registry: SwapEncoderRegistry,
288}
289
290impl TychoExecutorEncoder {
291    pub fn new(swap_encoder_registry: SwapEncoderRegistry) -> Result<Self, EncodingError> {
292        Ok(TychoExecutorEncoder { swap_encoder_registry })
293    }
294
295    fn encode_executor_calldata(
296        &self,
297        solution: &Solution,
298    ) -> Result<EncodedSolution, EncodingError> {
299        let grouped_swaps = group_swaps(&solution.swaps);
300        let number_of_groups = grouped_swaps.len();
301        if number_of_groups > 1 {
302            return Err(EncodingError::InvalidInput(format!(
303                "Tycho executor encoder only supports one swap. Found {number_of_groups}"
304            )))
305        }
306
307        let grouped_swap = grouped_swaps
308            .first()
309            .ok_or_else(|| EncodingError::FatalError("Swap grouping failed".to_string()))?;
310
311        let swap_encoder = self
312            .swap_encoder_registry
313            .get_encoder(&grouped_swap.protocol_system)
314            .ok_or_else(|| {
315                EncodingError::InvalidInput(format!(
316                    "Swap encoder not found for protocol: {}",
317                    grouped_swap.protocol_system
318                ))
319            })?;
320
321        let transfer = if IN_TRANSFER_REQUIRED_PROTOCOLS.contains(
322            &grouped_swap.swaps[0]
323                .component
324                .protocol_system
325                .as_str(),
326        ) {
327            TransferType::Transfer
328        } else {
329            TransferType::None
330        };
331        let encoding_context = EncodingContext {
332            receiver: solution.receiver.clone(),
333            exact_out: solution.exact_out,
334            router_address: None,
335            group_token_in: grouped_swap.token_in.clone(),
336            group_token_out: grouped_swap.token_out.clone(),
337            transfer_type: transfer,
338            historical_trade: false,
339        };
340        let mut grouped_protocol_data: Vec<Vec<u8>> = vec![];
341        let mut initial_protocol_data: Vec<u8> = vec![];
342        for swap in grouped_swap.swaps.iter() {
343            let protocol_data = swap_encoder.encode_swap(swap, &encoding_context)?;
344            if encoding_context.group_token_in == swap.token_in {
345                initial_protocol_data = protocol_data;
346            } else {
347                grouped_protocol_data.push(protocol_data);
348            }
349        }
350
351        if !grouped_protocol_data.is_empty() {
352            initial_protocol_data.extend(ple_encode(grouped_protocol_data));
353        }
354
355        Ok(EncodedSolution {
356            swaps: initial_protocol_data,
357            interacting_with: swap_encoder.executor_address().clone(),
358            permit: None,
359            function_signature: "".to_string(),
360            n_tokens: 0,
361        })
362    }
363}
364
365impl TychoEncoder for TychoExecutorEncoder {
366    fn encode_solutions(
367        &self,
368        solutions: Vec<Solution>,
369    ) -> Result<Vec<EncodedSolution>, EncodingError> {
370        let solution = solutions
371            .first()
372            .ok_or(EncodingError::FatalError("No solutions found".to_string()))?;
373        self.validate_solution(solution)?;
374
375        let encoded_solution = self.encode_executor_calldata(solution)?;
376
377        Ok(vec![encoded_solution])
378    }
379
380    fn encode_full_calldata(
381        &self,
382        _solutions: Vec<Solution>,
383    ) -> Result<Vec<Transaction>, EncodingError> {
384        Err(EncodingError::NotImplementedError(
385            "Full calldata encoding is not supported for TychoExecutorEncoder".to_string(),
386        ))
387    }
388
389    /// Raises an `EncodingError` if the solution is not considered valid.
390    ///
391    /// A solution is considered valid if all the following conditions are met:
392    /// * The solution is not exact out.
393    fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> {
394        if solution.exact_out {
395            return Err(EncodingError::FatalError(
396                "Currently only exact input solutions are supported".to_string(),
397            ));
398        }
399        Ok(())
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use std::{collections::HashMap, fs, str::FromStr};
406
407    use num_bigint::{BigInt, BigUint};
408    use tycho_common::models::{protocol::ProtocolComponent, Chain};
409
410    use super::*;
411    use crate::encoding::models::{Swap, SwapBuilder};
412
413    fn dai() -> Bytes {
414        Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap()
415    }
416
417    fn eth() -> Bytes {
418        Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap()
419    }
420
421    fn weth() -> Bytes {
422        Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap()
423    }
424
425    fn usdc() -> Bytes {
426        Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap()
427    }
428
429    fn wbtc() -> Bytes {
430        Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap()
431    }
432
433    fn pepe() -> Bytes {
434        Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap()
435    }
436
437    // Fee and tick spacing information for this test is obtained by querying the
438    // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e
439    // Using the poolKeys function with the first 25 bytes of the pool id
440    fn swap_usdc_eth_univ4() -> Swap {
441        let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be());
442        let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be());
443        let mut static_attributes_usdc_eth: HashMap<String, Bytes> = HashMap::new();
444        static_attributes_usdc_eth.insert("key_lp_fee".into(), pool_fee_usdc_eth);
445        static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth);
446        SwapBuilder::new(
447            ProtocolComponent {
448                id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d"
449                    .to_string(),
450                protocol_system: "uniswap_v4".to_string(),
451                static_attributes: static_attributes_usdc_eth,
452                ..Default::default()
453            },
454            usdc().clone(),
455            eth().clone(),
456        )
457        .build()
458    }
459
460    fn swap_eth_pepe_univ4() -> Swap {
461        let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be());
462        let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be());
463        let mut static_attributes_eth_pepe: HashMap<String, Bytes> = HashMap::new();
464        static_attributes_eth_pepe.insert("key_lp_fee".into(), pool_fee_eth_pepe);
465        static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe);
466        SwapBuilder::new(
467            ProtocolComponent {
468                id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9"
469                    .to_string(),
470                protocol_system: "uniswap_v4".to_string(),
471                static_attributes: static_attributes_eth_pepe,
472                ..Default::default()
473            },
474            eth().clone(),
475            pepe().clone(),
476        )
477        .build()
478    }
479
480    fn router_address() -> Bytes {
481        Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()
482    }
483
484    fn eth_chain() -> Chain {
485        Chain::Ethereum
486    }
487
488    fn get_swap_encoder_registry() -> SwapEncoderRegistry {
489        let executors_addresses =
490            fs::read_to_string("config/test_executor_addresses.json").unwrap();
491        SwapEncoderRegistry::new(Some(executors_addresses), eth_chain()).unwrap()
492    }
493
494    fn get_tycho_router_encoder(user_transfer_type: UserTransferType) -> TychoRouterEncoder {
495        TychoRouterEncoder::new(
496            eth_chain(),
497            get_swap_encoder_registry(),
498            router_address(),
499            user_transfer_type,
500            None,
501            false,
502        )
503        .unwrap()
504    }
505
506    mod router_encoder {
507        use super::*;
508
509        #[test]
510        #[allow(deprecated)]
511        fn test_encode_router_calldata_single_swap() {
512            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
513            let eth_amount_in = BigUint::from(1000u32);
514            let swap = SwapBuilder::new(
515                ProtocolComponent {
516                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
517                    protocol_system: "uniswap_v2".to_string(),
518                    ..Default::default()
519                },
520                weth().clone(),
521                dai().clone(),
522            )
523            .build();
524
525            let solution = Solution {
526                exact_out: false,
527                given_amount: eth_amount_in.clone(),
528                given_token: eth(),
529                checked_token: dai(),
530                swaps: vec![swap],
531                receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
532                native_action: Some(NativeAction::Wrap),
533                ..Default::default()
534            };
535
536            let transactions = encoder.encode_full_calldata(vec![solution]);
537            assert!(transactions.is_ok());
538            let transactions = transactions.unwrap();
539            assert_eq!(transactions.len(), 1);
540            assert_eq!(transactions[0].value, eth_amount_in);
541            assert_eq!(
542                transactions[0].to,
543                Bytes::from_str("0x3ede3eca2a72b3aecc820e955b36f38437d01395").unwrap()
544            );
545            // single swap selector
546            assert_eq!(&hex::encode(transactions[0].clone().data)[..8], "5c4b639c");
547        }
548
549        #[test]
550        #[allow(deprecated)]
551        fn test_encode_router_calldata_single_swap_group() {
552            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
553            let solution = Solution {
554                exact_out: false,
555                given_token: usdc(),
556                given_amount: BigUint::from_str("1000_000000").unwrap(),
557                checked_token: pepe(),
558                checked_amount: BigUint::from_str("105_152_000000000000000000").unwrap(),
559                sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
560                receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
561                swaps: vec![swap_usdc_eth_univ4(), swap_eth_pepe_univ4()],
562                ..Default::default()
563            };
564
565            let transactions = encoder.encode_full_calldata(vec![solution]);
566            assert!(transactions.is_ok());
567            let transactions = transactions.unwrap();
568            assert_eq!(transactions.len(), 1);
569            // single swap selector
570            assert_eq!(&hex::encode(transactions[0].clone().data)[..8], "5c4b639c");
571        }
572
573        #[test]
574        #[allow(deprecated)]
575        fn test_encode_router_calldata_sequential_swap() {
576            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
577            let eth_amount_in = BigUint::from(1000u32);
578            let swap_weth_dai = SwapBuilder::new(
579                ProtocolComponent {
580                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
581                    protocol_system: "uniswap_v2".to_string(),
582                    ..Default::default()
583                },
584                weth().clone(),
585                dai().clone(),
586            )
587            .build();
588
589            let swap_dai_usdc = SwapBuilder::new(
590                ProtocolComponent {
591                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
592                    protocol_system: "uniswap_v2".to_string(),
593                    ..Default::default()
594                },
595                dai().clone(),
596                usdc().clone(),
597            )
598            .build();
599
600            let solution = Solution {
601                exact_out: false,
602                given_amount: eth_amount_in.clone(),
603                given_token: eth(),
604                checked_token: usdc(),
605                swaps: vec![swap_weth_dai, swap_dai_usdc],
606                receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
607                native_action: Some(NativeAction::Wrap),
608                checked_amount: BigUint::from(1000u32),
609                ..Default::default()
610            };
611
612            let transactions = encoder.encode_full_calldata(vec![solution]);
613            assert!(transactions.is_ok());
614            let transactions = transactions.unwrap();
615            assert_eq!(transactions.len(), 1);
616            assert_eq!(transactions[0].value, eth_amount_in);
617            // sequential swap selector
618            assert_eq!(&hex::encode(transactions[0].clone().data)[..8], "e21dd0d3");
619        }
620
621        #[test]
622        fn test_encode_router_calldata_split_swap_group() {
623            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
624            let mut swap_usdc_eth = swap_usdc_eth_univ4();
625            swap_usdc_eth.split = 0.5; // Set split to 50%
626            let solution = Solution {
627                exact_out: false,
628                given_token: usdc(),
629                given_amount: BigUint::from_str("1000_000000").unwrap(),
630                checked_token: eth(),
631                checked_amount: BigUint::from_str("105_152_000000000000000000").unwrap(),
632                sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
633                receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
634                swaps: vec![swap_usdc_eth, swap_usdc_eth_univ4()],
635                ..Default::default()
636            };
637
638            let encoded_solution_res = encoder.encode_solution(&solution);
639            assert!(encoded_solution_res.is_ok());
640
641            let encoded_solution = encoded_solution_res.unwrap();
642            assert!(encoded_solution
643                .function_signature
644                .contains("splitSwap"));
645        }
646
647        #[test]
648        fn test_validate_fails_for_exact_out() {
649            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
650            let solution = Solution {
651                exact_out: true, // This should cause an error
652                ..Default::default()
653            };
654            let result = encoder.validate_solution(&solution);
655
656            assert!(result.is_err());
657            assert_eq!(
658                result.err().unwrap(),
659                EncodingError::FatalError(
660                    "Currently only exact input solutions are supported".to_string()
661                )
662            );
663        }
664
665        #[test]
666        fn test_validate_passes_for_wrap() {
667            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
668            let swap = SwapBuilder::new(
669                ProtocolComponent {
670                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
671                    protocol_system: "uniswap_v2".to_string(),
672                    ..Default::default()
673                },
674                weth().clone(),
675                dai().clone(),
676            )
677            .build();
678
679            let solution = Solution {
680                exact_out: false,
681                given_token: eth(),
682                checked_token: dai(),
683                swaps: vec![swap],
684                native_action: Some(NativeAction::Wrap),
685                ..Default::default()
686            };
687
688            let result = encoder.validate_solution(&solution);
689
690            assert!(result.is_ok());
691        }
692
693        #[test]
694        fn test_validate_fails_for_wrap_wrong_input() {
695            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
696            let swap = SwapBuilder::new(
697                ProtocolComponent {
698                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
699                    protocol_system: "uniswap_v2".to_string(),
700                    ..Default::default()
701                },
702                weth().clone(),
703                dai().clone(),
704            )
705            .build();
706
707            let solution = Solution {
708                exact_out: false,
709                given_token: weth(),
710                swaps: vec![swap],
711                native_action: Some(NativeAction::Wrap),
712                ..Default::default()
713            };
714
715            let result = encoder.validate_solution(&solution);
716
717            assert!(result.is_err());
718            assert_eq!(
719                result.err().unwrap(),
720                EncodingError::FatalError(
721                    "Native token must be the input token in order to wrap".to_string()
722                )
723            );
724        }
725
726        #[test]
727        fn test_validate_fails_for_wrap_wrong_first_swap() {
728            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
729            let swap = SwapBuilder::new(
730                ProtocolComponent {
731                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
732                    protocol_system: "uniswap_v2".to_string(),
733                    ..Default::default()
734                },
735                eth().clone(),
736                dai().clone(),
737            )
738            .build();
739
740            let solution = Solution {
741                exact_out: false,
742                given_token: eth(),
743                swaps: vec![swap],
744                native_action: Some(NativeAction::Wrap),
745                ..Default::default()
746            };
747
748            let result = encoder.validate_solution(&solution);
749
750            assert!(result.is_err());
751            assert_eq!(
752                result.err().unwrap(),
753                EncodingError::FatalError(
754                    "Wrapped token must be the first swap's input in order to wrap".to_string()
755                )
756            );
757        }
758
759        #[test]
760        fn test_validate_fails_no_swaps() {
761            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
762            let solution = Solution {
763                exact_out: false,
764                given_token: eth(),
765                swaps: vec![],
766                native_action: Some(NativeAction::Wrap),
767                ..Default::default()
768            };
769
770            let result = encoder.validate_solution(&solution);
771
772            assert!(result.is_err());
773            assert_eq!(
774                result.err().unwrap(),
775                EncodingError::FatalError("No swaps found in solution".to_string())
776            );
777        }
778
779        #[test]
780        fn test_validate_passes_for_unwrap() {
781            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
782            let swap = SwapBuilder::new(
783                ProtocolComponent {
784                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
785                    protocol_system: "uniswap_v2".to_string(),
786                    ..Default::default()
787                },
788                dai().clone(),
789                weth().clone(),
790            )
791            .build();
792
793            let solution = Solution {
794                exact_out: false,
795                checked_token: eth(),
796                swaps: vec![swap],
797                native_action: Some(NativeAction::Unwrap),
798                ..Default::default()
799            };
800
801            let result = encoder.validate_solution(&solution);
802
803            assert!(result.is_ok());
804        }
805
806        #[test]
807        fn test_validate_fails_for_unwrap_wrong_output() {
808            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
809            let swap = SwapBuilder::new(
810                ProtocolComponent {
811                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
812                    protocol_system: "uniswap_v2".to_string(),
813                    ..Default::default()
814                },
815                dai().clone(),
816                weth().clone(),
817            )
818            .build();
819
820            let solution = Solution {
821                exact_out: false,
822                given_token: dai(),
823                checked_token: weth(),
824                swaps: vec![swap],
825                native_action: Some(NativeAction::Unwrap),
826                ..Default::default()
827            };
828
829            let result = encoder.validate_solution(&solution);
830
831            assert!(result.is_err());
832            assert_eq!(
833                result.err().unwrap(),
834                EncodingError::FatalError(
835                    "Native token must be the output token in order to unwrap".to_string()
836                )
837            );
838        }
839
840        #[test]
841        fn test_validate_fails_for_unwrap_wrong_last_swap() {
842            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
843            let swap = SwapBuilder::new(
844                ProtocolComponent {
845                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
846                    protocol_system: "uniswap_v2".to_string(),
847                    ..Default::default()
848                },
849                dai().clone(),
850                eth().clone(),
851            )
852            .build();
853
854            let solution = Solution {
855                exact_out: false,
856                checked_token: eth(),
857                swaps: vec![swap],
858                native_action: Some(NativeAction::Unwrap),
859                ..Default::default()
860            };
861
862            let result = encoder.validate_solution(&solution);
863
864            assert!(result.is_err());
865            assert_eq!(
866                result.err().unwrap(),
867                EncodingError::FatalError(
868                    "Wrapped token must be the last swap's output in order to unwrap".to_string()
869                )
870            );
871        }
872
873        #[test]
874        fn test_validate_cyclical_swap() {
875            // This validation passes because the cyclical swap is the first and last token
876            //      50% ->  WETH
877            // DAI -              -> DAI
878            //      50% -> WETH
879            // (some of the pool addresses in this test are fake)
880            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
881            let swaps = vec![
882                SwapBuilder::new(
883                    ProtocolComponent {
884                        id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
885                        protocol_system: "uniswap_v2".to_string(),
886                        ..Default::default()
887                    },
888                    dai().clone(),
889                    weth().clone(),
890                )
891                .build(),
892                SwapBuilder::new(
893                    ProtocolComponent {
894                        id: "0x0000000000000000000000000000000000000000".to_string(),
895                        protocol_system: "uniswap_v2".to_string(),
896                        ..Default::default()
897                    },
898                    dai().clone(),
899                    weth().clone(),
900                )
901                .build(),
902                SwapBuilder::new(
903                    ProtocolComponent {
904                        id: "0x0000000000000000000000000000000000000000".to_string(),
905                        protocol_system: "uniswap_v2".to_string(),
906                        ..Default::default()
907                    },
908                    weth().clone(),
909                    dai().clone(),
910                )
911                .build(),
912            ];
913
914            let solution = Solution {
915                exact_out: false,
916                given_token: dai(),
917                checked_token: dai(),
918                swaps,
919                ..Default::default()
920            };
921
922            let result = encoder.validate_solution(&solution);
923
924            assert!(result.is_ok());
925        }
926
927        #[test]
928        fn test_validate_cyclical_swap_fail() {
929            // This test should fail because the cyclical swap is not the first and last token
930            // DAI -> WETH -> USDC -> DAI -> WBTC
931            // (some of the pool addresses in this test are fake)
932            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
933            let swaps = vec![
934                SwapBuilder::new(
935                    ProtocolComponent {
936                        id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
937                        protocol_system: "uniswap_v2".to_string(),
938                        ..Default::default()
939                    },
940                    dai().clone(),
941                    weth().clone(),
942                )
943                .build(),
944                SwapBuilder::new(
945                    ProtocolComponent {
946                        id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(),
947                        protocol_system: "uniswap_v2".to_string(),
948                        ..Default::default()
949                    },
950                    weth().clone(),
951                    usdc().clone(),
952                )
953                .build(),
954                SwapBuilder::new(
955                    ProtocolComponent {
956                        id: "0x0000000000000000000000000000000000000000".to_string(),
957                        protocol_system: "uniswap_v2".to_string(),
958                        ..Default::default()
959                    },
960                    usdc().clone(),
961                    dai().clone(),
962                )
963                .build(),
964                SwapBuilder::new(
965                    ProtocolComponent {
966                        id: "0x0000000000000000000000000000000000000000".to_string(),
967                        protocol_system: "uniswap_v2".to_string(),
968                        ..Default::default()
969                    },
970                    dai().clone(),
971                    wbtc().clone(),
972                )
973                .build(),
974            ];
975
976            let solution = Solution {
977                exact_out: false,
978                given_token: dai(),
979                checked_token: wbtc(),
980                swaps,
981                ..Default::default()
982            };
983
984            let result = encoder.validate_solution(&solution);
985
986            assert!(result.is_err());
987            assert_eq!(
988            result.err().unwrap(),
989            EncodingError::FatalError(
990                "Cyclical swaps are only allowed if they are the first and last token of a solution".to_string()
991            )
992        );
993        }
994        #[test]
995        fn test_validate_cyclical_swap_split_output() {
996            // This validation passes because it is a valid cyclical swap
997            //             -> WETH
998            // WETH -> DAI
999            //             -> WETH
1000            // (some of the pool addresses in this test are fake)
1001            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
1002            let swaps = vec![
1003                SwapBuilder::new(
1004                    ProtocolComponent {
1005                        id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
1006                        protocol_system: "uniswap_v2".to_string(),
1007                        ..Default::default()
1008                    },
1009                    weth(),
1010                    dai(),
1011                )
1012                .build(),
1013                SwapBuilder::new(
1014                    ProtocolComponent {
1015                        id: "0x0000000000000000000000000000000000000000".to_string(),
1016                        protocol_system: "uniswap_v2".to_string(),
1017                        ..Default::default()
1018                    },
1019                    dai(),
1020                    weth(),
1021                )
1022                .split(0.5)
1023                .build(),
1024                SwapBuilder::new(
1025                    ProtocolComponent {
1026                        id: "0x0000000000000000000000000000000000000000".to_string(),
1027                        protocol_system: "uniswap_v2".to_string(),
1028                        ..Default::default()
1029                    },
1030                    dai(),
1031                    weth(),
1032                )
1033                .build(),
1034            ];
1035
1036            let solution = Solution {
1037                exact_out: false,
1038                given_token: weth(),
1039                checked_token: weth(),
1040                swaps,
1041                ..Default::default()
1042            };
1043
1044            let result = encoder.validate_solution(&solution);
1045
1046            assert!(result.is_ok());
1047        }
1048
1049        #[test]
1050        fn test_validate_cyclical_swap_native_action_fail() {
1051            // This validation fails because there is a native action with a valid cyclical swap
1052            // ETH -> WETH -> DAI -> WETH
1053            // (some of the pool addresses in this test are fake)
1054            let encoder = get_tycho_router_encoder(UserTransferType::TransferFrom);
1055            let swaps = vec![
1056                SwapBuilder::new(
1057                    ProtocolComponent {
1058                        id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
1059                        protocol_system: "uniswap_v2".to_string(),
1060                        ..Default::default()
1061                    },
1062                    weth(),
1063                    dai(),
1064                )
1065                .build(),
1066                SwapBuilder::new(
1067                    ProtocolComponent {
1068                        id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
1069                        protocol_system: "uniswap_v2".to_string(),
1070                        ..Default::default()
1071                    },
1072                    dai(),
1073                    weth(),
1074                )
1075                .build(),
1076            ];
1077
1078            let solution = Solution {
1079                exact_out: false,
1080                given_token: eth(),
1081                checked_token: weth(),
1082                swaps,
1083                native_action: Some(NativeAction::Wrap),
1084                ..Default::default()
1085            };
1086
1087            let result = encoder.validate_solution(&solution);
1088
1089            assert!(result.is_err());
1090            assert_eq!(
1091                result.err().unwrap(),
1092                EncodingError::FatalError(
1093                    "Wrapping/Unwrapping is not available in cyclical swaps"
1094                        .to_string()
1095                        .to_string()
1096                )
1097            );
1098        }
1099    }
1100
1101    mod executor_encoder {
1102        use std::str::FromStr;
1103
1104        use alloy::hex::encode;
1105        use num_bigint::BigUint;
1106        use tycho_common::{models::protocol::ProtocolComponent, Bytes};
1107
1108        use super::*;
1109        use crate::encoding::models::Solution;
1110
1111        #[test]
1112        fn test_executor_encoder_encode() {
1113            let swap_encoder_registry = get_swap_encoder_registry();
1114            let encoder = TychoExecutorEncoder::new(swap_encoder_registry).unwrap();
1115
1116            let token_in = weth();
1117            let token_out = dai();
1118
1119            let swap = SwapBuilder::new(
1120                ProtocolComponent {
1121                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
1122                    protocol_system: "uniswap_v2".to_string(),
1123                    ..Default::default()
1124                },
1125                token_in.clone(),
1126                token_out.clone(),
1127            )
1128            .build();
1129
1130            let solution = Solution {
1131                exact_out: false,
1132                given_token: token_in,
1133                given_amount: BigUint::from(1000000000000000000u64),
1134                checked_token: token_out,
1135                checked_amount: BigUint::from(1000000000000000000u64),
1136                sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
1137                // The receiver was generated with `makeAddr("bob") using forge`
1138                receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(),
1139                swaps: vec![swap],
1140                native_action: None,
1141            };
1142
1143            let encoded_solutions = encoder
1144                .encode_solutions(vec![solution])
1145                .unwrap();
1146            let encoded = encoded_solutions
1147                .first()
1148                .expect("Expected at least one encoded solution");
1149            let hex_protocol_data = encode(&encoded.swaps);
1150            assert_eq!(
1151                encoded.interacting_with,
1152                Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap()
1153            );
1154            assert_eq!(
1155                hex_protocol_data,
1156                String::from(concat!(
1157                    // in token
1158                    "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
1159                    // component id
1160                    "a478c2975ab1ea89e8196811f51a7b7ade33eb11",
1161                    // receiver
1162                    "1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e",
1163                    // zero for one
1164                    "00",
1165                    // transfer true
1166                    "01",
1167                ))
1168            );
1169        }
1170
1171        #[test]
1172        fn test_executor_encoder_too_many_swaps() {
1173            let swap_encoder_registry = get_swap_encoder_registry();
1174            let encoder = TychoExecutorEncoder::new(swap_encoder_registry).unwrap();
1175
1176            let token_in = weth();
1177            let token_out = dai();
1178
1179            let swap = SwapBuilder::new(
1180                ProtocolComponent {
1181                    id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
1182                    protocol_system: "uniswap_v2".to_string(),
1183                    ..Default::default()
1184                },
1185                token_in.clone(),
1186                token_out.clone(),
1187            )
1188            .build();
1189
1190            let solution = Solution {
1191                exact_out: false,
1192                given_token: token_in,
1193                given_amount: BigUint::from(1000000000000000000u64),
1194                checked_token: token_out,
1195                checked_amount: BigUint::from(1000000000000000000u64),
1196                sender: Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(),
1197                receiver: Bytes::from_str("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e").unwrap(),
1198                swaps: vec![swap.clone(), swap],
1199                native_action: None,
1200            };
1201
1202            let result = encoder.encode_solutions(vec![solution]);
1203            assert!(result.is_err());
1204        }
1205
1206        #[test]
1207        fn test_executor_encoder_grouped_swaps() {
1208            let swap_encoder_registry = get_swap_encoder_registry();
1209            let encoder = TychoExecutorEncoder::new(swap_encoder_registry).unwrap();
1210
1211            let usdc = usdc();
1212            let pepe = pepe();
1213
1214            let solution = Solution {
1215                exact_out: false,
1216                given_token: usdc,
1217                given_amount: BigUint::from_str("1000_000000").unwrap(),
1218                checked_token: pepe,
1219                checked_amount: BigUint::from(1000000000000000000u64),
1220                sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
1221                receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
1222                swaps: vec![swap_usdc_eth_univ4(), swap_eth_pepe_univ4()],
1223                ..Default::default()
1224            };
1225
1226            let encoded_solutions = encoder
1227                .encode_solutions(vec![solution])
1228                .unwrap();
1229            let encoded_solution = encoded_solutions
1230                .first()
1231                .expect("Expected at least one encoded solution");
1232            let hex_protocol_data = encode(&encoded_solution.swaps);
1233            assert_eq!(
1234                encoded_solution.interacting_with,
1235                Bytes::from_str("0xf62849f9a0b5bf2913b396098f7c7019b51a820a").unwrap()
1236            );
1237            assert_eq!(
1238                hex_protocol_data,
1239                String::from(concat!(
1240                    // group in token
1241                    "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
1242                    // group out token
1243                    "6982508145454ce325ddbe47a25d4ec3d2311933",
1244                    // zero for one
1245                    "00",
1246                    // transfer type Transfer
1247                    "01",
1248                    // receiver
1249                    "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
1250                    // first pool intermediary token (ETH)
1251                    "0000000000000000000000000000000000000000",
1252                    // fee
1253                    "000bb8",
1254                    // tick spacing
1255                    "00003c",
1256                    // hook address (not set, so zero)
1257                    "0000000000000000000000000000000000000000",
1258                    // hook data length (0)
1259                    "0000",
1260                    // ple encoding
1261                    "0030",
1262                    // second pool intermediary token (PEPE)
1263                    "6982508145454ce325ddbe47a25d4ec3d2311933",
1264                    // fee
1265                    "0061a8",
1266                    // tick spacing
1267                    "0001f4",
1268                    // hook address (not set, so zero)
1269                    "0000000000000000000000000000000000000000",
1270                    // hook data length (0)
1271                    "0000",
1272                ))
1273            );
1274        }
1275    }
1276}