From 4bc615913ecb41a551a8b970ba5d96f0fc20ca42 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Fri, 31 Jan 2025 17:21:18 -0500 Subject: [PATCH 1/4] 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>; } From f873bb5187d8f0e24894c15d7ed189b27c5edcf7 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 4 Feb 2025 10:01:38 -0500 Subject: [PATCH 2/4] refactor: check given/checked tokens before swap tokens It's a more intuitive order, since these are higher-level attributes. Also remove the validation from the trait - it doesn't have to be part of the trait. --- src/encoding/evm/tycho_encoder.rs | 85 ++++++++++++++++--------------- src/encoding/tycho_encoder.rs | 2 - 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/encoding/evm/tycho_encoder.rs b/src/encoding/evm/tycho_encoder.rs index 44c3f9f..76fbd1d 100644 --- a/src/encoding/evm/tycho_encoder.rs +++ b/src/encoding/evm/tycho_encoder.rs @@ -32,6 +32,50 @@ impl EVMTychoEncoder { Ok(EVMTychoEncoder { strategy_selector, signer, chain, router_address }) } } + +impl EVMTychoEncoder { + 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 solution.given_token != *NATIVE_ADDRESS { + return Err(EncodingError::FatalError( + "ETH must be the input token in order to wrap".to_string(), + )); + } + 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(), + )); + } + } + } else if native_action == NativeAction::Unwrap { + if solution.checked_token != *NATIVE_ADDRESS { + return Err(EncodingError::FatalError( + "ETH must be the output token in order to unwrap".to_string(), + )); + } + 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(), + )); + } + } + } + } + Ok(()) + } +} + impl TychoEncoder for EVMTychoEncoder { fn encode_router_calldata( &self, @@ -67,47 +111,6 @@ 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)] diff --git a/src/encoding/tycho_encoder.rs b/src/encoding/tycho_encoder.rs index 0c92820..bfec250 100644 --- a/src/encoding/tycho_encoder.rs +++ b/src/encoding/tycho_encoder.rs @@ -10,6 +10,4 @@ pub trait TychoEncoder { &self, solutions: Vec, ) -> Result, EncodingError>; - - fn validate_solution(&self, solution: &Solution) -> Result<(), EncodingError>; } From 06603210bcd567ce50ec79344024ea2b722ebcd3 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 4 Feb 2025 10:28:16 -0500 Subject: [PATCH 3/4] fix: test_validate_fails_for_unwrap_wrong_last_swap ETH was the given token not the checked token, which was wrong. --- src/encoding/evm/tycho_encoder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/encoding/evm/tycho_encoder.rs b/src/encoding/evm/tycho_encoder.rs index 1ad1f6f..3006ee3 100644 --- a/src/encoding/evm/tycho_encoder.rs +++ b/src/encoding/evm/tycho_encoder.rs @@ -407,7 +407,7 @@ mod tests { let solution = Solution { exact_out: false, - given_token: NATIVE_ADDRESS.clone(), + checked_token: NATIVE_ADDRESS.clone(), swaps: vec![swap], native_action: Some(NativeAction::Unwrap), ..Default::default() From 00171ea72e980dcef509a884cfe874c873836563 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 4 Feb 2025 15:37:28 +0000 Subject: [PATCH 4/4] chore(release): 0.28.0 [skip ci] ## [0.28.0](https://github.com/propeller-heads/tycho-execution/compare/0.27.0...0.28.0) (2025-02-04) ### Features * Tycho encoder validation ([4bc6159](https://github.com/propeller-heads/tycho-execution/commit/4bc615913ecb41a551a8b970ba5d96f0fc20ca42)) ### Bug Fixes * test_validate_fails_for_unwrap_wrong_last_swap ([0660321](https://github.com/propeller-heads/tycho-execution/commit/06603210bcd567ce50ec79344024ea2b722ebcd3)) --- CHANGELOG.md | 12 ++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb75f6..cd9ec23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [0.28.0](https://github.com/propeller-heads/tycho-execution/compare/0.27.0...0.28.0) (2025-02-04) + + +### Features + +* Tycho encoder validation ([4bc6159](https://github.com/propeller-heads/tycho-execution/commit/4bc615913ecb41a551a8b970ba5d96f0fc20ca42)) + + +### Bug Fixes + +* test_validate_fails_for_unwrap_wrong_last_swap ([0660321](https://github.com/propeller-heads/tycho-execution/commit/06603210bcd567ce50ec79344024ea2b722ebcd3)) + ## [0.27.0](https://github.com/propeller-heads/tycho-execution/compare/0.26.0...0.27.0) (2025-02-04) diff --git a/Cargo.lock b/Cargo.lock index 484282a..1f04723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4163,7 +4163,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.27.0" +version = "0.28.0" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 958189f..6b66a70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.27.0" +version = "0.28.0" edition = "2021" [dependencies]