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] 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 4f02c0f..543ed21 100644 --- a/src/encoding/evm/mod.rs +++ b/src/encoding/evm/mod.rs @@ -1,4 +1,5 @@ -mod approvals; +pub mod approvals; +mod constants; mod models; pub mod strategy_encoder; mod swap_encoder; diff --git a/src/encoding/evm/tycho_encoder.rs b/src/encoding/evm/tycho_encoder.rs index 989f8f6..3006ee3 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, @@ -29,6 +30,50 @@ impl EVMTychoEncoder { Ok(EVMTychoEncoder { strategy_selector, signer_pk, 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, @@ -36,11 +81,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 @@ -72,11 +113,17 @@ impl TychoEncoder for EVMTychoEncoder { #[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, @@ -105,7 +152,7 @@ mod tests { } } - fn get_mocker_tycho_encoder() -> EVMTychoEncoder { + fn get_mocked_tycho_encoder() -> EVMTychoEncoder { let strategy_selector = MockStrategySelector; EVMTychoEncoder::new( strategy_selector, @@ -118,13 +165,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() }; @@ -140,15 +199,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!( @@ -158,4 +215,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, + checked_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() + ) + ); + } }