diff --git a/src/encoding/evm/constants.rs b/src/encoding/evm/constants.rs index feba1cc..ef44482 100644 --- a/src/encoding/evm/constants.rs +++ b/src/encoding/evm/constants.rs @@ -17,3 +17,12 @@ pub static GROUPABLE_PROTOCOLS: LazyLock> = LazyLock::new( set.insert("ekubo_v2"); set }); + +/// These protocols support the optimization of transferring straight from the user. +pub static IN_TRANSFER_OPTIMIZABLE_PROTOCOLS: LazyLock> = + LazyLock::new(|| { + let mut set = HashSet::new(); + set.insert("uniswap_v2"); + set.insert("uniswap_v3"); + set + }); diff --git a/src/encoding/evm/strategy_encoder/mod.rs b/src/encoding/evm/strategy_encoder/mod.rs index 8f53524..260436b 100644 --- a/src/encoding/evm/strategy_encoder/mod.rs +++ b/src/encoding/evm/strategy_encoder/mod.rs @@ -1,2 +1,4 @@ pub mod strategy_encoders; mod strategy_validators; + +mod transfer_optimizations; diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 3151034..e5174af 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -9,8 +9,9 @@ use crate::encoding::{ evm::{ approvals::permit2::Permit2, group_swaps::group_swaps, - strategy_encoder::strategy_validators::{ - SequentialSwapValidator, SplitSwapValidator, SwapValidator, + strategy_encoder::{ + strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator}, + transfer_optimizations::TransferOptimization, }, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, utils::{ @@ -69,6 +70,8 @@ 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); @@ -111,12 +114,19 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { + let transfer_type = self.get_transfer_method( + swap.clone(), + solution.given_token.clone(), + self.permit2.clone().is_some(), + ); + let encoding_context = EncodingContext { receiver: self.router_address.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); @@ -199,6 +209,8 @@ pub struct SequentialSwapStrategyEncoder { sequential_swap_validator: SequentialSwapValidator, } +impl TransferOptimization for SequentialSwapStrategyEncoder {} + impl SequentialSwapStrategyEncoder { pub fn new( chain: Chain, @@ -274,12 +286,19 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder { let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { + let transfer_type = self.get_transfer_method( + swap.clone(), + solution.given_token.clone(), + self.permit2.clone().is_some(), + ); + let encoding_context = EncodingContext { receiver: self.router_address.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())?; @@ -415,6 +434,8 @@ impl SplitSwapStrategyEncoder { } } +impl TransferOptimization for SplitSwapStrategyEncoder {} + impl StrategyEncoder for SplitSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { self.split_swap_validator @@ -491,12 +512,19 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { + let transfer_type = self.get_transfer_method( + swap.clone(), + solution.given_token.clone(), + self.permit2.clone().is_some(), + ); + let encoding_context = EncodingContext { receiver: self.router_address.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())?; @@ -722,7 +750,7 @@ mod tests { "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver "00", // zero2one - "00", // transfer type + "02", // transfer type "00000000000000", // padding )); let hex_calldata = encode(&calldata); @@ -825,9 +853,8 @@ mod tests { "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver "00", // zero2one - "00", // exact out - "00", // transfer type - "00000000000000000000000000", // padding + "02", // transfer type + "0000000000000000000000000000", // padding )); let hex_calldata = encode(&calldata); @@ -1484,9 +1511,8 @@ mod tests { "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver "00", // zero2one - "00", // exact out - "00", // transfer type - "00000000000000000000000000", // padding + "01", // transfer type + "0000000000000000000000000000", // padding ] .join(""); @@ -1568,9 +1594,8 @@ mod tests { "a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // receiver "00", // zero2one - "00", // exact out - "00", // transfer type - "000000000000", // padding + "01", // transfer type + "00000000000000", // padding ] .join(""); @@ -1821,7 +1846,7 @@ mod tests { "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", // component id "01", // zero2one - "00", // transfer type + "02", // transfer type "006e", // ple encoded swaps "01", // token in index "00000000", // split @@ -1975,7 +2000,7 @@ mod tests { "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address "88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", // component id "01", // zero2one - "00", // transfer type + "02", // transfer type "006e", // ple encoded swaps "00", // token in index "01", // token out index @@ -1987,7 +2012,7 @@ mod tests { "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address "8ad599c3a0ff1de082011efddc58f1908eb6e6d8", // component id "01", // zero2one - "00", // transfer type + "02", // transfer type "0057", // ple encoded swaps "01", // token in index "00", // token out index @@ -2135,7 +2160,7 @@ mod tests { "b4e16d0168e52d35cacd2c6185b44281ec28c9dc", // component id "3ede3eca2a72b3aecc820e955b36f38437d01395", // router address "01", // zero2one - "00", // transfer type + "02", // transfer type "006e", // ple encoded swaps "01", // token in index "00", // token out index diff --git a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs new file mode 100644 index 0000000..e3692ec --- /dev/null +++ b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs @@ -0,0 +1,95 @@ +use tycho_common::Bytes; + +use crate::encoding::{ + evm::constants::IN_TRANSFER_OPTIMIZABLE_PROTOCOLS, + models::{Swap, TransferType}, +}; + +/// A trait that defines how the tokens will be transferred into the given pool given the solution. +pub trait TransferOptimization { + /// Returns the transfer method that should be used for the given swap and solution. + /// + /// If the swap is for the in token of the solution and the protocol supports transferring + /// straight from the user, it will return `TransferType::Permit2Transfer` or + /// `TransferType::TransferFrom`. + fn get_transfer_method(&self, swap: Swap, given_token: Bytes, permit2: bool) -> TransferType { + let optimize_in_transfer = + IN_TRANSFER_OPTIMIZABLE_PROTOCOLS.contains(&swap.component.protocol_system.as_str()); + if (swap.token_in == given_token) && optimize_in_transfer { + if permit2 { + TransferType::Permit2Transfer + } else { + TransferType::TransferFrom + } + } else { + TransferType::Transfer + } + } +} + +#[cfg(test)] +mod tests { + use alloy_primitives::hex; + use tycho_common::{models::protocol::ProtocolComponent, Bytes}; + + use super::*; + + struct MockStrategy {} + impl TransferOptimization for MockStrategy {} + + fn weth() -> Bytes { + Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) + } + + fn dai() -> Bytes { + Bytes::from(hex!("6b175474e89094c44da98b954eedeac495271d0f").to_vec()) + } + + #[test] + fn test_first_swap_transfer_from_permit2() { + let swap = Swap { + component: ProtocolComponent { + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }; + let strategy = MockStrategy {}; + let transfer_method = strategy.get_transfer_method(swap.clone(), weth(), true); + assert_eq!(transfer_method, TransferType::Permit2Transfer); + } + + #[test] + fn test_first_swap_transfer_from() { + let swap = Swap { + component: ProtocolComponent { + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }; + let strategy = MockStrategy {}; + let transfer_method = strategy.get_transfer_method(swap.clone(), weth(), false); + assert_eq!(transfer_method, TransferType::TransferFrom); + } + + #[test] + fn test_first_swap_transfer() { + let swap = Swap { + component: ProtocolComponent { + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth(), + token_out: dai(), + split: 0f64, + }; + let strategy = MockStrategy {}; + let transfer_method = strategy.get_transfer_method(swap.clone(), dai(), false); + assert_eq!(transfer_method, TransferType::Transfer); + } +} diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index bdc170e..b05199a 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -22,15 +22,6 @@ use crate::encoding::{ swap_encoder::SwapEncoder, }; -#[allow(dead_code)] -#[repr(u8)] -pub enum TransferType { - Transfer = 0, - TransferFrom = 1, - Permit2Transfer = 2, - None = 3, -} - /// Encodes a swap on a Uniswap V2 pool through the given executor address. /// /// # Fields @@ -75,7 +66,7 @@ impl SwapEncoder for UniswapV2SwapEncoder { component_id, bytes_to_address(&encoding_context.receiver)?, zero_to_one, - (TransferType::Transfer as u8).to_be_bytes(), + (encoding_context.transfer_type as u8).to_be_bytes(), ); Ok(args.abi_encode_packed()) @@ -138,7 +129,7 @@ impl SwapEncoder for UniswapV3SwapEncoder { bytes_to_address(&encoding_context.receiver)?, component_id, zero_to_one, - (TransferType::Transfer as u8).to_be_bytes(), + (encoding_context.transfer_type as u8).to_be_bytes(), ); Ok(args.abi_encode_packed()) @@ -573,6 +564,7 @@ mod tests { }; use super::*; + use crate::encoding::models::TransferType; #[test] fn test_encode_uniswap_v2() { @@ -595,6 +587,7 @@ mod tests { router_address: Some(Bytes::zero(20)), group_token_in: token_in.clone(), group_token_out: token_out.clone(), + transfer_type: TransferType::Transfer, }; let encoder = UniswapV2SwapEncoder::new( String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), @@ -648,6 +641,7 @@ mod tests { router_address: Some(Bytes::zero(20)), group_token_in: token_in.clone(), group_token_out: token_out.clone(), + transfer_type: TransferType::Transfer, }; let encoder = UniswapV3SwapEncoder::new( String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), @@ -702,6 +696,7 @@ mod tests { router_address: Some(Bytes::zero(20)), group_token_in: token_in.clone(), group_token_out: token_out.clone(), + transfer_type: TransferType::Transfer, }; let encoder = BalancerV2SwapEncoder::new( String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), @@ -768,6 +763,7 @@ mod tests { group_token_in: token_in.clone(), group_token_out: token_out.clone(), + transfer_type: TransferType::Transfer, }; let encoder = UniswapV4SwapEncoder::new( String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), @@ -834,6 +830,7 @@ mod tests { group_token_in: group_token_in.clone(), // Token out is the same as the group token out group_token_out: token_out.clone(), + transfer_type: TransferType::Transfer, }; let encoder = UniswapV4SwapEncoder::new( @@ -876,6 +873,7 @@ mod tests { router_address: Some(router_address.clone()), group_token_in: usde_address.clone(), group_token_out: wbtc_address.clone(), + transfer_type: TransferType::Transfer, }; // Setup - First sequence: USDE -> USDT @@ -1004,6 +1002,7 @@ mod tests { group_token_out: token_out.clone(), exact_out: false, router_address: Some(Bytes::default()), + transfer_type: TransferType::Transfer, }; let encoder = @@ -1046,6 +1045,7 @@ mod tests { group_token_out: group_token_out.clone(), exact_out: false, router_address: Some(Bytes::default()), + transfer_type: TransferType::Transfer, }; let first_swap = Swap { diff --git a/src/encoding/evm/tycho_encoders.rs b/src/encoding/evm/tycho_encoders.rs index 92706fc..6ec4e36 100644 --- a/src/encoding/evm/tycho_encoders.rs +++ b/src/encoding/evm/tycho_encoders.rs @@ -12,7 +12,7 @@ use crate::encoding::{ }, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, }, - models::{Chain, EncodingContext, NativeAction, Solution, Transaction}, + models::{Chain, EncodingContext, NativeAction, Solution, Transaction, TransferType}, strategy_encoder::StrategyEncoder, tycho_encoder::TychoEncoder, }; @@ -201,9 +201,10 @@ impl TychoEncoder for TychoRouterEncoder { } } -/// Represents an encoder for one swap to be executed directly against an Executor. This is useful -/// when you want to bypass the Tycho Router, use your own Router contract and just need the -/// calldata for a particular swap. +/// Represents an encoder for one swap to be executed directly against an Executor. +/// +/// This is useful when you want to bypass the Tycho Router, use your own Router contract and +/// just need the calldata for a particular swap. /// /// # Fields /// * `swap_encoder_registry`: Registry of swap encoders @@ -259,6 +260,7 @@ impl TychoExecutorEncoder { router_address: None, group_token_in: grouped_swap.input_token.clone(), group_token_out: grouped_swap.output_token.clone(), + transfer_type: TransferType::Transfer, }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; grouped_protocol_data.extend(protocol_data); diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 9da64dd..7f33009 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -96,6 +96,23 @@ pub struct Transaction { pub data: Vec, } +/// Represents the type of transfer to be performed into the pool. +/// +/// # Fields +/// +/// * `Transfer`: Transfer the token from the router into the pool. +/// * `TransferFrom`: Transfer the token from the swapper to the pool. +/// * `Permit2Transfer`: Transfer the token from the sender to the pool using Permit2. +/// * `None`: No transfer is needed. Tokens are already in the pool. +#[repr(u8)] +#[derive(Clone, Debug, PartialEq)] +pub enum TransferType { + Transfer = 0, + TransferFrom = 1, + Permit2Transfer = 2, + None = 3, +} + /// Represents necessary attributes for encoding an order. /// /// # Fields @@ -113,6 +130,7 @@ pub struct EncodingContext { pub router_address: Option, pub group_token_in: Bytes, pub group_token_out: Bytes, + pub transfer_type: TransferType, } #[derive(Clone, PartialEq, Eq, Hash)]