From 87de8bd6379f16cf8236a37b4ad31741b618c167 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 22 Apr 2025 12:14:21 +0100 Subject: [PATCH 1/5] chore: Refactor TransferOptimization to be a struct instead of a trait --- don't change below this line --- ENG-4446 Took 39 minutes --- .../evm/strategy_encoder/strategy_encoders.rs | 78 +++++++++---------- .../transfer_optimizations.rs | 66 ++++++++-------- 2 files changed, 69 insertions(+), 75 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 7dd7afd..bad91bf 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -32,17 +32,15 @@ use crate::encoding::{ /// * `permit2`: Permit2, responsible for managing permit2 operations and providing necessary /// signatures and permit2 objects for calling the router /// * `selector`: String, the selector for the swap function in the router contract -/// * `native_address`: Address of the chain's native token -/// * `wrapped_address`: Address of the chain's wrapped token /// * `router_address`: Address of the router to be used to execute swaps +/// * `transfer_optimization`: TransferOptimization, responsible for optimizing the token transfers #[derive(Clone)] pub struct SingleSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, permit2: Option, selector: String, - native_address: Bytes, - wrapped_address: Bytes, router_address: Bytes, + transfer_optimization: TransferOptimization, } impl SingleSwapStrategyEncoder { @@ -60,13 +58,17 @@ impl SingleSwapStrategyEncoder { "singleSwap(uint256,address,address,uint256,bool,bool,address,bytes)".to_string(), ) }; + let permit2_is_active = permit2.is_some(); Ok(Self { permit2, selector, swap_encoder_registry, - native_address: chain.native_token()?, - wrapped_address: chain.wrapped_token()?, router_address, + transfer_optimization: TransferOptimization::new( + chain.native_token()?, + chain.wrapped_token()?, + permit2_is_active, + ), }) } @@ -80,8 +82,6 @@ impl SingleSwapStrategyEncoder { } } -impl TransferOptimization for SingleSwapStrategyEncoder {} - impl StrategyEncoder for SingleSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { let grouped_swaps = group_swaps(solution.clone().swaps); @@ -127,15 +127,9 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer_type = self.get_transfer_type( - swap.clone(), - solution.given_token.clone(), - self.native_address.clone(), - self.wrapped_address.clone(), - self.permit2.clone().is_some(), - wrap, - false, - ); + let transfer_type = self + .transfer_optimization + .get_transfer_type(swap.clone(), solution.given_token.clone(), wrap, false); let encoding_context = EncodingContext { receiver: swap_receiver.clone(), @@ -215,6 +209,7 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { /// * `router_address`: Address of the router to be used to execute swaps /// * `sequential_swap_validator`: SequentialSwapValidator, responsible for checking validity of /// sequential swap solutions +/// * `transfer_optimization`: TransferOptimization, responsible for optimizing the token transfers #[derive(Clone)] pub struct SequentialSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, @@ -224,10 +219,9 @@ pub struct SequentialSwapStrategyEncoder { native_address: Bytes, wrapped_address: Bytes, sequential_swap_validator: SequentialSwapValidator, + transfer_optimization: TransferOptimization, } -impl TransferOptimization for SequentialSwapStrategyEncoder {} - impl SequentialSwapStrategyEncoder { pub fn new( chain: Chain, @@ -244,6 +238,7 @@ impl SequentialSwapStrategyEncoder { .to_string(), ) }; + let permit2_is_active = permit2.is_some(); Ok(Self { permit2, selector, @@ -252,6 +247,11 @@ impl SequentialSwapStrategyEncoder { native_address: chain.native_token()?, wrapped_address: chain.wrapped_token()?, sequential_swap_validator: SequentialSwapValidator, + transfer_optimization: TransferOptimization::new( + chain.native_token()?, + chain.wrapped_token()?, + permit2_is_active, + ), }) } @@ -329,15 +329,14 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer_type = self.get_transfer_type( - swap.clone(), - solution.given_token.clone(), - self.native_address.clone(), - self.wrapped_address.clone(), - self.permit2.clone().is_some(), - wrap, - in_between_swap_optimization, - ); + let transfer_type = self + .transfer_optimization + .get_transfer_type( + swap.clone(), + solution.given_token.clone(), + wrap, + in_between_swap_optimization, + ); let encoding_context = EncodingContext { receiver: swap_receiver.clone(), @@ -422,6 +421,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { /// * `split_swap_validator`: SplitSwapValidator, responsible for checking validity of split swap /// solutions /// * `router_address`: Address of the router to be used to execute swaps +/// * `transfer_optimization`: TransferOptimization, responsible for optimizing the token transfers #[derive(Clone)] pub struct SplitSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, @@ -431,6 +431,7 @@ pub struct SplitSwapStrategyEncoder { wrapped_address: Bytes, split_swap_validator: SplitSwapValidator, router_address: Bytes, + transfer_optimization: TransferOptimization, } impl SplitSwapStrategyEncoder { @@ -449,7 +450,7 @@ impl SplitSwapStrategyEncoder { .to_string(), ) }; - + let permit2_is_active = permit2.is_some(); Ok(Self { permit2, selector, @@ -458,6 +459,11 @@ impl SplitSwapStrategyEncoder { wrapped_address: chain.wrapped_token()?, split_swap_validator: SplitSwapValidator, router_address, + transfer_optimization: TransferOptimization::new( + chain.native_token()?, + chain.wrapped_token()?, + permit2_is_active, + ), }) } @@ -481,8 +487,6 @@ impl SplitSwapStrategyEncoder { } } -impl TransferOptimization for SplitSwapStrategyEncoder {} - impl StrategyEncoder for SplitSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { self.split_swap_validator @@ -566,15 +570,9 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer_type = self.get_transfer_type( - swap.clone(), - solution.given_token.clone(), - self.native_address.clone(), - self.wrapped_address.clone(), - self.permit2.clone().is_some(), - wrap, - false, - ); + let transfer_type = self + .transfer_optimization + .get_transfer_type(swap.clone(), solution.given_token.clone(), wrap, false); let encoding_context = EncodingContext { receiver: swap_receiver.clone(), diff --git a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs index 97e08fc..2c19fec 100644 --- a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs +++ b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs @@ -5,17 +5,23 @@ use crate::encoding::{ models::{Swap, TransferType}, }; -/// A trait that defines how the tokens will be transferred into the given pool given the solution. -pub trait TransferOptimization { +/// A struct that defines how the tokens will be transferred into the given pool given the solution. +#[derive(Clone)] +pub struct TransferOptimization { + native_token: Bytes, + wrapped_token: Bytes, + permit2: bool, +} + +impl TransferOptimization { + pub fn new(native_token: Bytes, wrapped_token: Bytes, permit2: bool) -> Self { + TransferOptimization { native_token, wrapped_token, permit2 } + } /// Returns the transfer method that should be used for the given swap and solution. - #[allow(clippy::too_many_arguments)] - fn get_transfer_type( + pub fn get_transfer_type( &self, swap: Swap, given_token: Bytes, - native_token: Bytes, - wrapped_token: Bytes, - permit2: bool, wrap: bool, in_between_swap_optimization: bool, ) -> TransferType { @@ -24,22 +30,22 @@ pub trait TransferOptimization { let is_first_swap = swap.token_in == given_token; - if swap.token_in == native_token { + if swap.token_in == self.native_token { // Funds are already in router. All protocols currently take care of native transfers. TransferType::None - } else if (swap.token_in == wrapped_token) && wrap { + } else if (swap.token_in == self.wrapped_token) && wrap { // Wrapping already happened in the router so we can just use a normal transfer. TransferType::TransferToProtocol } else if is_first_swap { if in_transfer_required { - if permit2 { + if self.permit2 { // Transfer from swapper to pool using permit2. TransferType::TransferPermit2ToProtocol } else { // Transfer from swapper to pool. TransferType::TransferFromToProtocol } - } else if permit2 { + } else if self.permit2 { // Transfer from swapper to router using permit2. TransferType::TransferPermit2ToRouter } else { @@ -63,9 +69,6 @@ mod tests { use super::*; - struct MockStrategy {} - impl TransferOptimization for MockStrategy {} - fn weth() -> Bytes { Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) } @@ -94,9 +97,8 @@ mod tests { token_out: dai(), split: 0f64, }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), true, false, false); + let optimization = TransferOptimization::new(eth(), weth(), true); + let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::TransferPermit2ToProtocol); } @@ -112,9 +114,8 @@ mod tests { token_out: dai(), split: 0f64, }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, false); + let optimization = TransferOptimization::new(eth(), weth(), false); + let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::TransferFromToProtocol); } @@ -131,9 +132,8 @@ mod tests { token_out: dai(), split: 0f64, }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), eth(), eth(), weth(), false, false, false); + let optimization = TransferOptimization::new(eth(), weth(), false); + let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), false, false); assert_eq!(transfer_method, TransferType::None); } @@ -150,9 +150,8 @@ mod tests { token_out: dai(), split: 0f64, }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), eth(), eth(), weth(), false, true, false); + let optimization = TransferOptimization::new(eth(), weth(), false); + let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), true, false); assert_eq!(transfer_method, TransferType::TransferToProtocol); } @@ -169,9 +168,8 @@ mod tests { token_out: dai(), split: 0f64, }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, false); + let optimization = TransferOptimization::new(eth(), weth(), false); + let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::TransferToProtocol); } @@ -188,9 +186,8 @@ mod tests { token_out: dai(), split: 0f64, }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, false); + let optimization = TransferOptimization::new(eth(), weth(), false); + let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::None); } @@ -207,9 +204,8 @@ mod tests { token_out: dai(), split: 0f64, }; - let strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, true); + let optimization = TransferOptimization::new(eth(), weth(), false); + let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, true); assert_eq!(transfer_method, TransferType::None); } } From 304740574b31b9af6ce84971e2fe912ee1627cd8 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 22 Apr 2025 12:41:59 +0100 Subject: [PATCH 2/5] chore: get_transfer_type should receive a SwapGroup and not a Swap Only the first swap of a SwapGroup would actually get a transfer in, so we don't need to do this inside of the swaps loop Rename in_token and out_token to token_in and token_out in SwapGroup --- don't change below this line --- ENG-4446 Took 22 minutes --- src/encoding/evm/group_swaps.rs | 42 ++++----- .../evm/strategy_encoder/strategy_encoders.rs | 89 +++++++++---------- .../transfer_optimizations.rs | 66 ++++++-------- 3 files changed, 91 insertions(+), 106 deletions(-) diff --git a/src/encoding/evm/group_swaps.rs b/src/encoding/evm/group_swaps.rs index 1409b9f..5ec5fe6 100644 --- a/src/encoding/evm/group_swaps.rs +++ b/src/encoding/evm/group_swaps.rs @@ -6,15 +6,15 @@ use crate::encoding::{evm::constants::GROUPABLE_PROTOCOLS, models::Swap}; /// optimization. /// /// # Fields -/// * `input_token`: Bytes, the input token of the first swap -/// * `output_token`: Bytes, the output token of the final swap +/// * `token_in`: Bytes, the input token of the first swap +/// * `token_out`: Bytes, the output token of the final swap /// * `protocol_system`: String, the protocol system of the swaps /// * `swaps`: Vec, the sequence of swaps to be executed as a group /// * `split`: f64, the split percentage of the first swap in the group #[derive(Clone, PartialEq, Debug)] pub struct SwapGroup { - pub input_token: Bytes, - pub output_token: Bytes, + pub token_in: Bytes, + pub token_out: Bytes, pub protocol_system: String, pub swaps: Vec, pub split: f64, @@ -44,7 +44,7 @@ pub fn group_swaps(swaps: Vec) -> Vec { if let Some(group) = current_group.as_mut() { group.swaps.push(swap.clone()); // Update the output token of the current group. - group.output_token = swap.token_out.clone(); + group.token_out = swap.token_out.clone(); } } else { // Not second or later USV4 pool. Push the current group (if it exists) and then @@ -53,8 +53,8 @@ pub fn group_swaps(swaps: Vec) -> Vec { grouped_swaps.push(group.clone()); } current_group = Some(SwapGroup { - input_token: swap.token_in.clone(), - output_token: swap.token_out.clone(), + token_in: swap.token_in.clone(), + token_out: swap.token_out.clone(), protocol_system: current_swap_protocol.clone(), swaps: vec![swap.clone()], split: swap.split, @@ -135,15 +135,15 @@ mod tests { vec![ SwapGroup { swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], - input_token: weth, - output_token: usdc.clone(), + token_in: weth, + token_out: usdc.clone(), protocol_system: "uniswap_v4".to_string(), split: 0f64, }, SwapGroup { swaps: vec![swap_usdc_dai], - input_token: usdc, - output_token: dai, + token_in: usdc, + token_out: dai, protocol_system: "uniswap_v2".to_string(), split: 0f64, } @@ -216,22 +216,22 @@ mod tests { vec![ SwapGroup { swaps: vec![swap_wbtc_weth], - input_token: wbtc.clone(), - output_token: weth.clone(), + token_in: wbtc.clone(), + token_out: weth.clone(), protocol_system: "uniswap_v4".to_string(), split: 0f64, }, SwapGroup { swaps: vec![swap_weth_usdc], - input_token: weth.clone(), - output_token: usdc.clone(), + token_in: weth.clone(), + token_out: usdc.clone(), protocol_system: "uniswap_v4".to_string(), split: 0.5f64, }, SwapGroup { swaps: vec![swap_weth_dai, swap_dai_usdc], - input_token: weth, - output_token: usdc, + token_in: weth, + token_out: usdc, protocol_system: "uniswap_v4".to_string(), split: 0f64, } @@ -304,15 +304,15 @@ mod tests { vec![ SwapGroup { swaps: vec![swap_weth_wbtc, swap_wbtc_usdc], - input_token: weth.clone(), - output_token: usdc.clone(), + token_in: weth.clone(), + token_out: usdc.clone(), protocol_system: "vm:balancer_v3".to_string(), split: 0.5f64, }, SwapGroup { swaps: vec![swap_weth_dai, swap_dai_usdc], - input_token: weth, - output_token: usdc, + token_in: weth, + token_out: usdc, protocol_system: "uniswap_v4".to_string(), split: 0f64, } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index bad91bf..1d473c1 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -125,20 +125,20 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { let swap_receiver = if !unwrap { solution.receiver.clone() } else { self.router_address.clone() }; + let transfer_type = self + .transfer_optimization + .get_transfer_type(grouped_swap.clone(), solution.given_token.clone(), wrap, false); + let encoding_context = EncodingContext { + receiver: swap_receiver.clone(), + exact_out: solution.exact_out, + router_address: Some(self.router_address.clone()), + group_token_in: grouped_swap.token_in.clone(), + group_token_out: grouped_swap.token_out.clone(), + transfer_type: transfer_type.clone(), + }; + let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer_type = self - .transfer_optimization - .get_transfer_type(swap.clone(), solution.given_token.clone(), wrap, false); - - let encoding_context = EncodingContext { - receiver: swap_receiver.clone(), - exact_out: solution.exact_out, - router_address: Some(self.router_address.clone()), - group_token_in: grouped_swap.input_token.clone(), - group_token_out: grouped_swap.output_token.clone(), - transfer_type: transfer_type.clone(), - }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; grouped_protocol_data.extend(protocol_data); } @@ -327,25 +327,25 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { solution.receiver.clone() // last swap - there is not next swap }; + let transfer_type = self + .transfer_optimization + .get_transfer_type( + grouped_swap.clone(), + solution.given_token.clone(), + wrap, + in_between_swap_optimization, + ); + let encoding_context = EncodingContext { + receiver: swap_receiver.clone(), + exact_out: solution.exact_out, + router_address: Some(self.router_address.clone()), + group_token_in: grouped_swap.token_in.clone(), + group_token_out: grouped_swap.token_out.clone(), + transfer_type: transfer_type.clone(), + }; + let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer_type = self - .transfer_optimization - .get_transfer_type( - swap.clone(), - solution.given_token.clone(), - wrap, - in_between_swap_optimization, - ); - - let encoding_context = EncodingContext { - receiver: swap_receiver.clone(), - exact_out: solution.exact_out, - router_address: Some(self.router_address.clone()), - group_token_in: grouped_swap.input_token.clone(), - group_token_out: grouped_swap.output_token.clone(), - transfer_type: transfer_type.clone(), - }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; grouped_protocol_data.extend(protocol_data); @@ -517,7 +517,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { let intermediary_tokens: HashSet = grouped_swaps .iter() .flat_map(|grouped_swap| { - vec![grouped_swap.input_token.clone(), grouped_swap.output_token.clone()] + vec![grouped_swap.token_in.clone(), grouped_swap.token_out.clone()] }) .collect(); let mut intermediary_tokens: Vec = intermediary_tokens @@ -562,34 +562,33 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { )) })?; - let swap_receiver = if !unwrap && grouped_swap.output_token == solution.checked_token { + let swap_receiver = if !unwrap && grouped_swap.token_out == solution.checked_token { solution.receiver.clone() } else { self.router_address.clone() }; + let transfer_type = self + .transfer_optimization + .get_transfer_type(grouped_swap.clone(), solution.given_token.clone(), wrap, false); + let encoding_context = EncodingContext { + receiver: swap_receiver.clone(), + exact_out: solution.exact_out, + router_address: Some(self.router_address.clone()), + group_token_in: grouped_swap.token_in.clone(), + group_token_out: grouped_swap.token_out.clone(), + transfer_type: transfer_type.clone(), + }; let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { - let transfer_type = self - .transfer_optimization - .get_transfer_type(swap.clone(), solution.given_token.clone(), wrap, false); - - let encoding_context = EncodingContext { - receiver: swap_receiver.clone(), - exact_out: solution.exact_out, - router_address: Some(self.router_address.clone()), - group_token_in: grouped_swap.input_token.clone(), - group_token_out: grouped_swap.output_token.clone(), - transfer_type: transfer_type.clone(), - }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; grouped_protocol_data.extend(protocol_data); } let swap_data = self.encode_swap_header( - get_token_position(tokens.clone(), grouped_swap.input_token.clone())?, - get_token_position(tokens.clone(), grouped_swap.output_token.clone())?, + get_token_position(tokens.clone(), grouped_swap.token_in.clone())?, + get_token_position(tokens.clone(), grouped_swap.token_out.clone())?, percentage_to_uint24(grouped_swap.split), Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { EncodingError::FatalError("Invalid executor address".to_string()) diff --git a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs index 2c19fec..3d0b8a1 100644 --- a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs +++ b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs @@ -1,8 +1,8 @@ use tycho_common::Bytes; use crate::encoding::{ - evm::constants::IN_TRANSFER_REQUIRED_PROTOCOLS, - models::{Swap, TransferType}, + evm::{constants::IN_TRANSFER_REQUIRED_PROTOCOLS, group_swaps::SwapGroup}, + models::TransferType, }; /// A struct that defines how the tokens will be transferred into the given pool given the solution. @@ -20,13 +20,13 @@ impl TransferOptimization { /// Returns the transfer method that should be used for the given swap and solution. pub fn get_transfer_type( &self, - swap: Swap, + swap: SwapGroup, given_token: Bytes, wrap: bool, in_between_swap_optimization: bool, ) -> TransferType { let in_transfer_required: bool = - IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&swap.component.protocol_system.as_str()); + IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&swap.protocol_system.as_str()); let is_first_swap = swap.token_in == given_token; @@ -65,7 +65,7 @@ impl TransferOptimization { #[cfg(test)] mod tests { use alloy_primitives::hex; - use tycho_common::{models::protocol::ProtocolComponent, Bytes}; + use tycho_common::Bytes; use super::*; @@ -88,14 +88,12 @@ mod tests { #[test] fn test_first_swap_transfer_from_permit2() { // The swap token is the same as the given token, which is not the native token - let swap = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: weth(), token_out: dai(), split: 0f64, + swaps: vec![], }; let optimization = TransferOptimization::new(eth(), weth(), true); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); @@ -105,14 +103,12 @@ mod tests { #[test] fn test_first_swap_transfer_from() { // The swap token is the same as the given token, which is not the native token - let swap = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: weth(), token_out: dai(), split: 0f64, + swaps: vec![], }; let optimization = TransferOptimization::new(eth(), weth(), false); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); @@ -123,14 +119,12 @@ mod tests { fn test_first_swap_native() { // The swap token is the same as the given token, and it's the native token. // No transfer action is needed. - let swap = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: eth(), token_out: dai(), split: 0f64, + swaps: vec![], }; let optimization = TransferOptimization::new(eth(), weth(), false); let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), false, false); @@ -141,14 +135,12 @@ mod tests { fn test_first_swap_wrapped() { // The swap token is NOT the same as the given token, but we are wrapping. // Since the swap's token in is the wrapped token - this is the first swap. - let swap = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: weth(), token_out: dai(), split: 0f64, + swaps: vec![], }; let optimization = TransferOptimization::new(eth(), weth(), false); let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), true, false); @@ -159,14 +151,12 @@ mod tests { fn test_not_first_swap() { // The swap token is NOT the same as the given token, and we are NOT wrapping. // Thus, this is not the first swap. - let swap = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: usdc(), token_out: dai(), split: 0f64, + swaps: vec![], }; let optimization = TransferOptimization::new(eth(), weth(), false); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); @@ -177,14 +167,12 @@ mod tests { fn test_not_first_swap_funds_in_router() { // Not the first swap and the protocol requires the funds to be in the router (which they // already are, so the transfer type is None) - let swap = Swap { - component: ProtocolComponent { - protocol_system: "vm:curve".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "vm:curve".to_string(), token_in: usdc(), token_out: dai(), split: 0f64, + swaps: vec![], }; let optimization = TransferOptimization::new(eth(), weth(), false); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); @@ -195,14 +183,12 @@ mod tests { fn test_not_first_swap_in_between_swap_optimization() { // Not the first swap and the in between swaps are optimized. The funds should already be in // the next pool or in the router - let swap = Swap { - component: ProtocolComponent { - protocol_system: "uniswap_v2".to_string(), - ..Default::default() - }, + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), token_in: usdc(), token_out: dai(), split: 0f64, + swaps: vec![], }; let optimization = TransferOptimization::new(eth(), weth(), false); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, true); From df92be887573b297b22f2e01317305f2e5bb7e75 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 22 Apr 2025 14:01:03 +0100 Subject: [PATCH 3/5] feat: Allow for token_in_already_in_router --- don't change below this line --- ENG-4446 Took 31 minutes Took 26 seconds --- src/encoding/evm/encoder_builders.rs | 14 ++++ .../evm/strategy_encoder/strategy_encoders.rs | 26 +++++++ .../transfer_optimizations.rs | 78 +++++++++++++++---- src/encoding/evm/tycho_encoders.rs | 9 ++- 4 files changed, 109 insertions(+), 18 deletions(-) diff --git a/src/encoding/evm/encoder_builders.rs b/src/encoding/evm/encoder_builders.rs index 94929a7..ee412ac 100644 --- a/src/encoding/evm/encoder_builders.rs +++ b/src/encoding/evm/encoder_builders.rs @@ -21,6 +21,7 @@ pub struct TychoRouterEncoderBuilder { chain: Option, executors_file_path: Option, router_address: Option, + token_in_already_in_router: Option, } impl Default for TychoRouterEncoderBuilder { @@ -36,6 +37,7 @@ impl TychoRouterEncoderBuilder { chain: None, executors_file_path: None, router_address: None, + token_in_already_in_router: None, } } pub fn chain(mut self, chain: TychoCommonChain) -> Self { @@ -62,6 +64,16 @@ impl TychoRouterEncoderBuilder { self } + // Sets the `token_in_already_in_router` flag. + // If set to true, the encoder will assume that the token in is already in the router. + // WARNING: this is an advanced feature and should be used with caution. Make sure you have + // checks to make sure that your tokens won't be lost. The Router is not considered safe to hold + // tokens, so if this is not done within the same transaction you will lose your tokens. + pub fn token_in_already_in_router(mut self, token_in_already_in_router: bool) -> Self { + self.token_in_already_in_router = Some(token_in_already_in_router); + self + } + /// Builds the `TychoRouterEncoder` instance using the configured chain. /// Returns an error if either the chain has not been set. pub fn build(self) -> Result, EncodingError> { @@ -88,6 +100,8 @@ impl TychoRouterEncoderBuilder { swap_encoder_registry, self.swapper_pk, tycho_router_address, + self.token_in_already_in_router + .unwrap_or(false), )?)) } else { Err(EncodingError::FatalError( diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 1d473c1..81e9988 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -49,6 +49,7 @@ impl SingleSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, + token_in_already_in_router: bool, ) -> Result { let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { (Some(Permit2::new(swapper_pk, chain.clone())?), "singleSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) @@ -68,6 +69,7 @@ impl SingleSwapStrategyEncoder { chain.native_token()?, chain.wrapped_token()?, permit2_is_active, + token_in_already_in_router, ), }) } @@ -228,6 +230,7 @@ impl SequentialSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, + token_in_already_in_router: bool, ) -> Result { let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { (Some(Permit2::new(swapper_pk, chain.clone())?), "sequentialSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) @@ -251,6 +254,7 @@ impl SequentialSwapStrategyEncoder { chain.native_token()?, chain.wrapped_token()?, permit2_is_active, + token_in_already_in_router, ), }) } @@ -440,6 +444,7 @@ impl SplitSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, + token_in_already_in_router: bool, ) -> Result { let (permit2, selector) = if let Some(swapper_pk) = swapper_pk { (Some(Permit2::new(swapper_pk, chain.clone())?), "splitSwapPermit2(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string()) @@ -463,6 +468,7 @@ impl SplitSwapStrategyEncoder { chain.native_token()?, chain.wrapped_token()?, permit2_is_active, + token_in_already_in_router, ), }) } @@ -741,6 +747,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -824,6 +831,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -901,6 +909,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -953,6 +962,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -1024,6 +1034,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1083,6 +1094,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1201,6 +1213,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -1318,6 +1331,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1399,6 +1413,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1484,6 +1499,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1550,6 +1566,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1649,6 +1666,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -1758,6 +1776,7 @@ mod tests { swap_encoder_registry, Some(private_key.clone()), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -1920,6 +1939,7 @@ mod tests { swap_encoder_registry, Some(private_key.clone()), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -2045,6 +2065,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0xA4AD4f68d0b91CFD19687c881e50f3A00242828c").unwrap(), + false, ) .unwrap(); @@ -2110,6 +2131,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -2178,6 +2200,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -2265,6 +2288,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -2367,6 +2391,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); @@ -2429,6 +2454,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); diff --git a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs index 3d0b8a1..4b4586f 100644 --- a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs +++ b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs @@ -11,11 +11,17 @@ pub struct TransferOptimization { native_token: Bytes, wrapped_token: Bytes, permit2: bool, + token_in_already_in_router: bool, } impl TransferOptimization { - pub fn new(native_token: Bytes, wrapped_token: Bytes, permit2: bool) -> Self { - TransferOptimization { native_token, wrapped_token, permit2 } + pub fn new( + native_token: Bytes, + wrapped_token: Bytes, + permit2: bool, + token_in_already_in_router: bool, + ) -> Self { + TransferOptimization { native_token, wrapped_token, permit2, token_in_already_in_router } } /// Returns the transfer method that should be used for the given swap and solution. pub fn get_transfer_type( @@ -38,19 +44,28 @@ impl TransferOptimization { TransferType::TransferToProtocol } else if is_first_swap { if in_transfer_required { - if self.permit2 { + if self.token_in_already_in_router { + // Transfer from router to pool. + TransferType::TransferToProtocol + } else if self.permit2 { // Transfer from swapper to pool using permit2. TransferType::TransferPermit2ToProtocol } else { // Transfer from swapper to pool. TransferType::TransferFromToProtocol } - } else if self.permit2 { - // Transfer from swapper to router using permit2. - TransferType::TransferPermit2ToRouter + // in transfer is not necessary for these protocols. Only make a transfer if the + // tokens are not already in the router + } else if !self.token_in_already_in_router { + if self.permit2 { + // Transfer from swapper to router using permit2. + TransferType::TransferPermit2ToRouter + } else { + // Transfer from swapper to router. + TransferType::TransferFromToRouter + } } else { - // Transfer from swapper to router. - TransferType::TransferFromToRouter + TransferType::None } // all other swaps } else if !in_transfer_required || in_between_swap_optimization { @@ -65,7 +80,6 @@ impl TransferOptimization { #[cfg(test)] mod tests { use alloy_primitives::hex; - use tycho_common::Bytes; use super::*; @@ -95,7 +109,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), true); + let optimization = TransferOptimization::new(eth(), weth(), true, false); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::TransferPermit2ToProtocol); } @@ -110,7 +124,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false); + let optimization = TransferOptimization::new(eth(), weth(), false, false); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::TransferFromToProtocol); } @@ -126,7 +140,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false); + let optimization = TransferOptimization::new(eth(), weth(), false, false); let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), false, false); assert_eq!(transfer_method, TransferType::None); } @@ -142,7 +156,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false); + let optimization = TransferOptimization::new(eth(), weth(), false, false); let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), true, false); assert_eq!(transfer_method, TransferType::TransferToProtocol); } @@ -158,7 +172,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false); + let optimization = TransferOptimization::new(eth(), weth(), false, false); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::TransferToProtocol); } @@ -174,7 +188,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false); + let optimization = TransferOptimization::new(eth(), weth(), false, false); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::None); } @@ -190,8 +204,40 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false); + let optimization = TransferOptimization::new(eth(), weth(), false, false); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, true); assert_eq!(transfer_method, TransferType::None); } + + #[test] + fn test_first_swap_tokens_already_in_router_optimization() { + // It is the first swap, tokens are already in the router and the protocol requires the + // transfer in + let swap = SwapGroup { + protocol_system: "uniswap_v2".to_string(), + token_in: usdc(), + token_out: dai(), + split: 0f64, + swaps: vec![], + }; + let optimization = TransferOptimization::new(eth(), weth(), false, true); + let transfer_method = optimization.get_transfer_type(swap.clone(), usdc(), false, false); + assert_eq!(transfer_method, TransferType::TransferToProtocol); + } + + #[test] + fn test_first_swap_tokens_already_in_router_no_transfer_needed_optimization() { + // It is the first swap, tokens are already in the router and the protocol does not require + // the transfer in + let swap = SwapGroup { + protocol_system: "vm:curve".to_string(), + token_in: usdc(), + token_out: dai(), + split: 0f64, + swaps: vec![], + }; + let optimization = TransferOptimization::new(eth(), weth(), false, true); + let transfer_method = optimization.get_transfer_type(swap.clone(), usdc(), false, false); + assert_eq!(transfer_method, TransferType::None); + } } diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs index 5d285e0..a4bbc7a 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -39,6 +39,7 @@ impl TychoRouterEncoder { swap_encoder_registry: SwapEncoderRegistry, swapper_pk: Option, router_address: Bytes, + token_in_already_in_router: bool, ) -> Result { let native_address = chain.native_token()?; let wrapped_address = chain.wrapped_token()?; @@ -48,18 +49,21 @@ impl TychoRouterEncoder { swap_encoder_registry.clone(), swapper_pk.clone(), router_address.clone(), + token_in_already_in_router, )?, sequential_swap_strategy: SequentialSwapStrategyEncoder::new( chain.clone(), swap_encoder_registry.clone(), swapper_pk.clone(), router_address.clone(), + token_in_already_in_router, )?, split_swap_strategy: SplitSwapStrategyEncoder::new( chain, swap_encoder_registry, None, router_address.clone(), + token_in_already_in_router, )?, native_address, wrapped_address, @@ -258,8 +262,8 @@ impl TychoExecutorEncoder { receiver: receiver.clone(), exact_out: solution.exact_out, router_address: None, - group_token_in: grouped_swap.input_token.clone(), - group_token_out: grouped_swap.output_token.clone(), + group_token_in: grouped_swap.token_in.clone(), + group_token_out: grouped_swap.token_out.clone(), transfer_type: TransferType::TransferToProtocol, }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; @@ -354,6 +358,7 @@ mod tests { get_swap_encoder_registry(), None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap() } From cebacc68fea9a8dbfcc9d42b81b813fb61c5acdc Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 22 Apr 2025 14:15:39 +0100 Subject: [PATCH 4/5] test: Add integration test for it --- don't change below this line --- ENG-4446 Took 12 minutes --- foundry/test/TychoRouterSingleSwap.t.sol | 18 +++++ .../evm/strategy_encoder/strategy_encoders.rs | 79 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/foundry/test/TychoRouterSingleSwap.t.sol b/foundry/test/TychoRouterSingleSwap.t.sol index 1a374c7..fba1f15 100644 --- a/foundry/test/TychoRouterSingleSwap.t.sol +++ b/foundry/test/TychoRouterSingleSwap.t.sol @@ -367,4 +367,22 @@ contract TychoRouterSingleSwapTest is TychoRouterTestSetup { assertTrue(success, "Call Failed"); assertEq(balanceAfter - balanceBefore, 1120007305574805922); } + + function testSingleSwapIntegrationNoTransferIn() public { + // Tests swapping WETH -> DAI on a USV2 pool assuming that the tokens are already inside the router + deal(WETH_ADDR, tychoRouterAddr, 1 ether); + uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE); + + vm.startPrank(ALICE); + // Encoded solution generated using `test_single_swap_strategy_encoder_no_transfer_in` + (bool success,) = tychoRouterAddr.call( + hex"20144a070000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000008f1d5c1cae3740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000525615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb11cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000" + ); + + vm.stopPrank(); + + uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + assertTrue(success, "Call Failed"); + assertEq(balanceAfter - balanceBefore, 2659881924818443699787); + } } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 81e9988..dd1433a 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -881,6 +881,85 @@ mod tests { println!("test_single_swap_strategy_encoder_no_permit2: {}", hex_calldata); } + #[test] + fn test_single_swap_strategy_encoder_no_transfer_in() { + // Performs a single swap from WETH to DAI on a USV2 pool assuming that the tokens are + // already in the router + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let expected_amount = Some(BigUint::from_str("2_650_000000000000000000").unwrap()); + let slippage = Some(0.01f64); + let checked_amount = Some(BigUint::from_str("2_640_000000000000000000").unwrap()); + let expected_min_amount = U256::from_str("2_640_000000000000000000").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SingleSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + true, + ) + .unwrap(); + let solution = Solution { + exact_out: false, + given_token: weth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: dai, + expected_amount, + slippage, + checked_amount, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount)); + let expected_input = [ + "20144a07", // Function selector + "0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount in + "000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out + &expected_min_amount_encoded, // min amount out + "0000000000000000000000000000000000000000000000000000000000000000", // wrap + "0000000000000000000000000000000000000000000000000000000000000000", // unwrap + "000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "0000000000000000000000000000000000000000000000000000000000000100", // offset of swap bytes + "0000000000000000000000000000000000000000000000000000000000000052", // length of swap bytes without padding + + // Swap data + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in + "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id + "cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver + "00", // zero2one + "00", // transfer type + "0000000000000000000000000000", // padding + ] + .join(""); + + let hex_calldata = encode(&calldata); + + assert_eq!(hex_calldata, expected_input); + println!("test_single_swap_strategy_encoder_no_transfer_in: {}", hex_calldata); + } + #[test] fn test_single_swap_strategy_encoder_wrap() { // Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH From dff4a345fcfb36c1c9b87805cadc02701d2d63bb Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 22 Apr 2025 15:30:33 +0100 Subject: [PATCH 5/5] chore: Move get_receiver logic inside TransferOptimization Added tests for all cases --- don't change below this line --- ENG-4446 Took 1 hour 8 minutes --- .../evm/strategy_encoder/strategy_encoders.rs | 42 ++---- .../transfer_optimizations.rs | 131 ++++++++++++++++-- 2 files changed, 133 insertions(+), 40 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index dd1433a..af757c8 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -8,7 +8,6 @@ use crate::encoding::{ errors::EncodingError, evm::{ approvals::permit2::Permit2, - constants::{CALLBACK_CONSTRAINED_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS}, group_swaps::group_swaps, strategy_encoder::{ strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator}, @@ -64,12 +63,13 @@ impl SingleSwapStrategyEncoder { permit2, selector, swap_encoder_registry, - router_address, + router_address: router_address.clone(), transfer_optimization: TransferOptimization::new( chain.native_token()?, chain.wrapped_token()?, permit2_is_active, token_in_already_in_router, + router_address, ), }) } @@ -246,7 +246,7 @@ impl SequentialSwapStrategyEncoder { permit2, selector, swap_encoder_registry, - router_address, + router_address: router_address.clone(), native_address: chain.native_token()?, wrapped_address: chain.wrapped_token()?, sequential_swap_validator: SequentialSwapValidator, @@ -255,6 +255,7 @@ impl SequentialSwapStrategyEncoder { chain.wrapped_token()?, permit2_is_active, token_in_already_in_router, + router_address, ), }) } @@ -295,7 +296,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { } let mut swaps = vec![]; - let mut next_in_between_swap_optimization = true; + let mut next_in_between_swap_optimization_allowed = true; for (i, grouped_swap) in grouped_swaps.iter().enumerate() { let protocol = grouped_swap.protocol_system.clone(); let swap_encoder = self @@ -307,37 +308,19 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { )) })?; - let in_between_swap_optimization = next_in_between_swap_optimization; + let in_between_swap_optimization_allowed = next_in_between_swap_optimization_allowed; let next_swap = grouped_swaps.get(i + 1); - // if there is a next swap - let swap_receiver = if let Some(next) = next_swap { - // if the protocol of the next swap supports transfer in optimization - if IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&next.protocol_system.as_str()) { - // if the protocol does not allow for chained swaps, we can't optimize the - // receiver of this swap nor the transfer in of the next swap - if CALLBACK_CONSTRAINED_PROTOCOLS.contains(&next.protocol_system.as_str()) { - next_in_between_swap_optimization = false; - self.router_address.clone() - } else { - Bytes::from_str(&next.swaps[0].component.id.clone()).map_err(|_| { - EncodingError::FatalError("Invalid component id".to_string()) - })? - } - } else { - // the protocol of the next swap does not support transfer in optimization - self.router_address.clone() - } - } else { - solution.receiver.clone() // last swap - there is not next swap - }; - + let (swap_receiver, next_swap_optimization) = self + .transfer_optimization + .get_receiver(solution.receiver.clone(), next_swap)?; + next_in_between_swap_optimization_allowed = next_swap_optimization; let transfer_type = self .transfer_optimization .get_transfer_type( grouped_swap.clone(), solution.given_token.clone(), wrap, - in_between_swap_optimization, + in_between_swap_optimization_allowed, ); let encoding_context = EncodingContext { receiver: swap_receiver.clone(), @@ -463,12 +446,13 @@ impl SplitSwapStrategyEncoder { native_address: chain.native_token()?, wrapped_address: chain.wrapped_token()?, split_swap_validator: SplitSwapValidator, - router_address, + router_address: router_address.clone(), transfer_optimization: TransferOptimization::new( chain.native_token()?, chain.wrapped_token()?, permit2_is_active, token_in_already_in_router, + router_address, ), }) } diff --git a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs index 4b4586f..62bfca1 100644 --- a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs +++ b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs @@ -1,7 +1,13 @@ +use std::str::FromStr; + use tycho_common::Bytes; use crate::encoding::{ - evm::{constants::IN_TRANSFER_REQUIRED_PROTOCOLS, group_swaps::SwapGroup}, + errors::EncodingError, + evm::{ + constants::{CALLBACK_CONSTRAINED_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS}, + group_swaps::SwapGroup, + }, models::TransferType, }; @@ -12,6 +18,7 @@ pub struct TransferOptimization { wrapped_token: Bytes, permit2: bool, token_in_already_in_router: bool, + router_address: Bytes, } impl TransferOptimization { @@ -20,9 +27,17 @@ impl TransferOptimization { wrapped_token: Bytes, permit2: bool, token_in_already_in_router: bool, + router_address: Bytes, ) -> Self { - TransferOptimization { native_token, wrapped_token, permit2, token_in_already_in_router } + TransferOptimization { + native_token, + wrapped_token, + permit2, + token_in_already_in_router, + router_address, + } } + /// Returns the transfer method that should be used for the given swap and solution. pub fn get_transfer_type( &self, @@ -75,13 +90,50 @@ impl TransferOptimization { TransferType::TransferToProtocol } } + + // Returns the optimized receiver of the swap. This is used to chain swaps together and avoid + // unnecessary token transfers. + // Returns the receiver address and a boolean indicating whether the receiver is optimized (this + // is necessary for the next swap transfer type decision). + pub fn get_receiver( + &self, + solution_receiver: Bytes, + next_swap: Option<&SwapGroup>, + ) -> Result<(Bytes, bool), EncodingError> { + if let Some(next) = next_swap { + // if the protocol of the next swap supports transfer in optimization + if IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&next.protocol_system.as_str()) { + // if the protocol does not allow for chained swaps, we can't optimize the + // receiver of this swap nor the transfer in of the next swap + if CALLBACK_CONSTRAINED_PROTOCOLS.contains(&next.protocol_system.as_str()) { + Ok((self.router_address.clone(), false)) + } else { + Ok(( + Bytes::from_str(&next.swaps[0].component.id.clone()).map_err(|_| { + EncodingError::FatalError("Invalid component id".to_string()) + })?, + true, + )) + } + } else { + // the protocol of the next swap does not support transfer in optimization + Ok((self.router_address.clone(), false)) + } + } else { + // last swap - there is no next swap + Ok((solution_receiver, false)) + } + } } #[cfg(test)] mod tests { use alloy_primitives::hex; + use rstest::rstest; + use tycho_common::models::protocol::ProtocolComponent; use super::*; + use crate::encoding::models::Swap; fn weth() -> Bytes { Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) @@ -99,6 +151,10 @@ mod tests { Bytes::from(hex!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").to_vec()) } + fn router_address() -> Bytes { + Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f") + } + #[test] fn test_first_swap_transfer_from_permit2() { // The swap token is the same as the given token, which is not the native token @@ -109,7 +165,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), true, false); + let optimization = TransferOptimization::new(eth(), weth(), true, false, router_address()); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::TransferPermit2ToProtocol); } @@ -124,7 +180,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false, false); + let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address()); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::TransferFromToProtocol); } @@ -140,7 +196,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false, false); + let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address()); let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), false, false); assert_eq!(transfer_method, TransferType::None); } @@ -156,7 +212,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false, false); + let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address()); let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), true, false); assert_eq!(transfer_method, TransferType::TransferToProtocol); } @@ -172,7 +228,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false, false); + let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address()); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::TransferToProtocol); } @@ -188,7 +244,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false, false); + let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address()); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false); assert_eq!(transfer_method, TransferType::None); } @@ -204,7 +260,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false, false); + let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address()); let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, true); assert_eq!(transfer_method, TransferType::None); } @@ -220,7 +276,7 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false, true); + let optimization = TransferOptimization::new(eth(), weth(), false, true, router_address()); let transfer_method = optimization.get_transfer_type(swap.clone(), usdc(), false, false); assert_eq!(transfer_method, TransferType::TransferToProtocol); } @@ -236,8 +292,61 @@ mod tests { split: 0f64, swaps: vec![], }; - let optimization = TransferOptimization::new(eth(), weth(), false, true); + let optimization = TransferOptimization::new(eth(), weth(), false, true, router_address()); let transfer_method = optimization.get_transfer_type(swap.clone(), usdc(), false, false); assert_eq!(transfer_method, TransferType::None); } + + fn receiver() -> Bytes { + Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2") + } + + fn component_id() -> Bytes { + Bytes::from("0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11") + } + + #[rstest] + // there is no next swap -> receiver is the solution receiver + #[case(None, receiver(), false)] + // protocol of next swap supports transfer in optimization + #[case(Some("uniswap_v2"), component_id(), true)] + // protocol of next swap supports transfer in optimization but is callback constrained + #[case(Some("uniswap_v3"), router_address(), false)] + // protocol of next swap does not support transfer in optimization + #[case(Some("vm:curve"), router_address(), false)] + fn test_get_receiver( + #[case] protocol: Option<&str>, + #[case] expected_receiver: Bytes, + #[case] expected_optimization: bool, + ) { + let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address()); + + let next_swap = if protocol.is_none() { + None + } else { + Some(SwapGroup { + protocol_system: protocol.unwrap().to_string(), + token_in: usdc(), + token_out: dai(), + split: 0f64, + swaps: vec![Swap { + component: ProtocolComponent { + protocol_system: protocol.unwrap().to_string(), + id: component_id().to_string(), + ..Default::default() + }, + token_in: usdc(), + token_out: dai(), + split: 0f64, + }], + }) + }; + + let result = optimization.get_receiver(receiver(), next_swap.as_ref()); + + assert!(result.is_ok()); + let (actual_receiver, optimization_flag) = result.unwrap(); + assert_eq!(actual_receiver, expected_receiver); + assert_eq!(optimization_flag, expected_optimization); + } }