From 789416b2cd8f3cd9844bf2848768ea5bd7f3f6e3 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 18 Feb 2025 20:58:00 +0530 Subject: [PATCH 01/12] feat: add usv4 swap encoder with single swap test --- .../evm/swap_encoder/swap_encoders.rs | 200 +++++++++++++++++- 1 file changed, 199 insertions(+), 1 deletion(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index f3d0fb0..8754a11 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use alloy_primitives::{Address, Bytes as AlloyBytes}; +use alloy_primitives::{keccak256, Address, Bytes as AlloyBytes}; use alloy_sol_types::SolValue; use crate::encoding::{ @@ -147,6 +147,140 @@ impl SwapEncoder for UniswapV3SwapEncoder { } } +/// Encodes a swap on a Uniswap V4 pool through the given executor address. +/// +/// # Fields +/// * `executor_address` - The address of the executor contract that will perform the swap. +/// * `executor_selector` - The selector of the swap function in the executor contract. +/// * `callback_selector` - The pre-computed selector of the callback function in the executor +/// contract. +#[derive(Clone)] +pub struct UniswapV4SwapEncoder { + executor_address: String, + executor_selector: String, + callback_selector: [u8; 4], +} + +impl UniswapV4SwapEncoder { + fn get_zero_to_one(sell_token_address: Address, buy_token_address: Address) -> bool { + sell_token_address < buy_token_address + } + + fn encode_pool_params( + intermediary_token: Address, + fee: [u8; 3], + tick_spacing: [u8; 3], + ) -> Vec { + let mut encoded = Vec::with_capacity(26); + // Encode intermediary token (20 bytes) + encoded.extend_from_slice(intermediary_token.as_ref()); + // Encode fee (3 bytes) + encoded.extend_from_slice(fee.as_ref()); + // Encode tick spacing (3 bytes) + encoded.extend_from_slice(tick_spacing.as_ref()); + encoded + } +} + +impl SwapEncoder for UniswapV4SwapEncoder { + fn new(executor_address: String) -> Self { + // Pre-compute the callback selector for "unlockCallback(bytes)" + // This matches how Solidity computes function selectors + let callback_selector = keccak256(b"unlockCallback(bytes)")[..4] + .try_into() + .unwrap(); + + Self { + executor_address, + executor_selector: "swap(uint256,bytes)".to_string(), + callback_selector, + } + } + + fn encode_swap( + &self, + swap: Swap, + encoding_context: EncodingContext, + ) -> Result, EncodingError> { + let token_in_address = bytes_to_address(&swap.token_in)?; + let token_out_address = bytes_to_address(&swap.token_out)?; + let mut amount_out_min = [0u8; 32]; + amount_out_min[31] = 1; + let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address); + let callback_executor = bytes_to_address(&encoding_context.router_address)?; + + let fee = swap + .component + .static_attributes + .get("fee") + .ok_or_else(|| { + EncodingError::FatalError( + "Pool fee not found in Uniswap v4 static attributes".to_string(), + ) + })? + .to_vec(); + + // Pad on the left with zeros if the fee is less than 3 bytes + let mut padded_fee_bytes = [0u8; 3]; + let start = 3 - fee.len(); + padded_fee_bytes[start..].copy_from_slice(&fee); + + let pool_fee_u24: [u8; 3] = padded_fee_bytes[(padded_fee_bytes.len() - 3)..] + .try_into() + .map_err(|_| EncodingError::FatalError("Failed to extract fee bytes".to_string()))?; + + let tick_spacing = swap + .component + .static_attributes + .get("tickSpacing") + .ok_or_else(|| { + EncodingError::FatalError( + "Pool tick spacing not found in Uniswap v4 static attributes".to_string(), + ) + })? + .to_vec(); + + // Pad on the left with zeros if the tick spacing is less than 3 bytes + let mut padded_tick_spacing_bytes = [0u8; 3]; + let start = 3 - tick_spacing.len(); + padded_tick_spacing_bytes[start..].copy_from_slice(&tick_spacing); + + let pool_tick_spacing_u24: [u8; 3] = padded_tick_spacing_bytes + [(padded_tick_spacing_bytes.len() - 3)..] + .try_into() + .map_err(|_| { + EncodingError::FatalError("Failed to extract tick spacing bytes".to_string()) + })?; + + let pool_params = + Self::encode_pool_params(token_out_address, pool_fee_u24, pool_tick_spacing_u24); + + let args = ( + token_in_address, + token_out_address, + amount_out_min, + zero_to_one, + callback_executor, + self.callback_selector, + pool_params, + ); + + Ok(args.abi_encode_packed()) + } + + fn executor_address(&self) -> &str { + &self.executor_address + } + + fn executor_selector(&self) -> &str { + &self.executor_selector + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + /// Encodes a swap on a Balancer V2 pool through the given executor address. /// /// # Fields @@ -344,4 +478,68 @@ mod tests { )) ); } + + #[test] + fn test_encode_uniswap_v4() { + let fee = BigInt::from(100); + let tick_spacing = BigInt::from(1); + let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); + let encoded_tick_spacing = Bytes::from(tick_spacing.to_signed_bytes_be()); + + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec())); + static_attributes.insert("tickSpacing".into(), Bytes::from(encoded_tick_spacing.to_vec())); + + let usv4_pool = ProtocolComponent { + // Pool manager + id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), + static_attributes, + ..Default::default() + }; + let swap = Swap { + component: usv4_pool, + token_in: Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"), // USDE + token_out: Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"), // USDT + split: 0f64, + }; + let encoding_context = EncodingContext { + // The receiver address was taken from `address(uniswapV4Exposed)` in the + // UniswapV4Executor.t.sol + receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), + exact_out: false, + // Same as the executor address + router_address: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), + }; + let encoder = + UniswapV4SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + assert_eq!( + hex_swap, + String::from(concat!( + // token in + "4c9edd5852cd905f086c759e8383e09bff1e68b3", + // token out + "dac17f958d2ee523a2206206994597c13d831ec7", + // amount out min (0 as u128) + "0000000000000000000000000000000000000000000000000000000000000001", + // zero for one + "01", + // router address + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", + // callback selector for "unlockCallback(bytes)" + "91dd7346", + // pool params: + // - intermediary token (20 bytes) + "dac17f958d2ee523a2206206994597c13d831ec7", + // - fee (3 bytes) + "000064", + // - tick spacing (3 bytes) + "000001" + )) + ); + } } From 529456f40cee3e555090d7defdf50c9c341e6c50 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 18 Feb 2025 21:24:49 +0530 Subject: [PATCH 02/12] feat: add single swap integration test for usv4 executor --- .../test/executors/UniswapV4Executor.t.sol | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index e35b676..61a10ce 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -133,6 +133,27 @@ contract UniswapV4ExecutorTest is Test, Constants { assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); } + function testSingleSwapIntegration() public { + // USDE -> USDT + // Generated by the Tycho swap encoder - test_encode_uniswap_v4 + bytes memory protocolData = + hex"4c9edd5852cd905f086c759e8383e09bff1e68b3dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000000000001015615deb798bb3e4dfa0139dfa1b3d433cc23b72f91dd7346dac17f958d2ee523a2206206994597c13d831ec7000064000001"; + + uint256 amountIn = 100 ether; + deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); + uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); + uint256 usdeBalanceBeforeSwapExecutor = + USDE.balanceOf(address(uniswapV4Exposed)); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData); + assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); + assertEq( + USDE.balanceOf(address(uniswapV4Exposed)), + usdeBalanceBeforeSwapExecutor - amountIn + ); + assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); + } + function testMultipleSwap() public { // USDE -> USDT -> WBTC uint256 amountIn = 100 ether; From 1bfe656e6b1279482116be5ffddd81f83c0c381e Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 19 Feb 2025 08:39:21 +0530 Subject: [PATCH 03/12] feat: add new attributes in encoding context, update usv4 swap encoder and tests --- .../test/executors/UniswapV4Executor.t.sol | 2 +- .../evm/strategy_encoder/strategy_encoders.rs | 8 ++ .../evm/swap_encoder/swap_encoders.rs | 102 ++++++++++++++++-- src/encoding/models.rs | 6 ++ 4 files changed, 111 insertions(+), 7 deletions(-) diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 61a10ce..b1c8169 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -135,7 +135,7 @@ contract UniswapV4ExecutorTest is Test, Constants { function testSingleSwapIntegration() public { // USDE -> USDT - // Generated by the Tycho swap encoder - test_encode_uniswap_v4 + // Generated by the Tycho swap encoder - test_encode_uniswap_v4_simple_swap bytes memory protocolData = hex"4c9edd5852cd905f086c759e8383e09bff1e68b3dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000000000001015615deb798bb3e4dfa0139dfa1b3d433cc23b72f91dd7346dac17f958d2ee523a2206206994597c13d831ec7000064000001"; diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index efb79e4..de59a54 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -2,6 +2,8 @@ use std::{collections::HashSet, str::FromStr}; use alloy_primitives::{aliases::U24, FixedBytes, U256, U8}; use alloy_sol_types::SolValue; +use num_bigint::BigUint; +use num_traits::Zero; use tycho_core::{keccak256, Bytes}; use crate::encoding::{ @@ -257,6 +259,9 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { receiver: solution.router_address.clone(), exact_out: solution.exact_out, router_address: solution.router_address.clone(), + group_token_in: Some(tokens.first().unwrap().clone()), + group_token_out: Some(tokens.last().unwrap().clone()), + amount_out_min: Some(BigUint::zero()), }; let mut grouped_protocol_data: Vec> = vec![]; for swap in grouped_swap.swaps.iter() { @@ -347,6 +352,9 @@ impl StrategyEncoder for ExecutorStrategyEncoder { receiver: solution.receiver, exact_out: solution.exact_out, router_address: solution.router_address, + group_token_in: Some(solution.given_token.clone()), + group_token_out: Some(solution.checked_token.clone()), + amount_out_min: Some(BigUint::zero()), }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context)?; diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 8754a11..d207804 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -202,10 +202,21 @@ impl SwapEncoder for UniswapV4SwapEncoder { swap: Swap, encoding_context: EncodingContext, ) -> Result, EncodingError> { + let mut first_swap = false; + if encoding_context.group_token_in == Some(swap.token_in.clone()) && + encoding_context.group_token_out == Some(swap.token_out.clone()) + { + first_swap = true; + } let token_in_address = bytes_to_address(&swap.token_in)?; let token_out_address = bytes_to_address(&swap.token_out)?; - let mut amount_out_min = [0u8; 32]; - amount_out_min[31] = 1; + let mut amount_out_min = vec![0u8; 32]; // Create a zero-filled buffer of 32 bytes + let min_value = encoding_context + .amount_out_min + .unwrap_or_default() + .to_bytes_be(); + // Copy the actual value to the end of the buffer, maintaining leading zeros + amount_out_min[(32 - min_value.len())..].copy_from_slice(&min_value); let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address); let callback_executor = bytes_to_address(&encoding_context.router_address)?; @@ -255,6 +266,14 @@ impl SwapEncoder for UniswapV4SwapEncoder { let pool_params = Self::encode_pool_params(token_out_address, pool_fee_u24, pool_tick_spacing_u24); + if !first_swap { + return Ok(Self::encode_pool_params( + token_in_address, + pool_fee_u24, + pool_tick_spacing_u24, + )); + } + let args = ( token_in_address, token_out_address, @@ -345,7 +364,7 @@ mod tests { use std::collections::HashMap; use alloy::hex::encode; - use num_bigint::BigInt; + use num_bigint::{BigInt, BigUint}; use tycho_core::{dto::ProtocolComponent, Bytes}; use super::*; @@ -366,6 +385,9 @@ mod tests { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, router_address: Bytes::zero(20), + group_token_in: None, + group_token_out: None, + amount_out_min: None, }; let encoder = UniswapV2SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); @@ -409,6 +431,9 @@ mod tests { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, router_address: Bytes::zero(20), + group_token_in: None, + group_token_out: None, + amount_out_min: None, }; let encoder = UniswapV3SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); @@ -453,6 +478,9 @@ mod tests { receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), exact_out: false, router_address: Bytes::zero(20), + group_token_in: None, + group_token_out: None, + amount_out_min: None, }; let encoder = BalancerV2SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); @@ -480,11 +508,13 @@ mod tests { } #[test] - fn test_encode_uniswap_v4() { + fn test_encode_uniswap_v4_simple_swap() { let fee = BigInt::from(100); let tick_spacing = BigInt::from(1); let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); let encoded_tick_spacing = Bytes::from(tick_spacing.to_signed_bytes_be()); + let token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE + let token_out = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT let mut static_attributes: HashMap = HashMap::new(); static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec())); @@ -498,8 +528,8 @@ mod tests { }; let swap = Swap { component: usv4_pool, - token_in: Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"), // USDE - token_out: Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"), // USDT + token_in: token_in.clone(), + token_out: token_out.clone(), split: 0f64, }; let encoding_context = EncodingContext { @@ -509,6 +539,9 @@ mod tests { exact_out: false, // Same as the executor address router_address: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), + group_token_in: Some(token_in), + group_token_out: Some(token_out), + amount_out_min: Some(BigUint::from(1u128)), }; let encoder = UniswapV4SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); @@ -542,4 +575,61 @@ mod tests { )) ); } + + #[test] + fn test_encode_uniswap_v4_grouped() { + let fee = BigInt::from(500); + let tick_spacing = BigInt::from(10); + let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); + let encoded_tick_spacing = Bytes::from(tick_spacing.to_signed_bytes_be()); + let token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE + let token_out = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT + + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec())); + static_attributes.insert("tickSpacing".into(), Bytes::from(encoded_tick_spacing.to_vec())); + + let usv4_pool = ProtocolComponent { + id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), + static_attributes, + ..Default::default() + }; + + let swap = Swap { + component: usv4_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let encoding_context = EncodingContext { + receiver: Bytes::from("0x0000000000000000000000000000000000000001"), + exact_out: false, + router_address: Bytes::zero(20), + // Different from token_in and token_out + group_token_in: Some(Bytes::zero(20)), + group_token_out: Some(Bytes::zero(20)), + amount_out_min: Some(BigUint::from(1u128)), + }; + + let encoder = + UniswapV4SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + assert_eq!( + hex_swap, + String::from(concat!( + // pool params: + // - intermediary token (20 bytes) + "4c9edd5852cd905f086c759e8383e09bff1e68b3", + // - fee (3 bytes) + "0001f4", + // - tick spacing (3 bytes) + "00000a" + )) + ); + } } diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 78a4a3f..543b77a 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -111,11 +111,17 @@ pub struct Transaction { /// * `receiver`: Address of the receiver of the out token after the swaps are completed. /// * `exact_out`: true if the solution is a buy order, false if it is a sell order. /// * `router_address`: Address of the router contract to be used for the swaps. +/// * `group_token_in`: Token to be used as the input for the group swap. +/// * `group_token_out`: Token to be used as the output for the group swap. +/// * `amount_out_min`: Minimum amount of the output token to be received. #[derive(Clone, Debug)] pub struct EncodingContext { pub receiver: Bytes, pub exact_out: bool, pub router_address: Bytes, + pub group_token_in: Option, + pub group_token_out: Option, + pub amount_out_min: Option, } #[derive(Clone, PartialEq, Eq, Hash)] From 9219dd329d5d38b8c51b40aa5664e5120c3b6dcd Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 19 Feb 2025 18:56:32 +0530 Subject: [PATCH 04/12] feat: add util fns, change callback_selector to string, update first_swap check --- .../evm/swap_encoder/swap_encoders.rs | 70 +++++-------------- src/encoding/evm/utils.rs | 16 ++++- 2 files changed, 32 insertions(+), 54 deletions(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index d207804..69b037a 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -1,12 +1,13 @@ use std::str::FromStr; -use alloy_primitives::{keccak256, Address, Bytes as AlloyBytes}; +use alloy_primitives::{Address, Bytes as AlloyBytes}; use alloy_sol_types::SolValue; use crate::encoding::{ errors::EncodingError, evm::{ - approvals::protocol_approvals_manager::ProtocolApprovalsManager, utils::bytes_to_address, + approvals::protocol_approvals_manager::ProtocolApprovalsManager, + utils::{bytes_to_address, encode_function_selector, pad_to_fixed_size}, }, models::{EncodingContext, Swap}, swap_encoder::SwapEncoder, @@ -115,13 +116,7 @@ impl SwapEncoder for UniswapV3SwapEncoder { })? .to_vec(); - // this is necessary to pad on the left with zeros if the fee is less than 3 bytes - let mut padded_fee_bytes = [0u8; 3]; - let start = 3 - pool_fee_bytes.len(); - padded_fee_bytes[start..].copy_from_slice(&pool_fee_bytes); - - let pool_fee_u24: [u8; 3] = padded_fee_bytes[(padded_fee_bytes.len() - 3)..] - .try_into() + let pool_fee_u24 = pad_to_fixed_size::<3>(&pool_fee_bytes) .map_err(|_| EncodingError::FatalError("Failed to extract fee bytes".to_string()))?; let args = ( @@ -152,13 +147,12 @@ impl SwapEncoder for UniswapV3SwapEncoder { /// # Fields /// * `executor_address` - The address of the executor contract that will perform the swap. /// * `executor_selector` - The selector of the swap function in the executor contract. -/// * `callback_selector` - The pre-computed selector of the callback function in the executor -/// contract. +/// * `callback_selector` - The selector of the callback function in the executor contract. #[derive(Clone)] pub struct UniswapV4SwapEncoder { executor_address: String, executor_selector: String, - callback_selector: [u8; 4], + callback_selector: String, } impl UniswapV4SwapEncoder { @@ -171,29 +165,16 @@ impl UniswapV4SwapEncoder { fee: [u8; 3], tick_spacing: [u8; 3], ) -> Vec { - let mut encoded = Vec::with_capacity(26); - // Encode intermediary token (20 bytes) - encoded.extend_from_slice(intermediary_token.as_ref()); - // Encode fee (3 bytes) - encoded.extend_from_slice(fee.as_ref()); - // Encode tick spacing (3 bytes) - encoded.extend_from_slice(tick_spacing.as_ref()); - encoded + (intermediary_token, fee, tick_spacing).abi_encode_packed() } } impl SwapEncoder for UniswapV4SwapEncoder { fn new(executor_address: String) -> Self { - // Pre-compute the callback selector for "unlockCallback(bytes)" - // This matches how Solidity computes function selectors - let callback_selector = keccak256(b"unlockCallback(bytes)")[..4] - .try_into() - .unwrap(); - Self { executor_address, executor_selector: "swap(uint256,bytes)".to_string(), - callback_selector, + callback_selector: "unlockCallback(bytes)".to_string(), } } @@ -203,19 +184,16 @@ impl SwapEncoder for UniswapV4SwapEncoder { encoding_context: EncodingContext, ) -> Result, EncodingError> { let mut first_swap = false; - if encoding_context.group_token_in == Some(swap.token_in.clone()) && - encoding_context.group_token_out == Some(swap.token_out.clone()) - { + if encoding_context.group_token_in == Some(swap.token_in.clone()) { first_swap = true; } let token_in_address = bytes_to_address(&swap.token_in)?; let token_out_address = bytes_to_address(&swap.token_out)?; - let mut amount_out_min = vec![0u8; 32]; // Create a zero-filled buffer of 32 bytes + let mut amount_out_min = vec![0u8; 32]; // Zero-filled buffer of 32 bytes let min_value = encoding_context .amount_out_min .unwrap_or_default() .to_bytes_be(); - // Copy the actual value to the end of the buffer, maintaining leading zeros amount_out_min[(32 - min_value.len())..].copy_from_slice(&min_value); let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address); let callback_executor = bytes_to_address(&encoding_context.router_address)?; @@ -231,13 +209,7 @@ impl SwapEncoder for UniswapV4SwapEncoder { })? .to_vec(); - // Pad on the left with zeros if the fee is less than 3 bytes - let mut padded_fee_bytes = [0u8; 3]; - let start = 3 - fee.len(); - padded_fee_bytes[start..].copy_from_slice(&fee); - - let pool_fee_u24: [u8; 3] = padded_fee_bytes[(padded_fee_bytes.len() - 3)..] - .try_into() + let pool_fee_u24 = pad_to_fixed_size::<3>(&fee) .map_err(|_| EncodingError::FatalError("Failed to extract fee bytes".to_string()))?; let tick_spacing = swap @@ -251,24 +223,16 @@ impl SwapEncoder for UniswapV4SwapEncoder { })? .to_vec(); - // Pad on the left with zeros if the tick spacing is less than 3 bytes - let mut padded_tick_spacing_bytes = [0u8; 3]; - let start = 3 - tick_spacing.len(); - padded_tick_spacing_bytes[start..].copy_from_slice(&tick_spacing); - - let pool_tick_spacing_u24: [u8; 3] = padded_tick_spacing_bytes - [(padded_tick_spacing_bytes.len() - 3)..] - .try_into() - .map_err(|_| { - EncodingError::FatalError("Failed to extract tick spacing bytes".to_string()) - })?; + let pool_tick_spacing_u24 = pad_to_fixed_size::<3>(&tick_spacing).map_err(|_| { + EncodingError::FatalError("Failed to extract tick spacing bytes".to_string()) + })?; let pool_params = Self::encode_pool_params(token_out_address, pool_fee_u24, pool_tick_spacing_u24); if !first_swap { return Ok(Self::encode_pool_params( - token_in_address, + token_out_address, pool_fee_u24, pool_tick_spacing_u24, )); @@ -280,7 +244,7 @@ impl SwapEncoder for UniswapV4SwapEncoder { amount_out_min, zero_to_one, callback_executor, - self.callback_selector, + encode_function_selector(&self.callback_selector), pool_params, ); @@ -624,7 +588,7 @@ mod tests { String::from(concat!( // pool params: // - intermediary token (20 bytes) - "4c9edd5852cd905f086c759e8383e09bff1e68b3", + "dac17f958d2ee523a2206206994597c13d831ec7", // - fee (3 bytes) "0001f4", // - tick spacing (3 bytes) diff --git a/src/encoding/evm/utils.rs b/src/encoding/evm/utils.rs index d4017f6..6f15d4e 100644 --- a/src/encoding/evm/utils.rs +++ b/src/encoding/evm/utils.rs @@ -1,6 +1,6 @@ use std::cmp::max; -use alloy_primitives::{aliases::U24, Address, Keccak256, U256, U8}; +use alloy_primitives::{aliases::U24, keccak256, Address, FixedBytes, Keccak256, U256, U8}; use num_bigint::BigUint; use tycho_core::Bytes; @@ -91,3 +91,17 @@ pub fn get_token_position(tokens: Vec, token: Bytes) -> Result(input: &[u8]) -> Result<[u8; N], EncodingError> { + let mut padded = [0u8; N]; + let start = N - input.len(); + padded[start..].copy_from_slice(input); + Ok(padded) +} + +/// Encodes a function selector to a fixed size array of 4 bytes. +pub fn encode_function_selector(selector: &str) -> FixedBytes<4> { + let hash = keccak256(selector.as_bytes()); + FixedBytes::<4>::from([hash[0], hash[1], hash[2], hash[3]]) +} From 5b183b66900c6408345a670660a11ac80f64ce7e Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 19 Feb 2025 19:01:32 +0530 Subject: [PATCH 05/12] chore: update executor_selector to swap_selector in swap encoder trait and attribute --- .../evm/strategy_encoder/strategy_encoders.rs | 12 +----- .../evm/swap_encoder/swap_encoders.rs | 40 +++++++++---------- src/encoding/swap_encoder.rs | 2 +- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index de59a54..c9792b2 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -277,7 +277,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { EncodingError::FatalError("Invalid executor address".to_string()) })?, - self.encode_executor_selector(swap_encoder.executor_selector()), + self.encode_executor_selector(swap_encoder.swap_selector()), grouped_protocol_data.abi_encode_packed(), ); swaps.push(swap_data); @@ -360,15 +360,7 @@ impl StrategyEncoder for ExecutorStrategyEncoder { let executor_address = Bytes::from_str(swap_encoder.executor_address()) .map_err(|_| EncodingError::FatalError("Invalid executor address".to_string()))?; - Ok(( - protocol_data, - executor_address, - Some( - swap_encoder - .executor_selector() - .to_string(), - ), - )) + Ok((protocol_data, executor_address, Some(swap_encoder.swap_selector().to_string()))) } fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box> { diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 69b037a..69ae085 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -17,11 +17,11 @@ use crate::encoding::{ /// /// # Fields /// * `executor_address` - The address of the executor contract that will perform the swap. -/// * `executor_selector` - The selector of the swap function in the executor contract. +/// * `swap_selector` - The selector of the swap function in the executor contract. #[derive(Clone)] pub struct UniswapV2SwapEncoder { executor_address: String, - executor_selector: String, + swap_selector: String, } impl UniswapV2SwapEncoder { @@ -32,7 +32,7 @@ impl UniswapV2SwapEncoder { impl SwapEncoder for UniswapV2SwapEncoder { fn new(executor_address: String) -> Self { - Self { executor_address, executor_selector: "swap(uint256,bytes)".to_string() } + Self { executor_address, swap_selector: "swap(uint256,bytes)".to_string() } } fn encode_swap( @@ -63,8 +63,8 @@ impl SwapEncoder for UniswapV2SwapEncoder { &self.executor_address } - fn executor_selector(&self) -> &str { - &self.executor_selector + fn swap_selector(&self) -> &str { + &self.swap_selector } fn clone_box(&self) -> Box { @@ -76,11 +76,11 @@ impl SwapEncoder for UniswapV2SwapEncoder { /// /// # Fields /// * `executor_address` - The address of the executor contract that will perform the swap. -/// * `executor_selector` - The selector of the swap function in the executor contract. +/// * `swap_selector` - The selector of the swap function in the executor contract. #[derive(Clone)] pub struct UniswapV3SwapEncoder { executor_address: String, - executor_selector: String, + swap_selector: String, } impl UniswapV3SwapEncoder { @@ -91,7 +91,7 @@ impl UniswapV3SwapEncoder { impl SwapEncoder for UniswapV3SwapEncoder { fn new(executor_address: String) -> Self { - Self { executor_address, executor_selector: "swap(uint256,bytes)".to_string() } + Self { executor_address, swap_selector: "swap(uint256,bytes)".to_string() } } fn encode_swap( @@ -134,8 +134,8 @@ impl SwapEncoder for UniswapV3SwapEncoder { fn executor_address(&self) -> &str { &self.executor_address } - fn executor_selector(&self) -> &str { - &self.executor_selector + fn swap_selector(&self) -> &str { + &self.swap_selector } fn clone_box(&self) -> Box { Box::new(self.clone()) @@ -146,12 +146,12 @@ impl SwapEncoder for UniswapV3SwapEncoder { /// /// # Fields /// * `executor_address` - The address of the executor contract that will perform the swap. -/// * `executor_selector` - The selector of the swap function in the executor contract. +/// * `swap_selector` - The selector of the swap function in the executor contract. /// * `callback_selector` - The selector of the callback function in the executor contract. #[derive(Clone)] pub struct UniswapV4SwapEncoder { executor_address: String, - executor_selector: String, + swap_selector: String, callback_selector: String, } @@ -173,7 +173,7 @@ impl SwapEncoder for UniswapV4SwapEncoder { fn new(executor_address: String) -> Self { Self { executor_address, - executor_selector: "swap(uint256,bytes)".to_string(), + swap_selector: "swap(uint256,bytes)".to_string(), callback_selector: "unlockCallback(bytes)".to_string(), } } @@ -255,8 +255,8 @@ impl SwapEncoder for UniswapV4SwapEncoder { &self.executor_address } - fn executor_selector(&self) -> &str { - &self.executor_selector + fn swap_selector(&self) -> &str { + &self.swap_selector } fn clone_box(&self) -> Box { @@ -268,11 +268,11 @@ impl SwapEncoder for UniswapV4SwapEncoder { /// /// # Fields /// * `executor_address` - The address of the executor contract that will perform the swap. -/// * `executor_selector` - The selector of the swap function in the executor contract. +/// * `swap_selector` - The selector of the swap function in the executor contract. #[derive(Clone)] pub struct BalancerV2SwapEncoder { executor_address: String, - executor_selector: String, + swap_selector: String, vault_address: String, } @@ -280,7 +280,7 @@ impl SwapEncoder for BalancerV2SwapEncoder { fn new(executor_address: String) -> Self { Self { executor_address, - executor_selector: "swap(uint256,bytes)".to_string(), + swap_selector: "swap(uint256,bytes)".to_string(), vault_address: "0xba12222222228d8ba445958a75a0704d566bf2c8".to_string(), } } @@ -315,8 +315,8 @@ impl SwapEncoder for BalancerV2SwapEncoder { fn executor_address(&self) -> &str { &self.executor_address } - fn executor_selector(&self) -> &str { - &self.executor_selector + fn swap_selector(&self) -> &str { + &self.swap_selector } fn clone_box(&self) -> Box { Box::new(self.clone()) diff --git a/src/encoding/swap_encoder.rs b/src/encoding/swap_encoder.rs index a0a9d0f..6061fb2 100644 --- a/src/encoding/swap_encoder.rs +++ b/src/encoding/swap_encoder.rs @@ -20,7 +20,7 @@ pub trait SwapEncoder: Sync + Send { fn executor_address(&self) -> &str; /// The selector of the executor function that will be called in order to perform a swap. - fn executor_selector(&self) -> &str; + fn swap_selector(&self) -> &str; /// Clones the swap encoder as a trait object. /// This allows the encoder to be cloned when it is being used as a `Box`. From baeebb9fe4d1df4db06aa3ce9606a59f6b12552d Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 19 Feb 2025 19:08:06 +0530 Subject: [PATCH 06/12] feat: update test_encode_uniswap_v4_grouped --- src/encoding/evm/swap_encoder/swap_encoders.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 69ae085..d391349 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -542,12 +542,12 @@ mod tests { #[test] fn test_encode_uniswap_v4_grouped() { - let fee = BigInt::from(500); - let tick_spacing = BigInt::from(10); + let fee = BigInt::from(3000); + let tick_spacing = BigInt::from(60); let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); let encoded_tick_spacing = Bytes::from(tick_spacing.to_signed_bytes_be()); - let token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE - let token_out = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT + let token_in = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT + let token_out = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); // WBTC let mut static_attributes: HashMap = HashMap::new(); static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec())); @@ -588,11 +588,11 @@ mod tests { String::from(concat!( // pool params: // - intermediary token (20 bytes) - "dac17f958d2ee523a2206206994597c13d831ec7", + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", // - fee (3 bytes) - "0001f4", + "000bb8", // - tick spacing (3 bytes) - "00000a" + "00003c" )) ); } From 5e9b38876e974aa9f2e62ebf754237a57fca28eb Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 19 Feb 2025 20:32:55 +0530 Subject: [PATCH 07/12] feat: add integration test for complex swaps --- .../test/executors/UniswapV4Executor.t.sol | 23 +++ .../evm/swap_encoder/swap_encoders.rs | 134 +++++++++++++++++- 2 files changed, 154 insertions(+), 3 deletions(-) diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index b1c8169..ad9093f 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -195,4 +195,27 @@ contract UniswapV4ExecutorTest is Test, Constants { IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut ); } + + function testMultipleSwapIntegration() public { + // USDE -> USDT -> WBTC -> + // Generated by the Tycho swap encoder - test_encode_uniswap_v4_combined + bytes memory protocolData = + hex"4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000000000001015615deb798bb3e4dfa0139dfa1b3d433cc23b72f91dd7346dac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c"; + + uint256 amountIn = 100 ether; + deal(USDE_ADDR, address(uniswapV4Exposed), amountIn); + uint256 usdeBalanceBeforePool = USDE.balanceOf(poolManager); + uint256 usdeBalanceBeforeSwapExecutor = + USDE.balanceOf(address(uniswapV4Exposed)); + + uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData); + assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); + assertEq( + USDE.balanceOf(address(uniswapV4Exposed)), + usdeBalanceBeforeSwapExecutor - amountIn + ); + assertTrue( + IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut + ); + } } diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index d391349..f096e46 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -189,6 +189,16 @@ impl SwapEncoder for UniswapV4SwapEncoder { } let token_in_address = bytes_to_address(&swap.token_in)?; let token_out_address = bytes_to_address(&swap.token_out)?; + let group_token_in = if let Some(group_token_in) = encoding_context.group_token_in { + bytes_to_address(&group_token_in)? + } else { + token_in_address + }; + let group_token_out = if let Some(group_token_out) = encoding_context.group_token_out { + bytes_to_address(&group_token_out)? + } else { + token_out_address + }; let mut amount_out_min = vec![0u8; 32]; // Zero-filled buffer of 32 bytes let min_value = encoding_context .amount_out_min @@ -239,8 +249,8 @@ impl SwapEncoder for UniswapV4SwapEncoder { } let args = ( - token_in_address, - token_out_address, + group_token_in, + group_token_out, amount_out_min, zero_to_one, callback_executor, @@ -541,7 +551,7 @@ mod tests { } #[test] - fn test_encode_uniswap_v4_grouped() { + fn test_encode_uniswap_v4_second_swap() { let fee = BigInt::from(3000); let tick_spacing = BigInt::from(60); let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); @@ -596,4 +606,122 @@ mod tests { )) ); } + + #[test] + fn test_encode_uniswap_v4_combined() { + // USDE -> USDT -> WBTC + let fee = BigInt::from(100); + let tick_spacing = BigInt::from(1); + let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); + let encoded_tick_spacing = Bytes::from(tick_spacing.to_signed_bytes_be()); + let group_token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE + let group_token_out = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); // WBTC + let token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE + let token_out = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT + + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec())); + static_attributes.insert("tickSpacing".into(), Bytes::from(encoded_tick_spacing.to_vec())); + + let usv4_pool = ProtocolComponent { + id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), + static_attributes, + ..Default::default() + }; + let swap = Swap { + component: usv4_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), + exact_out: false, + router_address: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), + group_token_in: Some(group_token_in.clone()), + group_token_out: Some(group_token_out.clone()), + amount_out_min: Some(BigUint::from(1u128)), + }; + let encoder = + UniswapV4SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); + let encoded_simple_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_simple_swap = encode(&encoded_simple_swap); + + let fee = BigInt::from(3000); + let tick_spacing = BigInt::from(60); + let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); + let encoded_tick_spacing = Bytes::from(tick_spacing.to_signed_bytes_be()); + let token_in = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT + let token_out = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); // WBTC + + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec())); + static_attributes.insert("tickSpacing".into(), Bytes::from(encoded_tick_spacing.to_vec())); + + let usv4_pool = ProtocolComponent { + id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), + static_attributes, + ..Default::default() + }; + + let swap = Swap { + component: usv4_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let encoding_context = EncodingContext { + receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), + exact_out: false, + router_address: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), + group_token_in: Some(group_token_in), + group_token_out: Some(group_token_out), + amount_out_min: Some(BigUint::from(1u128)), + }; + + let encoded_grouped_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_grouped_swap = encode(&encoded_grouped_swap); + + let combined_hex = format!("{}{}", hex_simple_swap, hex_grouped_swap); + + println!("{}", combined_hex); + + assert_eq!( + combined_hex, + String::from(concat!( + // group_token in + "4c9edd5852cd905f086c759e8383e09bff1e68b3", + // group_token out + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", + // amount out min (0 as u128) + "0000000000000000000000000000000000000000000000000000000000000001", + // zero for one + "01", + // router address + "5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", + // callback selector for "unlockCallback(bytes)" + "91dd7346", + // pool params: + // - intermediary token USDT + "dac17f958d2ee523a2206206994597c13d831ec7", + // - fee + "000064", + // - tick spacing + "000001", + // Second swap (grouped) + // pool params: + // - intermediary token WBTC + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", + // - fee + "000bb8", + // - tick spacing + "00003c" + )) + ); + } } From f7ddace5591fcee3425c138b031de321fb8336ef Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 19 Feb 2025 21:42:13 +0530 Subject: [PATCH 08/12] feat: early return in usv4 swap encoder for second swap, add utils --- .../test/executors/UniswapV4Executor.t.sol | 2 +- .../evm/swap_encoder/swap_encoders.rs | 92 +++++++------------ src/encoding/evm/utils.rs | 19 +++- 3 files changed, 51 insertions(+), 62 deletions(-) diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index ad9093f..09d9028 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -197,7 +197,7 @@ contract UniswapV4ExecutorTest is Test, Constants { } function testMultipleSwapIntegration() public { - // USDE -> USDT -> WBTC -> + // USDE -> USDT -> WBTC // Generated by the Tycho swap encoder - test_encode_uniswap_v4_combined bytes memory protocolData = hex"4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000000000001015615deb798bb3e4dfa0139dfa1b3d433cc23b72f91dd7346dac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c"; diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index f096e46..e172c41 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -7,7 +7,9 @@ use crate::encoding::{ errors::EncodingError, evm::{ approvals::protocol_approvals_manager::ProtocolApprovalsManager, - utils::{bytes_to_address, encode_function_selector, pad_to_fixed_size}, + utils::{ + bytes_to_address, encode_function_selector, get_static_attribute, pad_to_fixed_size, + }, }, models::{EncodingContext, Swap}, swap_encoder::SwapEncoder, @@ -105,16 +107,7 @@ impl SwapEncoder for UniswapV3SwapEncoder { let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address); let component_id = Address::from_str(&swap.component.id) .map_err(|_| EncodingError::FatalError("Invalid USV3 component id".to_string()))?; - let pool_fee_bytes = swap - .component - .static_attributes - .get("fee") - .ok_or_else(|| { - EncodingError::FatalError( - "Pool fee not found in Uniswap v3 static attributes".to_string(), - ) - })? - .to_vec(); + let pool_fee_bytes = get_static_attribute(&swap, "fee")?; let pool_fee_u24 = pad_to_fixed_size::<3>(&pool_fee_bytes) .map_err(|_| EncodingError::FatalError("Failed to extract fee bytes".to_string()))?; @@ -183,10 +176,27 @@ impl SwapEncoder for UniswapV4SwapEncoder { swap: Swap, encoding_context: EncodingContext, ) -> Result, EncodingError> { - let mut first_swap = false; - if encoding_context.group_token_in == Some(swap.token_in.clone()) { - first_swap = true; + let fee = get_static_attribute(&swap, "fee")?; + + let pool_fee_u24 = pad_to_fixed_size::<3>(&fee) + .map_err(|_| EncodingError::FatalError("Failed to pad fee bytes".to_string()))?; + + let tick_spacing = get_static_attribute(&swap, "tickSpacing")?; + + let pool_tick_spacing_u24 = pad_to_fixed_size::<3>(&tick_spacing).map_err(|_| { + EncodingError::FatalError("Failed to pad tick spacing bytes".to_string()) + })?; + + // Early check if this is not the first swap + if encoding_context.group_token_in != Some(swap.token_in.clone()) { + return Ok(Self::encode_pool_params( + bytes_to_address(&swap.token_out)?, + pool_fee_u24, + pool_tick_spacing_u24, + )); } + + // This is the first swap, compute all necessary values let token_in_address = bytes_to_address(&swap.token_in)?; let token_out_address = bytes_to_address(&swap.token_out)?; let group_token_in = if let Some(group_token_in) = encoding_context.group_token_in { @@ -208,46 +218,9 @@ impl SwapEncoder for UniswapV4SwapEncoder { let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address); let callback_executor = bytes_to_address(&encoding_context.router_address)?; - let fee = swap - .component - .static_attributes - .get("fee") - .ok_or_else(|| { - EncodingError::FatalError( - "Pool fee not found in Uniswap v4 static attributes".to_string(), - ) - })? - .to_vec(); - - let pool_fee_u24 = pad_to_fixed_size::<3>(&fee) - .map_err(|_| EncodingError::FatalError("Failed to extract fee bytes".to_string()))?; - - let tick_spacing = swap - .component - .static_attributes - .get("tickSpacing") - .ok_or_else(|| { - EncodingError::FatalError( - "Pool tick spacing not found in Uniswap v4 static attributes".to_string(), - ) - })? - .to_vec(); - - let pool_tick_spacing_u24 = pad_to_fixed_size::<3>(&tick_spacing).map_err(|_| { - EncodingError::FatalError("Failed to extract tick spacing bytes".to_string()) - })?; - let pool_params = Self::encode_pool_params(token_out_address, pool_fee_u24, pool_tick_spacing_u24); - if !first_swap { - return Ok(Self::encode_pool_params( - token_out_address, - pool_fee_u24, - pool_tick_spacing_u24, - )); - } - let args = ( group_token_in, group_token_out, @@ -527,9 +500,9 @@ mod tests { assert_eq!( hex_swap, String::from(concat!( - // token in + // group token in "4c9edd5852cd905f086c759e8383e09bff1e68b3", - // token out + // group token out "dac17f958d2ee523a2206206994597c13d831ec7", // amount out min (0 as u128) "0000000000000000000000000000000000000000000000000000000000000001", @@ -540,11 +513,11 @@ mod tests { // callback selector for "unlockCallback(bytes)" "91dd7346", // pool params: - // - intermediary token (20 bytes) + // - intermediary token "dac17f958d2ee523a2206206994597c13d831ec7", - // - fee (3 bytes) + // - fee "000064", - // - tick spacing (3 bytes) + // - tick spacing "000001" )) ); @@ -556,6 +529,7 @@ mod tests { let tick_spacing = BigInt::from(60); let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); let encoded_tick_spacing = Bytes::from(tick_spacing.to_signed_bytes_be()); + let group_token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE let token_in = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT let token_out = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); // WBTC @@ -580,9 +554,9 @@ mod tests { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, router_address: Bytes::zero(20), - // Different from token_in and token_out - group_token_in: Some(Bytes::zero(20)), - group_token_out: Some(Bytes::zero(20)), + group_token_in: Some(group_token_in), + // Token out is the same as the group token out + group_token_out: Some(token_out), amount_out_min: Some(BigUint::from(1u128)), }; diff --git a/src/encoding/evm/utils.rs b/src/encoding/evm/utils.rs index 6f15d4e..a59abff 100644 --- a/src/encoding/evm/utils.rs +++ b/src/encoding/evm/utils.rs @@ -4,7 +4,10 @@ use alloy_primitives::{aliases::U24, keccak256, Address, FixedBytes, Keccak256, use num_bigint::BigUint; use tycho_core::Bytes; -use crate::encoding::{errors::EncodingError, models::Solution}; +use crate::encoding::{ + errors::EncodingError, + models::{Solution, Swap}, +}; /// Safely converts a `Bytes` object to an `Address` object. /// @@ -92,7 +95,7 @@ pub fn get_token_position(tokens: Vec, token: Bytes) -> Result(input: &[u8]) -> Result<[u8; N], EncodingError> { let mut padded = [0u8; N]; let start = N - input.len(); @@ -105,3 +108,15 @@ pub fn encode_function_selector(selector: &str) -> FixedBytes<4> { let hash = keccak256(selector.as_bytes()); FixedBytes::<4>::from([hash[0], hash[1], hash[2], hash[3]]) } + +/// Extracts a static attribute from a swap. +pub fn get_static_attribute(swap: &Swap, attribute_name: &str) -> Result, EncodingError> { + Ok(swap + .component + .static_attributes + .get(attribute_name) + .ok_or_else(|| { + EncodingError::FatalError(format!("Attribute {} not found", attribute_name)) + })? + .to_vec()) +} From 7f3aca90ba20ec68fa6f3ba3267aa87cb2540d70 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 19 Feb 2025 21:50:43 +0530 Subject: [PATCH 09/12] fix: update EncodingContext in strategy_encoder --- .../evm/strategy_encoder/strategy_encoders.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index a9b3107..1d2aaf3 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -2,6 +2,7 @@ use std::{collections::HashSet, str::FromStr}; use alloy_primitives::{aliases::U24, FixedBytes, U256, U8}; use alloy_sol_types::SolValue; +use num_bigint::BigUint; use tycho_core::{keccak256, Bytes}; use crate::encoding::{ @@ -189,6 +190,9 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { receiver: solution.router_address.clone(), exact_out: solution.exact_out, router_address: solution.router_address.clone(), + group_token_in: Some(tokens.first().unwrap().clone()), + group_token_out: Some(tokens.last().unwrap().clone()), + amount_out_min: Some(min_amount_out.clone()), }; let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { @@ -204,7 +208,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { Bytes::from_str(swap_encoder.executor_address()).map_err(|_| { EncodingError::FatalError("Invalid executor address".to_string()) })?, - self.encode_executor_selector(swap_encoder.executor_selector()), + self.encode_executor_selector(swap_encoder.swap_selector()), grouped_protocol_data, ); swaps.push(swap_data); @@ -292,6 +296,9 @@ impl StrategyEncoder for ExecutorStrategyEncoder { receiver: receiver.clone(), exact_out: solution.exact_out, router_address: router_address.clone(), + group_token_in: Some(swap.token_in.clone()), + group_token_out: Some(swap.token_out.clone()), + amount_out_min: Some(BigUint::from(1u128)), }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; grouped_protocol_data.extend(protocol_data); @@ -303,11 +310,7 @@ impl StrategyEncoder for ExecutorStrategyEncoder { Ok(( grouped_protocol_data, executor_address, - Some( - swap_encoder - .executor_selector() - .to_string(), - ), + Some(swap_encoder.swap_selector().to_string()), )) } @@ -964,4 +967,4 @@ mod tests { assert_eq!(hex_calldata[..520], expected_input); assert_eq!(hex_calldata[1288..], expected_swaps); } -} \ No newline at end of file +} From 1e9b411ca5f50a0e5123df8456b9e4ff411bc7a6 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 19 Feb 2025 23:07:26 +0530 Subject: [PATCH 10/12] chore: rm redundant pool params encoder fn --- src/encoding/evm/swap_encoder/swap_encoders.rs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index e172c41..9a38d06 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -152,14 +152,6 @@ impl UniswapV4SwapEncoder { fn get_zero_to_one(sell_token_address: Address, buy_token_address: Address) -> bool { sell_token_address < buy_token_address } - - fn encode_pool_params( - intermediary_token: Address, - fee: [u8; 3], - tick_spacing: [u8; 3], - ) -> Vec { - (intermediary_token, fee, tick_spacing).abi_encode_packed() - } } impl SwapEncoder for UniswapV4SwapEncoder { @@ -189,11 +181,8 @@ impl SwapEncoder for UniswapV4SwapEncoder { // Early check if this is not the first swap if encoding_context.group_token_in != Some(swap.token_in.clone()) { - return Ok(Self::encode_pool_params( - bytes_to_address(&swap.token_out)?, - pool_fee_u24, - pool_tick_spacing_u24, - )); + return Ok((bytes_to_address(&swap.token_out)?, pool_fee_u24, pool_tick_spacing_u24) + .abi_encode_packed()); } // This is the first swap, compute all necessary values @@ -219,7 +208,7 @@ impl SwapEncoder for UniswapV4SwapEncoder { let callback_executor = bytes_to_address(&encoding_context.router_address)?; let pool_params = - Self::encode_pool_params(token_out_address, pool_fee_u24, pool_tick_spacing_u24); + (token_out_address, pool_fee_u24, pool_tick_spacing_u24).abi_encode_packed(); let args = ( group_token_in, From e2a2aaaad109f4012125d517286614d1a7469b94 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 19 Feb 2025 23:59:27 +0530 Subject: [PATCH 11/12] refactor: usv4 swap encoder test, encoder --- .../test/executors/UniswapV4Executor.t.sol | 2 +- .../evm/swap_encoder/swap_encoders.rs | 173 +++++++++--------- 2 files changed, 88 insertions(+), 87 deletions(-) diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index 09d9028..c4b1845 100644 --- a/foundry/test/executors/UniswapV4Executor.t.sol +++ b/foundry/test/executors/UniswapV4Executor.t.sol @@ -198,7 +198,7 @@ contract UniswapV4ExecutorTest is Test, Constants { function testMultipleSwapIntegration() public { // USDE -> USDT -> WBTC - // Generated by the Tycho swap encoder - test_encode_uniswap_v4_combined + // Generated by the Tycho swap encoder - test_encode_uniswap_v4_sequential_swap bytes memory protocolData = hex"4c9edd5852cd905f086c759e8383e09bff1e68b32260fac5e5542a773aa44fbcfedf7c193bc2c5990000000000000000000000000000000000000000000000000000000000000001015615deb798bb3e4dfa0139dfa1b3d433cc23b72f91dd7346dac17f958d2ee523a2206206994597c13d831ec70000640000012260fac5e5542a773aa44fbcfedf7c193bc2c599000bb800003c"; diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 9a38d06..729b326 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -8,7 +8,8 @@ use crate::encoding::{ evm::{ approvals::protocol_approvals_manager::ProtocolApprovalsManager, utils::{ - bytes_to_address, encode_function_selector, get_static_attribute, pad_to_fixed_size, + biguint_to_u256, bytes_to_address, encode_function_selector, get_static_attribute, + pad_to_fixed_size, }, }, models::{EncodingContext, Swap}, @@ -180,7 +181,11 @@ impl SwapEncoder for UniswapV4SwapEncoder { })?; // Early check if this is not the first swap - if encoding_context.group_token_in != Some(swap.token_in.clone()) { + if encoding_context + .group_token_in + .is_some() && + encoding_context.group_token_in != Some(swap.token_in.clone()) + { return Ok((bytes_to_address(&swap.token_out)?, pool_fee_u24, pool_tick_spacing_u24) .abi_encode_packed()); } @@ -198,12 +203,11 @@ impl SwapEncoder for UniswapV4SwapEncoder { } else { token_out_address }; - let mut amount_out_min = vec![0u8; 32]; // Zero-filled buffer of 32 bytes - let min_value = encoding_context - .amount_out_min - .unwrap_or_default() - .to_bytes_be(); - amount_out_min[(32 - min_value.len())..].copy_from_slice(&min_value); + let amount_out_min = biguint_to_u256( + &encoding_context + .amount_out_min + .unwrap_or_default(), + ); let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address); let callback_executor = bytes_to_address(&encoding_context.router_address)?; @@ -475,8 +479,9 @@ mod tests { exact_out: false, // Same as the executor address router_address: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), - group_token_in: Some(token_in), - group_token_out: Some(token_out), + // When group tokens are None, the token in and out are the group tokens + group_token_in: None, + group_token_out: None, amount_out_min: Some(BigUint::from(1u128)), }; let encoder = @@ -571,88 +576,86 @@ mod tests { } #[test] - fn test_encode_uniswap_v4_combined() { - // USDE -> USDT -> WBTC - let fee = BigInt::from(100); - let tick_spacing = BigInt::from(1); - let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); - let encoded_tick_spacing = Bytes::from(tick_spacing.to_signed_bytes_be()); - let group_token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE - let group_token_out = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); // WBTC - let token_in = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); // USDE - let token_out = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT + fn test_encode_uniswap_v4_sequential_swap() { + let usde_address = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); + let usdt_address = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); + let wbtc_address = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); + let router_address = Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"); + let receiver_address = router_address.clone(); - let mut static_attributes: HashMap = HashMap::new(); - static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec())); - static_attributes.insert("tickSpacing".into(), Bytes::from(encoded_tick_spacing.to_vec())); - - let usv4_pool = ProtocolComponent { - id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), - static_attributes, - ..Default::default() - }; - let swap = Swap { - component: usv4_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - let encoding_context = EncodingContext { - receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), + // The context is the same for both swaps, since the group token in and out are the same + let context = EncodingContext { + receiver: receiver_address.clone(), exact_out: false, - router_address: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), - group_token_in: Some(group_token_in.clone()), - group_token_out: Some(group_token_out.clone()), + router_address: router_address.clone(), + group_token_in: Some(usde_address.clone()), + group_token_out: Some(wbtc_address.clone()), amount_out_min: Some(BigUint::from(1u128)), }; + + // Setup - First sequence: USDE -> USDT + let usde_usdt_fee = BigInt::from(100); + let usde_usdt_tick_spacing = BigInt::from(1); + let usde_usdt_encoded_pool_fee = Bytes::from(usde_usdt_fee.to_signed_bytes_be()); + let usde_usdt_encoded_tick_spacing = + Bytes::from(usde_usdt_tick_spacing.to_signed_bytes_be()); + + let mut usde_usdt_static_attributes: HashMap = HashMap::new(); + usde_usdt_static_attributes + .insert("fee".into(), Bytes::from(usde_usdt_encoded_pool_fee.to_vec())); + usde_usdt_static_attributes + .insert("tickSpacing".into(), Bytes::from(usde_usdt_encoded_tick_spacing.to_vec())); + + let usde_usdt_component = ProtocolComponent { + id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), + static_attributes: usde_usdt_static_attributes, + ..Default::default() + }; + + // Setup - Second sequence: USDT -> WBTC + let usdt_wbtc_fee = BigInt::from(3000); + let usdt_wbtc_tick_spacing = BigInt::from(60); + let usdt_wbtc_encoded_pool_fee = Bytes::from(usdt_wbtc_fee.to_signed_bytes_be()); + let usdt_wbtc_encoded_tick_spacing = + Bytes::from(usdt_wbtc_tick_spacing.to_signed_bytes_be()); + + let mut usdt_wbtc_static_attributes: HashMap = HashMap::new(); + usdt_wbtc_static_attributes + .insert("fee".into(), Bytes::from(usdt_wbtc_encoded_pool_fee.to_vec())); + usdt_wbtc_static_attributes + .insert("tickSpacing".into(), Bytes::from(usdt_wbtc_encoded_tick_spacing.to_vec())); + + let usdt_wbtc_component = ProtocolComponent { + id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), + static_attributes: usdt_wbtc_static_attributes, + ..Default::default() + }; + + let initial_swap = Swap { + component: usde_usdt_component, + token_in: usde_address.clone(), + token_out: usdt_address.clone(), + split: 0f64, + }; + + let second_swap = Swap { + component: usdt_wbtc_component, + token_in: usdt_address, + token_out: wbtc_address.clone(), + split: 0f64, + }; + let encoder = UniswapV4SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); - let encoded_simple_swap = encoder - .encode_swap(swap, encoding_context) + let initial_encoded_swap = encoder + .encode_swap(initial_swap, context.clone()) .unwrap(); - let hex_simple_swap = encode(&encoded_simple_swap); - - let fee = BigInt::from(3000); - let tick_spacing = BigInt::from(60); - let encoded_pool_fee = Bytes::from(fee.to_signed_bytes_be()); - let encoded_tick_spacing = Bytes::from(tick_spacing.to_signed_bytes_be()); - let token_in = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT - let token_out = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); // WBTC - - let mut static_attributes: HashMap = HashMap::new(); - static_attributes.insert("fee".into(), Bytes::from(encoded_pool_fee.to_vec())); - static_attributes.insert("tickSpacing".into(), Bytes::from(encoded_tick_spacing.to_vec())); - - let usv4_pool = ProtocolComponent { - id: String::from("0x000000000004444c5dc75cB358380D2e3dE08A90"), - static_attributes, - ..Default::default() - }; - - let swap = Swap { - component: usv4_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - - let encoding_context = EncodingContext { - receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), - exact_out: false, - router_address: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), - group_token_in: Some(group_token_in), - group_token_out: Some(group_token_out), - amount_out_min: Some(BigUint::from(1u128)), - }; - - let encoded_grouped_swap = encoder - .encode_swap(swap, encoding_context) + let second_encoded_swap = encoder + .encode_swap(second_swap, context) .unwrap(); - let hex_grouped_swap = encode(&encoded_grouped_swap); - let combined_hex = format!("{}{}", hex_simple_swap, hex_grouped_swap); - - println!("{}", combined_hex); + let combined_hex = + format!("{}{}", encode(&initial_encoded_swap), encode(&second_encoded_swap)); assert_eq!( combined_hex, @@ -661,7 +664,7 @@ mod tests { "4c9edd5852cd905f086c759e8383e09bff1e68b3", // group_token out "2260fac5e5542a773aa44fbcfedf7c193bc2c599", - // amount out min (0 as u128) + // amount out min (1 as u128) "0000000000000000000000000000000000000000000000000000000000000001", // zero for one "01", @@ -676,8 +679,6 @@ mod tests { "000064", // - tick spacing "000001", - // Second swap (grouped) - // pool params: // - intermediary token WBTC "2260fac5e5542a773aa44fbcfedf7c193bc2c599", // - fee From c472cda697f57315cde977efa7167b935995ed5d Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 20 Feb 2025 00:05:47 +0530 Subject: [PATCH 12/12] refactor: make group tokens and amount out min non optional --- .../evm/strategy_encoder/strategy_encoders.rs | 12 +-- .../evm/swap_encoder/swap_encoders.rs | 86 +++++++++---------- src/encoding/models.rs | 6 +- 3 files changed, 48 insertions(+), 56 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 1d2aaf3..dfa27d7 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -190,9 +190,9 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { receiver: solution.router_address.clone(), exact_out: solution.exact_out, router_address: solution.router_address.clone(), - group_token_in: Some(tokens.first().unwrap().clone()), - group_token_out: Some(tokens.last().unwrap().clone()), - amount_out_min: Some(min_amount_out.clone()), + group_token_in: tokens.first().unwrap().clone(), + group_token_out: tokens.last().unwrap().clone(), + amount_out_min: min_amount_out.clone(), }; let mut grouped_protocol_data: Vec = vec![]; for swap in grouped_swap.swaps.iter() { @@ -296,9 +296,9 @@ impl StrategyEncoder for ExecutorStrategyEncoder { receiver: receiver.clone(), exact_out: solution.exact_out, router_address: router_address.clone(), - group_token_in: Some(swap.token_in.clone()), - group_token_out: Some(swap.token_out.clone()), - amount_out_min: Some(BigUint::from(1u128)), + group_token_in: swap.token_in.clone(), + group_token_out: swap.token_out.clone(), + amount_out_min: BigUint::from(1u128), }; let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?; grouped_protocol_data.extend(protocol_data); diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 729b326..b252ab8 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -181,11 +181,7 @@ impl SwapEncoder for UniswapV4SwapEncoder { })?; // Early check if this is not the first swap - if encoding_context - .group_token_in - .is_some() && - encoding_context.group_token_in != Some(swap.token_in.clone()) - { + if encoding_context.group_token_in != swap.token_in { return Ok((bytes_to_address(&swap.token_out)?, pool_fee_u24, pool_tick_spacing_u24) .abi_encode_packed()); } @@ -193,21 +189,10 @@ impl SwapEncoder for UniswapV4SwapEncoder { // This is the first swap, compute all necessary values let token_in_address = bytes_to_address(&swap.token_in)?; let token_out_address = bytes_to_address(&swap.token_out)?; - let group_token_in = if let Some(group_token_in) = encoding_context.group_token_in { - bytes_to_address(&group_token_in)? - } else { - token_in_address - }; - let group_token_out = if let Some(group_token_out) = encoding_context.group_token_out { - bytes_to_address(&group_token_out)? - } else { - token_out_address - }; - let amount_out_min = biguint_to_u256( - &encoding_context - .amount_out_min - .unwrap_or_default(), - ); + let group_token_in_address = bytes_to_address(&encoding_context.group_token_in)?; + let group_token_out_address = bytes_to_address(&encoding_context.group_token_out)?; + + let amount_out_min = biguint_to_u256(&encoding_context.amount_out_min); let zero_to_one = Self::get_zero_to_one(token_in_address, token_out_address); let callback_executor = bytes_to_address(&encoding_context.router_address)?; @@ -215,8 +200,8 @@ impl SwapEncoder for UniswapV4SwapEncoder { (token_out_address, pool_fee_u24, pool_tick_spacing_u24).abi_encode_packed(); let args = ( - group_token_in, - group_token_out, + group_token_in_address, + group_token_out_address, amount_out_min, zero_to_one, callback_executor, @@ -315,19 +300,22 @@ mod tests { id: String::from("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640"), ..Default::default() }; + + let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); + let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); let swap = Swap { component: usv2_pool, - token_in: Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), - token_out: Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"), + token_in: token_in.clone(), + token_out: token_out.clone(), split: 0f64, }; let encoding_context = EncodingContext { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, router_address: Bytes::zero(20), - group_token_in: None, - group_token_out: None, - amount_out_min: None, + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + amount_out_min: BigUint::from(0u128), }; let encoder = UniswapV2SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); @@ -361,19 +349,21 @@ mod tests { static_attributes, ..Default::default() }; + let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); + let token_out = Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"); let swap = Swap { component: usv3_pool, - token_in: Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), - token_out: Bytes::from("0x6b175474e89094c44da98b954eedeac495271d0f"), + token_in: token_in.clone(), + token_out: token_out.clone(), split: 0f64, }; let encoding_context = EncodingContext { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, router_address: Bytes::zero(20), - group_token_in: None, - group_token_out: None, - amount_out_min: None, + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + amount_out_min: BigUint::from(0u128), }; let encoder = UniswapV3SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); @@ -407,10 +397,12 @@ mod tests { protocol_system: String::from("vm:balancer_v2"), ..Default::default() }; + let token_in = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); + let token_out = Bytes::from("0xba100000625a3754423978a60c9317c58a424e3D"); let swap = Swap { component: balancer_pool, - token_in: Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"), // WETH - token_out: Bytes::from("0xba100000625a3754423978a60c9317c58a424e3D"), // BAL + token_in: token_in.clone(), + token_out: token_out.clone(), split: 0f64, }; let encoding_context = EncodingContext { @@ -418,9 +410,9 @@ mod tests { receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), exact_out: false, router_address: Bytes::zero(20), - group_token_in: None, - group_token_out: None, - amount_out_min: None, + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + amount_out_min: BigUint::from(0u128), }; let encoder = BalancerV2SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); @@ -479,10 +471,10 @@ mod tests { exact_out: false, // Same as the executor address router_address: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"), - // When group tokens are None, the token in and out are the group tokens - group_token_in: None, - group_token_out: None, - amount_out_min: Some(BigUint::from(1u128)), + + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + amount_out_min: BigUint::from(1u128), }; let encoder = UniswapV4SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); @@ -548,10 +540,10 @@ mod tests { receiver: Bytes::from("0x0000000000000000000000000000000000000001"), exact_out: false, router_address: Bytes::zero(20), - group_token_in: Some(group_token_in), + group_token_in: group_token_in.clone(), // Token out is the same as the group token out - group_token_out: Some(token_out), - amount_out_min: Some(BigUint::from(1u128)), + group_token_out: token_out.clone(), + amount_out_min: BigUint::from(1u128), }; let encoder = @@ -588,9 +580,9 @@ mod tests { receiver: receiver_address.clone(), exact_out: false, router_address: router_address.clone(), - group_token_in: Some(usde_address.clone()), - group_token_out: Some(wbtc_address.clone()), - amount_out_min: Some(BigUint::from(1u128)), + group_token_in: usde_address.clone(), + group_token_out: wbtc_address.clone(), + amount_out_min: BigUint::from(1u128), }; // Setup - First sequence: USDE -> USDT diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 543b77a..12a0c22 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -119,9 +119,9 @@ pub struct EncodingContext { pub receiver: Bytes, pub exact_out: bool, pub router_address: Bytes, - pub group_token_in: Option, - pub group_token_out: Option, - pub amount_out_min: Option, + pub group_token_in: Bytes, + pub group_token_out: Bytes, + pub amount_out_min: BigUint, } #[derive(Clone, PartialEq, Eq, Hash)]