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/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/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 7dd7afd..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}, @@ -32,17 +31,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 { @@ -51,6 +48,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()) @@ -60,13 +58,19 @@ 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, + 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, + ), }) } @@ -80,8 +84,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); @@ -125,26 +127,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.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 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); } @@ -215,6 +211,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,16 +221,16 @@ 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, 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()) @@ -244,14 +241,22 @@ impl SequentialSwapStrategyEncoder { .to_string(), ) }; + let permit2_is_active = permit2.is_some(); Ok(Self { 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, + transfer_optimization: TransferOptimization::new( + chain.native_token()?, + chain.wrapped_token()?, + permit2_is_active, + token_in_already_in_router, + router_address, + ), }) } @@ -291,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 @@ -303,50 +308,31 @@ 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_allowed, + ); + 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.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 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); @@ -422,6 +408,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 +418,7 @@ pub struct SplitSwapStrategyEncoder { wrapped_address: Bytes, split_swap_validator: SplitSwapValidator, router_address: Bytes, + transfer_optimization: TransferOptimization, } impl SplitSwapStrategyEncoder { @@ -439,6 +427,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()) @@ -449,7 +438,7 @@ impl SplitSwapStrategyEncoder { .to_string(), ) }; - + let permit2_is_active = permit2.is_some(); Ok(Self { permit2, selector, @@ -457,7 +446,14 @@ 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, + ), }) } @@ -481,8 +477,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 @@ -513,7 +507,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 @@ -558,40 +552,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.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 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()) @@ -744,6 +731,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -827,6 +815,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -876,6 +865,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 @@ -904,6 +972,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -956,6 +1025,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -1027,6 +1097,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1086,6 +1157,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1204,6 +1276,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -1321,6 +1394,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1402,6 +1476,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1487,6 +1562,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1553,6 +1629,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); let solution = Solution { @@ -1652,6 +1729,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -1761,6 +1839,7 @@ mod tests { swap_encoder_registry, Some(private_key.clone()), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -1923,6 +2002,7 @@ mod tests { swap_encoder_registry, Some(private_key.clone()), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -2048,6 +2128,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0xA4AD4f68d0b91CFD19687c881e50f3A00242828c").unwrap(), + false, ) .unwrap(); @@ -2113,6 +2194,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -2181,6 +2263,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); @@ -2268,6 +2351,7 @@ mod tests { swap_encoder_registry, Some(private_key), Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"), + false, ) .unwrap(); let solution = Solution { @@ -2370,6 +2454,7 @@ mod tests { swap_encoder_registry, None, Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + false, ) .unwrap(); @@ -2432,6 +2517,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 97e08fc..62bfca1 100644 --- a/src/encoding/evm/strategy_encoder/transfer_optimizations.rs +++ b/src/encoding/evm/strategy_encoder/transfer_optimizations.rs @@ -1,50 +1,86 @@ +use std::str::FromStr; + use tycho_common::Bytes; use crate::encoding::{ - evm::constants::IN_TRANSFER_REQUIRED_PROTOCOLS, - models::{Swap, TransferType}, + errors::EncodingError, + evm::{ + constants::{CALLBACK_CONSTRAINED_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS}, + group_swaps::SwapGroup, + }, + models::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. - #[allow(clippy::too_many_arguments)] - fn get_transfer_type( - &self, - swap: Swap, - given_token: Bytes, +/// 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, + token_in_already_in_router: bool, + router_address: Bytes, +} + +impl TransferOptimization { + pub fn new( native_token: Bytes, 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, + router_address, + } + } + + /// Returns the transfer method that should be used for the given swap and solution. + pub fn get_transfer_type( + &self, + 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; - 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.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 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 { @@ -54,17 +90,50 @@ pub trait 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 tycho_common::{models::protocol::ProtocolComponent, Bytes}; + use rstest::rstest; + use tycho_common::models::protocol::ProtocolComponent; use super::*; - - struct MockStrategy {} - impl TransferOptimization for MockStrategy {} + use crate::encoding::models::Swap; fn weth() -> Bytes { Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()) @@ -82,39 +151,37 @@ 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 - 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 strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), true, false, 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); } #[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 strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, 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); } @@ -122,18 +189,15 @@ 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 strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), eth(), eth(), weth(), false, 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); } @@ -141,18 +205,15 @@ 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 strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), eth(), eth(), weth(), false, true, 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); } @@ -160,18 +221,15 @@ 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 strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, 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); } @@ -179,18 +237,15 @@ 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 strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, 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); } @@ -198,18 +253,100 @@ 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 strategy = MockStrategy {}; - let transfer_method = - strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, true); + 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); } + + #[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, router_address()); + 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, 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); + } } 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() }