tycho_execution/encoding/evm/
tycho_encoders.rs

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