From 2d4b0b995b903e42c8b48f499cf406c8d5e46254 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Fri, 11 Jul 2025 18:12:27 -0400 Subject: [PATCH 1/2] feat: UniswapX encoding example --- examples/uniswapx-encoding-example/README.md | 12 ++ examples/uniswapx-encoding-example/main.rs | 172 +++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 examples/uniswapx-encoding-example/README.md create mode 100644 examples/uniswapx-encoding-example/main.rs diff --git a/examples/uniswapx-encoding-example/README.md b/examples/uniswapx-encoding-example/README.md new file mode 100644 index 0000000..d139426 --- /dev/null +++ b/examples/uniswapx-encoding-example/README.md @@ -0,0 +1,12 @@ +# UniswapX Encoding Example + +This guide enables you to: + +1. Create a Solution object +2. Create callback data for executing a UniswapX Order + +## How to run + +```bash +cargo run --release --example uniswapx-encoding-example +``` \ No newline at end of file diff --git a/examples/uniswapx-encoding-example/main.rs b/examples/uniswapx-encoding-example/main.rs new file mode 100644 index 0000000..d60d126 --- /dev/null +++ b/examples/uniswapx-encoding-example/main.rs @@ -0,0 +1,172 @@ +use std::{collections::HashMap, str::FromStr}; + +use alloy::{ + hex::encode, + primitives::{Address, Keccak256}, + sol_types::SolValue, +}; +use num_bigint::{BigInt, BigUint}; +use tycho_common::{models::protocol::ProtocolComponent, Bytes}; +use tycho_execution::encoding::{ + evm::{ + approvals::protocol_approvals_manager::ProtocolApprovalsManager, + encoder_builders::TychoRouterEncoderBuilder, + utils::{biguint_to_u256, bytes_to_address}, + }, + models::{Solution, Swap, UserTransferType}, +}; + +/// Encodes the input data for a function call to the given function selector. +pub fn encode_input(selector: &str, mut encoded_args: Vec) -> Vec { + let mut hasher = Keccak256::new(); + hasher.update(selector.as_bytes()); + let selector_bytes = &hasher.finalize()[..4]; + let mut call_data = selector_bytes.to_vec(); + // Remove extra prefix if present (32 bytes for dynamic data) + // Alloy encoding is including a prefix for dynamic data indicating the offset or length + // but at this point we don't want that + if encoded_args.len() > 32 && + encoded_args[..32] == + [0u8; 31] + .into_iter() + .chain([32].to_vec()) + .collect::>() + { + encoded_args = encoded_args[32..].to_vec(); + } + call_data.extend(encoded_args); + call_data +} + +fn main() { + let router_address = Bytes::from_str("0xfD0b31d2E955fA55e3fa641Fe90e08b677188d35") + .expect("Failed to create router address"); + + // Initialize the encoder + let encoder = TychoRouterEncoderBuilder::new() + .chain(tycho_common::models::Chain::Ethereum) + .user_transfer_type(UserTransferType::TransferFrom) + .executors_file_path("config/test_executor_addresses.json".to_string()) + .router_address(router_address.clone()) + .build() + .expect("Failed to build encoder"); + + // Set up UniswapX-related variables + let filler = Bytes::from_str("0x6D9da78B6A5BEdcA287AA5d49613bA36b90c15C4").unwrap(); + let usx_reactor = Address::from_str("0x00000011F84B9aa48e5f8aA8B9897600006289Be").unwrap(); + + // ------------------- Encode a sequential swap ------------------- + // Prepare data to encode. We will encode a sequential swap from DAI to USDT though USDC using + // USV3 pools + // + // DAI ───(USV3)──> USDC ───(USV2)──> USDT + // + // First we need to create swap objects + + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let usdt = Bytes::from_str("0xdAC17F958D2ee523a2206206994597C13D831ec7").unwrap(); + + let swap_dai_usdc = Swap { + component: ProtocolComponent { + id: "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168".to_string(), + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs + .insert("fee".to_string(), Bytes::from(BigInt::from(100).to_signed_bytes_be())); + attrs + }, + ..Default::default() + }, + token_in: dai.clone(), + token_out: usdc.clone(), + split: 0f64, + user_data: None, + }; + let swap_usdc_usdt = Swap { + component: ProtocolComponent { + id: "0x3416cF6C708Da44DB2624D63ea0AAef7113527C6".to_string(), + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs + .insert("fee".to_string(), Bytes::from(BigInt::from(100).to_signed_bytes_be())); + attrs + }, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: usdt.clone(), + split: 0f64, + user_data: None, + }; + + // Then we create a solution object with the previous swap + let solution = Solution { + exact_out: false, + given_token: dai.clone(), + given_amount: BigUint::from_str("2_000_000000000000000000").unwrap(), + checked_token: usdt.clone(), + checked_amount: BigUint::from_str("1_990_000000").unwrap(), + sender: filler.clone(), + receiver: filler.clone(), + swaps: vec![swap_dai_usdc, swap_usdc_usdt], + ..Default::default() + }; + + // Encode the solution using appropriate safety checks + let encoded_solution = encoder + .encode_solutions(vec![solution.clone()]) + .unwrap()[0] + .clone(); + + let given_amount = biguint_to_u256(&solution.given_amount); + let min_amount_out = biguint_to_u256(&solution.checked_amount); + let given_token = bytes_to_address(&solution.given_token).unwrap(); + let checked_token = bytes_to_address(&solution.checked_token).unwrap(); + let receiver = bytes_to_address(&solution.receiver).unwrap(); + + let method_calldata = ( + given_amount, + given_token, + checked_token, + min_amount_out, + false, // wrap + false, // unwrap + receiver, + true, // transferFrom permitted + encoded_solution.swaps, + ) + .abi_encode(); + + let tycho_calldata = encode_input(&encoded_solution.function_signature, method_calldata); + + // Uniswap X specific part (check necessary approvals) + let filler_address = bytes_to_address(&filler).unwrap(); + let token_approvals_manager = ProtocolApprovalsManager::new().unwrap(); + + let token_in_approval_needed = token_approvals_manager + .approval_needed( + bytes_to_address(&dai).unwrap(), + filler_address, + bytes_to_address(&router_address).unwrap(), + ) + .unwrap(); + + let token_out_approval_needed = token_approvals_manager + .approval_needed(bytes_to_address(&usdc).unwrap(), filler_address, usx_reactor) + .unwrap(); + + let full_calldata = + (token_in_approval_needed, token_out_approval_needed, tycho_calldata).abi_encode_packed(); + + let hex_calldata = encode(&full_calldata); + + println!(" ====== Simple swap WETH -> USDC ======"); + println!( + "The following callback data should be sent to the filler contract, along with the \ + encoded order and signature: {:?}", + hex_calldata + ); +} From 7aa292ef82f411aaf575bcd91ffe640ea12e997b Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Mon, 14 Jul 2025 11:24:05 -0400 Subject: [PATCH 2/2] chore: small polishings to example - Add note in readme that order will need to be encoded still - Fix log - Don't pass executors to builder --- examples/uniswapx-encoding-example/README.md | 3 +++ examples/uniswapx-encoding-example/main.rs | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/uniswapx-encoding-example/README.md b/examples/uniswapx-encoding-example/README.md index d139426..807e685 100644 --- a/examples/uniswapx-encoding-example/README.md +++ b/examples/uniswapx-encoding-example/README.md @@ -5,6 +5,9 @@ This guide enables you to: 1. Create a Solution object 2. Create callback data for executing a UniswapX Order +Note: This guide only encodes the callback data for you. You will still have to encode the call to the +`execute` method of the filler, which also includes the encoded UniswapX order. + ## How to run ```bash diff --git a/examples/uniswapx-encoding-example/main.rs b/examples/uniswapx-encoding-example/main.rs index d60d126..b2e2f5e 100644 --- a/examples/uniswapx-encoding-example/main.rs +++ b/examples/uniswapx-encoding-example/main.rs @@ -46,7 +46,6 @@ fn main() { let encoder = TychoRouterEncoderBuilder::new() .chain(tycho_common::models::Chain::Ethereum) .user_transfer_type(UserTransferType::TransferFrom) - .executors_file_path("config/test_executor_addresses.json".to_string()) .router_address(router_address.clone()) .build() .expect("Failed to build encoder"); @@ -163,7 +162,7 @@ fn main() { let hex_calldata = encode(&full_calldata); - println!(" ====== Simple swap WETH -> USDC ======"); + println!(" ====== Simple swap DAI -> USDT ======"); println!( "The following callback data should be sent to the filler contract, along with the \ encoded order and signature: {:?}",