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" + )) + ); + } }