From 4bc615913ecb41a551a8b970ba5d96f0fc20ca42 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Fri, 31 Jan 2025 17:21:18 -0500 Subject: [PATCH] feat: Tycho encoder validation Validates: - Proper sequence of input, output, and first/last swap tokens for wrap/unwrap cases - All solutions contain at least one swap - Only exact in solutions are inputted (since we don't yet support exact out) --- src/encoding/evm/constants.rs | 10 ++ src/encoding/evm/mod.rs | 1 + src/encoding/evm/tycho_encoder.rs | 290 ++++++++++++++++++++++++++++-- src/encoding/tycho_encoder.rs | 2 + 4 files changed, 289 insertions(+), 14 deletions(-) create mode 100644 src/encoding/evm/constants.rs diff --git a/src/encoding/evm/constants.rs b/src/encoding/evm/constants.rs new file mode 100644 index 0000000..f0fa47d --- /dev/null +++ b/src/encoding/evm/constants.rs @@ -0,0 +1,10 @@ +use alloy_primitives::hex; +use lazy_static::lazy_static; +use tycho_core::Bytes; + +lazy_static! { + pub static ref NATIVE_ADDRESS: Bytes = + Bytes::from(hex!("0000000000000000000000000000000000000000").to_vec()); + pub static ref WETH_ADDRESS: Bytes = + Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec()); +} diff --git a/src/encoding/evm/mod.rs b/src/encoding/evm/mod.rs index bffb498..080f919 100644 --- a/src/encoding/evm/mod.rs +++ b/src/encoding/evm/mod.rs @@ -1,4 +1,5 @@ pub mod approvals; +mod constants; mod models; mod strategy_encoder; mod swap_encoder; diff --git a/src/encoding/evm/tycho_encoder.rs b/src/encoding/evm/tycho_encoder.rs index 5be718d..44c3f9f 100644 --- a/src/encoding/evm/tycho_encoder.rs +++ b/src/encoding/evm/tycho_encoder.rs @@ -5,6 +5,7 @@ use tycho_core::{models::Chain, Bytes}; use crate::encoding::{ errors::EncodingError, + evm::constants::{NATIVE_ADDRESS, WETH_ADDRESS}, models::{NativeAction, Solution, Transaction}, strategy_encoder::StrategySelector, tycho_encoder::TychoEncoder, @@ -38,11 +39,7 @@ impl TychoEncoder for EVMTychoEncoder { ) -> Result, EncodingError> { let mut transactions: Vec = Vec::new(); for solution in solutions.iter() { - if solution.exact_out { - return Err(EncodingError::FatalError( - "Currently only exact input solutions are supported".to_string(), - )); - } + self.validate_solution(solution)?; let router_address = solution .router_address @@ -70,15 +67,62 @@ impl TychoEncoder for EVMTychoEncoder { } Ok(transactions) } + + fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError> { + if solution.exact_out { + return Err(EncodingError::FatalError( + "Currently only exact input solutions are supported".to_string(), + )); + } + if solution.swaps.is_empty() { + return Err(EncodingError::FatalError("No swaps found in solution".to_string())); + } + if let Some(native_action) = solution.clone().native_action { + if native_action == NativeAction::Wrap { + if let Some(first_swap) = solution.swaps.first() { + if first_swap.token_in != *WETH_ADDRESS { + return Err(EncodingError::FatalError( + "WETH must be the first swap's input in order to wrap".to_string(), + )); + } + } + if solution.given_token != *NATIVE_ADDRESS { + return Err(EncodingError::FatalError( + "ETH must be the input token in order to wrap".to_string(), + )); + } + } else if native_action == NativeAction::Unwrap { + if let Some(last_swap) = solution.swaps.last() { + if last_swap.token_out != *WETH_ADDRESS { + return Err(EncodingError::FatalError( + "WETH must be the last swap's output in order to unwrap".to_string(), + )); + } + } + if solution.checked_token != *NATIVE_ADDRESS { + return Err(EncodingError::FatalError( + "ETH must be the output token in order to unwrap".to_string(), + )); + } + } + } + Ok(()) + } } #[cfg(test)] mod tests { + use tycho_core::dto::ProtocolComponent; + use super::*; - use crate::encoding::strategy_encoder::StrategyEncoder; + use crate::encoding::{models::Swap, strategy_encoder::StrategyEncoder}; struct MockStrategySelector; + fn dai() -> Bytes { + Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap() + } + impl StrategySelector for MockStrategySelector { fn select_strategy( &self, @@ -107,7 +151,7 @@ mod tests { } } - fn get_mocker_tycho_encoder() -> EVMTychoEncoder { + fn get_mocked_tycho_encoder() -> EVMTychoEncoder { let strategy_selector = MockStrategySelector; EVMTychoEncoder::new( strategy_selector, @@ -120,13 +164,25 @@ mod tests { #[test] fn test_encode_router_calldata() { - let encoder = get_mocker_tycho_encoder(); - + let encoder = get_mocked_tycho_encoder(); let eth_amount_in = BigUint::from(1000u32); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: WETH_ADDRESS.clone(), + token_out: dai(), + split: 0f64, + }; + let solution = Solution { exact_out: false, given_amount: eth_amount_in.clone(), + given_token: NATIVE_ADDRESS.clone(), router_address: None, + swaps: vec![swap], native_action: Some(NativeAction::Wrap), ..Default::default() }; @@ -142,15 +198,13 @@ mod tests { } #[test] - fn test_encode_router_calldata_fails_for_exact_out() { - let encoder = get_mocker_tycho_encoder(); - + fn test_validate_fails_for_exact_out() { + let encoder = get_mocked_tycho_encoder(); let solution = Solution { exact_out: true, // This should cause an error ..Default::default() }; - - let result = encoder.encode_router_calldata(vec![solution]); + let result = encoder.validate_solution(&solution); assert!(result.is_err()); assert_eq!( @@ -160,4 +214,212 @@ mod tests { ) ); } + + #[test] + fn test_validate_passes_for_wrap() { + let encoder = get_mocked_tycho_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: WETH_ADDRESS.clone(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: NATIVE_ADDRESS.clone(), + checked_token: dai(), + check_amount: None, + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_ok()); + } + + #[test] + fn test_validate_fails_for_wrap_wrong_input() { + let encoder = get_mocked_tycho_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: WETH_ADDRESS.clone(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: WETH_ADDRESS.clone(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError("ETH must be the input token in order to wrap".to_string()) + ); + } + + #[test] + fn test_validate_fails_for_wrap_wrong_first_swap() { + let encoder = get_mocked_tycho_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: NATIVE_ADDRESS.clone(), + token_out: dai(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: NATIVE_ADDRESS.clone(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "WETH must be the first swap's input in order to wrap".to_string() + ) + ); + } + + #[test] + fn test_validate_fails_no_swaps() { + let encoder = get_mocked_tycho_encoder(); + let solution = Solution { + exact_out: false, + given_token: NATIVE_ADDRESS.clone(), + swaps: vec![], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError("No swaps found in solution".to_string()) + ); + } + + #[test] + fn test_validate_passes_for_unwrap() { + let encoder = get_mocked_tycho_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: WETH_ADDRESS.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + checked_token: NATIVE_ADDRESS.clone(), + check_amount: None, + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_ok()); + } + + #[test] + fn test_validate_fails_for_unwrap_wrong_output() { + let encoder = get_mocked_tycho_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: WETH_ADDRESS.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: dai(), + checked_token: WETH_ADDRESS.clone(), + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "ETH must be the output token in order to unwrap".to_string() + ) + ); + } + + #[test] + fn test_validate_fails_for_unwrap_wrong_last_swap() { + let encoder = get_mocked_tycho_encoder(); + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai(), + token_out: NATIVE_ADDRESS.clone(), + split: 0f64, + }; + + let solution = Solution { + exact_out: false, + given_token: NATIVE_ADDRESS.clone(), + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + + let result = encoder.validate_solution(&solution); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + EncodingError::FatalError( + "WETH must be the last swap's output in order to unwrap".to_string() + ) + ); + } } diff --git a/src/encoding/tycho_encoder.rs b/src/encoding/tycho_encoder.rs index bfec250..0c92820 100644 --- a/src/encoding/tycho_encoder.rs +++ b/src/encoding/tycho_encoder.rs @@ -10,4 +10,6 @@ pub trait TychoEncoder { &self, solutions: Vec, ) -> Result, EncodingError>; + + fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError>; }