diff --git a/foundry/test/executors/UniswapV4Executor.t.sol b/foundry/test/executors/UniswapV4Executor.t.sol index e35b676..c4b1845 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_simple_swap + 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; @@ -174,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_sequential_swap + 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/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 73e62e9..ea676d8 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::{ @@ -190,6 +191,9 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { receiver: solution.router_address.clone(), exact_out: solution.exact_out, router_address: solution.router_address.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() { @@ -205,7 +209,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); @@ -293,6 +297,9 @@ impl StrategyEncoder for ExecutorStrategyEncoder { receiver: receiver.clone(), exact_out: solution.exact_out, router_address: router_address.clone(), + 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); @@ -304,11 +311,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()), )) } diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index f3d0fb0..b252ab8 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -6,7 +6,11 @@ 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::{ + biguint_to_u256, bytes_to_address, encode_function_selector, get_static_attribute, + pad_to_fixed_size, + }, }, models::{EncodingContext, Swap}, swap_encoder::SwapEncoder, @@ -16,11 +20,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 { @@ -31,7 +35,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( @@ -62,8 +66,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 { @@ -75,11 +79,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 { @@ -90,7 +94,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( @@ -104,24 +108,9 @@ 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")?; - // 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 = ( @@ -139,23 +128,112 @@ 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()) } } +/// 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. +/// * `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, + swap_selector: String, + callback_selector: String, +} + +impl UniswapV4SwapEncoder { + fn get_zero_to_one(sell_token_address: Address, buy_token_address: Address) -> bool { + sell_token_address < buy_token_address + } +} + +impl SwapEncoder for UniswapV4SwapEncoder { + fn new(executor_address: String) -> Self { + Self { + executor_address, + swap_selector: "swap(uint256,bytes)".to_string(), + callback_selector: "unlockCallback(bytes)".to_string(), + } + } + + fn encode_swap( + &self, + swap: Swap, + encoding_context: EncodingContext, + ) -> Result, EncodingError> { + 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 != swap.token_in { + 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 + let token_in_address = bytes_to_address(&swap.token_in)?; + let token_out_address = bytes_to_address(&swap.token_out)?; + 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)?; + + let pool_params = + (token_out_address, pool_fee_u24, pool_tick_spacing_u24).abi_encode_packed(); + + let args = ( + group_token_in_address, + group_token_out_address, + amount_out_min, + zero_to_one, + callback_executor, + encode_function_selector(&self.callback_selector), + pool_params, + ); + + Ok(args.abi_encode_packed()) + } + + fn executor_address(&self) -> &str { + &self.executor_address + } + + fn swap_selector(&self) -> &str { + &self.swap_selector + } + + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + /// Encodes a swap on a Balancer V2 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. +/// * `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, } @@ -163,7 +241,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(), } } @@ -198,8 +276,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()) @@ -211,7 +289,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::*; @@ -222,16 +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: token_in.clone(), + group_token_out: token_out.clone(), + amount_out_min: BigUint::from(0u128), }; let encoder = UniswapV2SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); @@ -265,16 +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: token_in.clone(), + group_token_out: token_out.clone(), + amount_out_min: BigUint::from(0u128), }; let encoder = UniswapV3SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); @@ -308,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 { @@ -319,6 +410,9 @@ mod tests { receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), exact_out: false, router_address: Bytes::zero(20), + 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")); @@ -344,4 +438,246 @@ mod tests { )) ); } + + #[test] + 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())); + 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: token_in.clone(), + token_out: token_out.clone(), + 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"), + + 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")); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + assert_eq!( + hex_swap, + String::from(concat!( + // group token in + "4c9edd5852cd905f086c759e8383e09bff1e68b3", + // group 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 + "dac17f958d2ee523a2206206994597c13d831ec7", + // - fee + "000064", + // - tick spacing + "000001" + )) + ); + } + + #[test] + 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()); + 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 + + 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), + group_token_in: group_token_in.clone(), + // Token out is the same as the group token out + group_token_out: token_out.clone(), + amount_out_min: 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) + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", + // - fee (3 bytes) + "000bb8", + // - tick spacing (3 bytes) + "00003c" + )) + ); + } + + #[test] + 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(); + + // 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: router_address.clone(), + group_token_in: usde_address.clone(), + group_token_out: wbtc_address.clone(), + amount_out_min: 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 initial_encoded_swap = encoder + .encode_swap(initial_swap, context.clone()) + .unwrap(); + let second_encoded_swap = encoder + .encode_swap(second_swap, context) + .unwrap(); + + let combined_hex = + format!("{}{}", encode(&initial_encoded_swap), encode(&second_encoded_swap)); + + assert_eq!( + combined_hex, + String::from(concat!( + // group_token in + "4c9edd5852cd905f086c759e8383e09bff1e68b3", + // group_token out + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", + // amount out min (1 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", + // - intermediary token WBTC + "2260fac5e5542a773aa44fbcfedf7c193bc2c599", + // - fee + "000bb8", + // - tick spacing + "00003c" + )) + ); + } } diff --git a/src/encoding/evm/utils.rs b/src/encoding/evm/utils.rs index d4017f6..a59abff 100644 --- a/src/encoding/evm/utils.rs +++ b/src/encoding/evm/utils.rs @@ -1,10 +1,13 @@ 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; -use crate::encoding::{errors::EncodingError, models::Solution}; +use crate::encoding::{ + errors::EncodingError, + models::{Solution, Swap}, +}; /// Safely converts a `Bytes` object to an `Address` object. /// @@ -91,3 +94,29 @@ 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]]) +} + +/// 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()) +} diff --git a/src/encoding/models.rs b/src/encoding/models.rs index 1111ffc..6b24030 100644 --- a/src/encoding/models.rs +++ b/src/encoding/models.rs @@ -106,11 +106,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: Bytes, + pub group_token_out: Bytes, + pub amount_out_min: BigUint, } #[derive(Clone, PartialEq, Eq, Hash)] 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`.