From f80ffa924f1da626bef0751c92c09fb133d2ba85 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Mon, 3 Feb 2025 21:24:44 +0530 Subject: [PATCH 01/13] feat: add validation for split swap --- .../evm/strategy_encoder/strategy_encoders.rs | 153 +++++++++++++++++- 1 file changed, 151 insertions(+), 2 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 8dc9eb7..59c359d 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1,4 +1,8 @@ -use std::{cmp::max, collections::HashSet, str::FromStr}; +use std::{ + cmp::max, + collections::{HashMap, HashSet, VecDeque}, + str::FromStr, +}; use alloy_primitives::{aliases::U24, FixedBytes, U256, U8}; use alloy_sol_types::SolValue; @@ -13,7 +17,7 @@ use crate::encoding::{ swap_encoder::SWAP_ENCODER_REGISTRY, utils::{biguint_to_u256, bytes_to_address, encode_input, percentage_to_uint24}, }, - models::{EncodingContext, NativeAction, Solution}, + models::{EncodingContext, NativeAction, Solution, Swap}, strategy_encoder::StrategyEncoder, }; @@ -71,6 +75,12 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { solution: Solution, router_address: Bytes, ) -> Result<(Vec, Bytes), EncodingError> { + validate_swaps(&solution.swaps)?; + validate_token_path_connectivity( + &solution.swaps, + &solution.given_token, + &solution.checked_token, + )?; let (permit, signature) = self.permit2.get_permit( &router_address, &solution.sender, @@ -203,6 +213,145 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { } } +fn validate_swaps(swaps: &[Swap]) -> Result<(), EncodingError> { + for swap in swaps { + if swap.split >= 1.0 { + return Err(EncodingError::InvalidInput(format!( + "Split percentage must be less than 1 (100%), got {}", + swap.split + ))); + } + } + + let mut swaps_by_token: HashMap> = HashMap::new(); + + for swap in swaps { + swaps_by_token + .entry(swap.token_in.clone()) + .or_default() + .push(swap); + } + + for (token, token_swaps) in swaps_by_token { + if token_swaps.is_empty() { + return Err(EncodingError::InvalidInput(format!( + "No swaps found for token {:?}", + token + ))); + } + + // Single swaps don't need remainder handling + if token_swaps.len() == 1 { + continue; + } + + // Check if exactly one swap has 0% split and it's the last one + let zero_splits: Vec<_> = token_swaps + .iter() + .enumerate() + .filter(|(_, s)| s.split == 0.0) + .collect(); + + if zero_splits.len() != 1 { + return Err(EncodingError::InvalidInput(format!( + "Token {:?} must have exactly one 0% split for remainder handling", + token + ))); + } + + if zero_splits[0].0 != token_swaps.len() - 1 { + return Err(EncodingError::InvalidInput(format!( + "The 0% split for token {:?} must be the last swap", + token + ))); + } + + // Sum non-zero splits and validate each is >0% and <100% + let mut total_percentage = 0.0; + for swap in token_swaps + .iter() + .take(token_swaps.len() - 1) + { + if swap.split <= 0.0 { + return Err(EncodingError::InvalidInput(format!( + "Non-remainder splits must be >0% for token {:?}", + token + ))); + } + total_percentage += swap.split; + } + + // Total must be <100% to leave room for remainder + if total_percentage >= 1.0 { + return Err(EncodingError::InvalidInput(format!( + "Total of non-remainder splits for token {:?} must be <100%, got {}%", + token, + total_percentage * 100.0 + ))); + } + } + + Ok(()) +} + +fn validate_token_path_connectivity( + swaps: &[Swap], + given_token: &Bytes, + checked_token: &Bytes, +) -> Result<(), EncodingError> { + // Build directed graph of token flows + let mut graph: HashMap> = HashMap::new(); + for swap in swaps { + graph + .entry(swap.token_in.clone()) + .or_default() + .insert(swap.token_out.clone()); + } + + // BFS from given_token + let mut visited = HashSet::new(); + let mut queue = VecDeque::new(); + queue.push_back(given_token.clone()); + + while let Some(token) = queue.pop_front() { + if !visited.insert(token.clone()) { + continue; + } + + if let Some(next_tokens) = graph.get(&token) { + for next_token in next_tokens { + if !visited.contains(next_token) { + queue.push_back(next_token.clone()); + } + } + } + } + + // Verify all tokens are visited + let all_tokens: HashSet<_> = graph + .keys() + .chain(graph.values().flat_map(|v| v.iter())) + .collect(); + + for token in all_tokens { + if !visited.contains(token) { + return Err(EncodingError::InvalidInput(format!( + "Token {:?} is not connected to the main path", + token + ))); + } + } + + // Verify checked_token is reachable + if !visited.contains(checked_token) { + return Err(EncodingError::InvalidInput( + "Checked token is not reachable through swap path".to_string(), + )); + } + + Ok(()) +} + /// This strategy encoder is used for solutions that are sent directly to the pool. /// Only 1 solution with 1 swap is supported. pub struct ExecutorStrategyEncoder {} From 95edd5b1fe99fd96163dcf74c2a570a7c8a480a1 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 4 Feb 2025 18:44:09 +0530 Subject: [PATCH 02/13] fix: checks in validations --- .../evm/strategy_encoder/strategy_encoders.rs | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 59c359d..26f61ad 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -242,26 +242,33 @@ fn validate_swaps(swaps: &[Swap]) -> Result<(), EncodingError> { // Single swaps don't need remainder handling if token_swaps.len() == 1 { + if token_swaps[0].split != 1.0 { + return Err(EncodingError::InvalidInput(format!( + "Single swap must have 100% split for token {:?}", + token + ))); + } continue; } // Check if exactly one swap has 0% split and it's the last one - let zero_splits: Vec<_> = token_swaps - .iter() - .enumerate() - .filter(|(_, s)| s.split == 0.0) - .collect(); - - if zero_splits.len() != 1 { - return Err(EncodingError::InvalidInput(format!( - "Token {:?} must have exactly one 0% split for remainder handling", - token - ))); + let mut found_zero_split = false; + for (i, swap) in token_swaps.iter().enumerate() { + match (swap.split == 0.0, i == token_swaps.len() - 1) { + (true, false) => { + return Err(EncodingError::InvalidInput(format!( + "The 0% split for token {:?} must be the last swap", + token + ))) + } + (true, true) => found_zero_split = true, + (false, _) => (), + } } - if zero_splits[0].0 != token_swaps.len() - 1 { + if !found_zero_split { return Err(EncodingError::InvalidInput(format!( - "The 0% split for token {:?} must be the last swap", + "Token {:?} must have exactly one 0% split for remainder handling", token ))); } @@ -300,56 +307,48 @@ fn validate_token_path_connectivity( checked_token: &Bytes, ) -> Result<(), EncodingError> { // Build directed graph of token flows - let mut graph: HashMap> = HashMap::new(); + let mut graph: HashMap<&Bytes, HashSet<&Bytes>> = HashMap::new(); for swap in swaps { graph - .entry(swap.token_in.clone()) + .entry(&swap.token_in) .or_default() - .insert(swap.token_out.clone()); + .insert(&swap.token_out); } // BFS from given_token let mut visited = HashSet::new(); let mut queue = VecDeque::new(); - queue.push_back(given_token.clone()); + queue.push_back(given_token); while let Some(token) = queue.pop_front() { - if !visited.insert(token.clone()) { + if !visited.insert(token) { continue; } - if let Some(next_tokens) = graph.get(&token) { - for next_token in next_tokens { + // Early success check + if token == checked_token && visited.len() == graph.len() + 1 { + return Ok(()); + } + + if let Some(next_tokens) = graph.get(token) { + for &next_token in next_tokens { if !visited.contains(next_token) { - queue.push_back(next_token.clone()); + queue.push_back(next_token); } } } } - // Verify all tokens are visited - let all_tokens: HashSet<_> = graph - .keys() - .chain(graph.values().flat_map(|v| v.iter())) - .collect(); - - for token in all_tokens { - if !visited.contains(token) { - return Err(EncodingError::InvalidInput(format!( - "Token {:?} is not connected to the main path", - token - ))); - } - } - - // Verify checked_token is reachable + // If we get here, either checked_token wasn't reached or not all tokens were visited if !visited.contains(checked_token) { - return Err(EncodingError::InvalidInput( + Err(EncodingError::InvalidInput( "Checked token is not reachable through swap path".to_string(), - )); + )) + } else { + Err(EncodingError::InvalidInput( + "Some tokens are not connected to the main path".to_string(), + )) } - - Ok(()) } /// This strategy encoder is used for solutions that are sent directly to the pool. From b69aef9b8f1d253bb465a39669bd18aa5f355aa5 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 4 Feb 2025 19:08:17 +0530 Subject: [PATCH 03/13] feat: add tests for split swap validations --- .../evm/strategy_encoder/strategy_encoders.rs | 275 +++++++++++++++++- 1 file changed, 273 insertions(+), 2 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 26f61ad..0de3fb8 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -242,9 +242,9 @@ fn validate_swaps(swaps: &[Swap]) -> Result<(), EncodingError> { // Single swaps don't need remainder handling if token_swaps.len() == 1 { - if token_swaps[0].split != 1.0 { + if token_swaps[0].split != 0.0 { return Err(EncodingError::InvalidInput(format!( - "Single swap must have 100% split for token {:?}", + "Single swap must have 0% split for token {:?}", token ))); } @@ -779,4 +779,275 @@ mod tests { let _hex_calldata = encode(&calldata); println!("{}", _hex_calldata); } + + #[test] + fn test_validate_token_path_connectivity_single_swap() { + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + let swaps = vec![Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0f64, + }]; + let result = validate_token_path_connectivity(&swaps, &weth, &dai); + assert_eq!(result, Ok(())); + } + + #[test] + fn test_validate_token_path_connectivity_multiple_swaps() { + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let swaps = vec![ + Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.5f64, + }, + Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai.clone(), + token_out: usdc.clone(), + split: 0f64, + }, + ]; + let result = validate_token_path_connectivity(&swaps, &weth, &usdc); + assert_eq!(result, Ok(())); + } + + #[test] + fn test_validate_token_path_connectivity_multiple_swaps_failure() { + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); + + // Test case 1: Disconnected path + let disconnected_swaps = vec![ + Swap { + component: ProtocolComponent { + id: "pool1".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.5, + }, + // This swap is disconnected from the WETH->DAI path + Swap { + component: ProtocolComponent { + id: "pool2".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: wbtc.clone(), + token_out: usdc.clone(), + split: 0.0, + }, + ]; + let result = validate_token_path_connectivity(&disconnected_swaps, &weth, &usdc); + assert!(matches!( + result, + Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") + )); + + // Test case 2: Unreachable checked token + let unreachable_swaps = vec![Swap { + component: ProtocolComponent { + id: "pool1".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 1.0, + }]; + let result = validate_token_path_connectivity(&unreachable_swaps, &weth, &usdc); + assert!(matches!( + result, + Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") + )); + + // Test case 3: Empty swaps + let empty_swaps: Vec = vec![]; + let result = validate_token_path_connectivity(&empty_swaps, &weth, &usdc); + assert!(matches!( + result, + Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") + )); + } + + #[test] + fn test_validate_swaps_single_swap() { + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + let swaps = vec![Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0f64, + }]; + let result = validate_swaps(&swaps); + assert_eq!(result, Ok(())); + } + + #[test] + fn test_validate_swaps_multiple_swaps() { + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + // Valid case: Multiple swaps with proper splits (50%, 30%, remainder) + let valid_swaps = vec![ + Swap { + component: ProtocolComponent { + id: "pool1".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.5, + }, + Swap { + component: ProtocolComponent { + id: "pool2".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.3, + }, + Swap { + component: ProtocolComponent { + id: "pool3".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.0, // Remainder (20%) + }, + ]; + assert!(validate_swaps(&valid_swaps).is_ok()); + } + + #[test] + fn test_validate_swaps_multiple_swaps_failures() { + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + // Test case 1: Invalid - splits sum to 100% with no remainder + let invalid_total_swaps = vec![ + Swap { + component: ProtocolComponent { + id: "pool1".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.7, + }, + Swap { + component: ProtocolComponent { + id: "pool2".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.3, + }, + ]; + assert!(matches!( + validate_swaps(&invalid_total_swaps), + Err(EncodingError::InvalidInput(msg)) if msg.contains("must have exactly one 0% split") + )); + + // Test case 2: Invalid - zero split not at end + let invalid_zero_position_swaps = vec![ + Swap { + component: ProtocolComponent { + id: "pool1".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.0, + }, + Swap { + component: ProtocolComponent { + id: "pool2".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.5, + }, + ]; + assert!(matches!( + validate_swaps(&invalid_zero_position_swaps), + Err(EncodingError::InvalidInput(msg)) if msg.contains("must be the last swap") + )); + + // Test case 3: Invalid - splits exceed 100% + let invalid_overflow_swaps = vec![ + Swap { + component: ProtocolComponent { + id: "pool1".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.6, + }, + Swap { + component: ProtocolComponent { + id: "pool2".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.5, + }, + Swap { + component: ProtocolComponent { + id: "pool3".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: dai.clone(), + split: 0.0, + }, + ]; + assert!(matches!( + validate_swaps(&invalid_overflow_swaps), + Err(EncodingError::InvalidInput(msg)) if msg.contains("must be <100%") + )); + } } From b8013c6e7e3cc2d3190529ea1efb661bfa099dff Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 4 Feb 2025 21:25:06 +0530 Subject: [PATCH 04/13] chore: move validation methods inside impl --- .../evm/strategy_encoder/strategy_encoders.rs | 316 +++++++++--------- 1 file changed, 166 insertions(+), 150 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 0de3fb8..070b166 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -67,6 +67,145 @@ impl SplitSwapStrategyEncoder { let selector = "swap(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string(); Ok(Self { permit2: Permit2::new(signer_pk, chain)?, selector }) } + + fn validate_swaps(&self, swaps: &[Swap]) -> Result<(), EncodingError> { + for swap in swaps { + if swap.split >= 1.0 { + return Err(EncodingError::InvalidInput(format!( + "Split percentage must be less than 1 (100%), got {}", + swap.split + ))); + } + } + + let mut swaps_by_token: HashMap> = HashMap::new(); + + for swap in swaps { + swaps_by_token + .entry(swap.token_in.clone()) + .or_default() + .push(swap); + } + + for (token, token_swaps) in swaps_by_token { + if token_swaps.is_empty() { + return Err(EncodingError::InvalidInput(format!( + "No swaps found for token {:?}", + token + ))); + } + + // Single swaps don't need remainder handling + if token_swaps.len() == 1 { + if token_swaps[0].split != 0.0 { + return Err(EncodingError::InvalidInput(format!( + "Single swap must have 0% split for token {:?}", + token + ))); + } + continue; + } + + // Check if exactly one swap has 0% split and it's the last one + let mut found_zero_split = false; + for (i, swap) in token_swaps.iter().enumerate() { + match (swap.split == 0.0, i == token_swaps.len() - 1) { + (true, false) => { + return Err(EncodingError::InvalidInput(format!( + "The 0% split for token {:?} must be the last swap", + token + ))) + } + (true, true) => found_zero_split = true, + (false, _) => (), + } + } + + if !found_zero_split { + return Err(EncodingError::InvalidInput(format!( + "Token {:?} must have exactly one 0% split for remainder handling", + token + ))); + } + + // Sum non-zero splits and validate each is >0% and <100% + let mut total_percentage = 0.0; + for swap in token_swaps + .iter() + .take(token_swaps.len() - 1) + { + if swap.split <= 0.0 { + return Err(EncodingError::InvalidInput(format!( + "Non-remainder splits must be >0% for token {:?}", + token + ))); + } + total_percentage += swap.split; + } + + // Total must be <100% to leave room for remainder + if total_percentage >= 1.0 { + return Err(EncodingError::InvalidInput(format!( + "Total of non-remainder splits for token {:?} must be <100%, got {}%", + token, + total_percentage * 100.0 + ))); + } + } + + Ok(()) + } + + fn validate_token_path_connectivity( + &self, + swaps: &[Swap], + given_token: &Bytes, + checked_token: &Bytes, + ) -> Result<(), EncodingError> { + // Build directed graph of token flows + let mut graph: HashMap<&Bytes, HashSet<&Bytes>> = HashMap::new(); + for swap in swaps { + graph + .entry(&swap.token_in) + .or_default() + .insert(&swap.token_out); + } + + // BFS from given_token + let mut visited = HashSet::new(); + let mut queue = VecDeque::new(); + queue.push_back(given_token); + + while let Some(token) = queue.pop_front() { + if !visited.insert(token) { + continue; + } + + // Early success check + if token == checked_token && visited.len() == graph.len() + 1 { + return Ok(()); + } + + if let Some(next_tokens) = graph.get(token) { + for &next_token in next_tokens { + if !visited.contains(next_token) { + queue.push_back(next_token); + } + } + } + } + + // If we get here, either checked_token wasn't reached or not all tokens were visited + if !visited.contains(checked_token) { + Err(EncodingError::InvalidInput( + "Checked token is not reachable through swap path".to_string(), + )) + } else { + Err(EncodingError::InvalidInput( + "Some tokens are not connected to the main path".to_string(), + )) + } + } } impl EVMStrategyEncoder for SplitSwapStrategyEncoder {} impl StrategyEncoder for SplitSwapStrategyEncoder { @@ -75,8 +214,8 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { solution: Solution, router_address: Bytes, ) -> Result<(Vec, Bytes), EncodingError> { - validate_swaps(&solution.swaps)?; - validate_token_path_connectivity( + self.validate_swaps(&solution.swaps)?; + self.validate_token_path_connectivity( &solution.swaps, &solution.given_token, &solution.checked_token, @@ -213,144 +352,6 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { } } -fn validate_swaps(swaps: &[Swap]) -> Result<(), EncodingError> { - for swap in swaps { - if swap.split >= 1.0 { - return Err(EncodingError::InvalidInput(format!( - "Split percentage must be less than 1 (100%), got {}", - swap.split - ))); - } - } - - let mut swaps_by_token: HashMap> = HashMap::new(); - - for swap in swaps { - swaps_by_token - .entry(swap.token_in.clone()) - .or_default() - .push(swap); - } - - for (token, token_swaps) in swaps_by_token { - if token_swaps.is_empty() { - return Err(EncodingError::InvalidInput(format!( - "No swaps found for token {:?}", - token - ))); - } - - // Single swaps don't need remainder handling - if token_swaps.len() == 1 { - if token_swaps[0].split != 0.0 { - return Err(EncodingError::InvalidInput(format!( - "Single swap must have 0% split for token {:?}", - token - ))); - } - continue; - } - - // Check if exactly one swap has 0% split and it's the last one - let mut found_zero_split = false; - for (i, swap) in token_swaps.iter().enumerate() { - match (swap.split == 0.0, i == token_swaps.len() - 1) { - (true, false) => { - return Err(EncodingError::InvalidInput(format!( - "The 0% split for token {:?} must be the last swap", - token - ))) - } - (true, true) => found_zero_split = true, - (false, _) => (), - } - } - - if !found_zero_split { - return Err(EncodingError::InvalidInput(format!( - "Token {:?} must have exactly one 0% split for remainder handling", - token - ))); - } - - // Sum non-zero splits and validate each is >0% and <100% - let mut total_percentage = 0.0; - for swap in token_swaps - .iter() - .take(token_swaps.len() - 1) - { - if swap.split <= 0.0 { - return Err(EncodingError::InvalidInput(format!( - "Non-remainder splits must be >0% for token {:?}", - token - ))); - } - total_percentage += swap.split; - } - - // Total must be <100% to leave room for remainder - if total_percentage >= 1.0 { - return Err(EncodingError::InvalidInput(format!( - "Total of non-remainder splits for token {:?} must be <100%, got {}%", - token, - total_percentage * 100.0 - ))); - } - } - - Ok(()) -} - -fn validate_token_path_connectivity( - swaps: &[Swap], - given_token: &Bytes, - checked_token: &Bytes, -) -> Result<(), EncodingError> { - // Build directed graph of token flows - let mut graph: HashMap<&Bytes, HashSet<&Bytes>> = HashMap::new(); - for swap in swaps { - graph - .entry(&swap.token_in) - .or_default() - .insert(&swap.token_out); - } - - // BFS from given_token - let mut visited = HashSet::new(); - let mut queue = VecDeque::new(); - queue.push_back(given_token); - - while let Some(token) = queue.pop_front() { - if !visited.insert(token) { - continue; - } - - // Early success check - if token == checked_token && visited.len() == graph.len() + 1 { - return Ok(()); - } - - if let Some(next_tokens) = graph.get(token) { - for &next_token in next_tokens { - if !visited.contains(next_token) { - queue.push_back(next_token); - } - } - } - } - - // If we get here, either checked_token wasn't reached or not all tokens were visited - if !visited.contains(checked_token) { - Err(EncodingError::InvalidInput( - "Checked token is not reachable through swap path".to_string(), - )) - } else { - Err(EncodingError::InvalidInput( - "Some tokens are not connected to the main path".to_string(), - )) - } -} - /// This strategy encoder is used for solutions that are sent directly to the pool. /// Only 1 solution with 1 swap is supported. pub struct ExecutorStrategyEncoder {} @@ -780,8 +781,15 @@ mod tests { println!("{}", _hex_calldata); } + fn get_mock_split_swap_strategy_encoder() -> SplitSwapStrategyEncoder { + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + SplitSwapStrategyEncoder::new(private_key, Chain::Ethereum).unwrap() + } + #[test] fn test_validate_token_path_connectivity_single_swap() { + let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let swaps = vec![Swap { @@ -794,12 +802,13 @@ mod tests { token_out: dai.clone(), split: 0f64, }]; - let result = validate_token_path_connectivity(&swaps, &weth, &dai); + let result = encoder.validate_token_path_connectivity(&swaps, &weth, &dai); assert_eq!(result, Ok(())); } #[test] fn test_validate_token_path_connectivity_multiple_swaps() { + let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); @@ -825,12 +834,13 @@ mod tests { split: 0f64, }, ]; - let result = validate_token_path_connectivity(&swaps, &weth, &usdc); + let result = encoder.validate_token_path_connectivity(&swaps, &weth, &usdc); assert_eq!(result, Ok(())); } #[test] fn test_validate_token_path_connectivity_multiple_swaps_failure() { + let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); @@ -860,7 +870,7 @@ mod tests { split: 0.0, }, ]; - let result = validate_token_path_connectivity(&disconnected_swaps, &weth, &usdc); + let result = encoder.validate_token_path_connectivity(&disconnected_swaps, &weth, &usdc); assert!(matches!( result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") @@ -877,7 +887,7 @@ mod tests { token_out: dai.clone(), split: 1.0, }]; - let result = validate_token_path_connectivity(&unreachable_swaps, &weth, &usdc); + let result = encoder.validate_token_path_connectivity(&unreachable_swaps, &weth, &usdc); assert!(matches!( result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") @@ -885,7 +895,7 @@ mod tests { // Test case 3: Empty swaps let empty_swaps: Vec = vec![]; - let result = validate_token_path_connectivity(&empty_swaps, &weth, &usdc); + let result = encoder.validate_token_path_connectivity(&empty_swaps, &weth, &usdc); assert!(matches!( result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") @@ -894,6 +904,7 @@ mod tests { #[test] fn test_validate_swaps_single_swap() { + let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let swaps = vec![Swap { @@ -906,7 +917,7 @@ mod tests { token_out: dai.clone(), split: 0f64, }]; - let result = validate_swaps(&swaps); + let result = encoder.validate_swaps(&swaps); assert_eq!(result, Ok(())); } @@ -948,7 +959,10 @@ mod tests { split: 0.0, // Remainder (20%) }, ]; - assert!(validate_swaps(&valid_swaps).is_ok()); + let encoder = get_mock_split_swap_strategy_encoder(); + assert!(encoder + .validate_swaps(&valid_swaps) + .is_ok()); } #[test] @@ -979,8 +993,9 @@ mod tests { split: 0.3, }, ]; + let encoder = get_mock_split_swap_strategy_encoder(); assert!(matches!( - validate_swaps(&invalid_total_swaps), + encoder.validate_swaps(&invalid_total_swaps), Err(EncodingError::InvalidInput(msg)) if msg.contains("must have exactly one 0% split") )); @@ -1008,7 +1023,7 @@ mod tests { }, ]; assert!(matches!( - validate_swaps(&invalid_zero_position_swaps), + encoder.validate_swaps(&invalid_zero_position_swaps), Err(EncodingError::InvalidInput(msg)) if msg.contains("must be the last swap") )); @@ -1045,8 +1060,9 @@ mod tests { split: 0.0, }, ]; + let encoder = get_mock_split_swap_strategy_encoder(); assert!(matches!( - validate_swaps(&invalid_overflow_swaps), + encoder.validate_swaps(&invalid_overflow_swaps), Err(EncodingError::InvalidInput(msg)) if msg.contains("must be <100%") )); } From b0dda205b09f8be0d97a1a68b4cd477a9ab5fc09 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 4 Feb 2025 21:40:50 +0530 Subject: [PATCH 05/13] chore: break tests --- .../evm/strategy_encoder/strategy_encoders.rs | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 070b166..65ea61c 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -69,6 +69,7 @@ impl SplitSwapStrategyEncoder { } fn validate_swaps(&self, swaps: &[Swap]) -> Result<(), EncodingError> { + let mut swaps_by_token: HashMap> = HashMap::new(); for swap in swaps { if swap.split >= 1.0 { return Err(EncodingError::InvalidInput(format!( @@ -76,11 +77,6 @@ impl SplitSwapStrategyEncoder { swap.split ))); } - } - - let mut swaps_by_token: HashMap> = HashMap::new(); - - for swap in swaps { swaps_by_token .entry(swap.token_in.clone()) .or_default() @@ -88,13 +84,6 @@ impl SplitSwapStrategyEncoder { } for (token, token_swaps) in swaps_by_token { - if token_swaps.is_empty() { - return Err(EncodingError::InvalidInput(format!( - "No swaps found for token {:?}", - token - ))); - } - // Single swaps don't need remainder handling if token_swaps.len() == 1 { if token_swaps[0].split != 0.0 { @@ -839,14 +828,13 @@ mod tests { } #[test] - fn test_validate_token_path_connectivity_multiple_swaps_failure() { + fn test_validate_token_path_connectivity_disconnected_path() { let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap(); - // Test case 1: Disconnected path let disconnected_swaps = vec![ Swap { component: ProtocolComponent { @@ -875,8 +863,15 @@ mod tests { result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") )); + } + + #[test] + fn test_validate_token_path_connectivity_unreachable_checked_token() { + let encoder = get_mock_split_swap_strategy_encoder(); + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - // Test case 2: Unreachable checked token let unreachable_swaps = vec![Swap { component: ProtocolComponent { id: "pool1".to_string(), @@ -892,8 +887,14 @@ mod tests { result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") )); + } + + #[test] + fn test_validate_token_path_connectivity_empty_swaps() { + let encoder = get_mock_split_swap_strategy_encoder(); + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); - // Test case 3: Empty swaps let empty_swaps: Vec = vec![]; let result = encoder.validate_token_path_connectivity(&empty_swaps, &weth, &usdc); assert!(matches!( @@ -966,11 +967,10 @@ mod tests { } #[test] - fn test_validate_swaps_multiple_swaps_failures() { + fn test_validate_swaps_no_remainder_split() { let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - // Test case 1: Invalid - splits sum to 100% with no remainder let invalid_total_swaps = vec![ Swap { component: ProtocolComponent { @@ -998,8 +998,13 @@ mod tests { encoder.validate_swaps(&invalid_total_swaps), Err(EncodingError::InvalidInput(msg)) if msg.contains("must have exactly one 0% split") )); + } + + #[test] + fn test_validate_swaps_zero_split_not_at_end() { + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - // Test case 2: Invalid - zero split not at end let invalid_zero_position_swaps = vec![ Swap { component: ProtocolComponent { @@ -1022,12 +1027,18 @@ mod tests { split: 0.5, }, ]; + let encoder = get_mock_split_swap_strategy_encoder(); assert!(matches!( encoder.validate_swaps(&invalid_zero_position_swaps), Err(EncodingError::InvalidInput(msg)) if msg.contains("must be the last swap") )); + } + + #[test] + fn test_validate_swaps_splits_exceed_hundred_percent() { + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); - // Test case 3: Invalid - splits exceed 100% let invalid_overflow_swaps = vec![ Swap { component: ProtocolComponent { From 4d97c3f16d263c975f1f42bebae9666af789eb10 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 4 Feb 2025 22:18:03 +0530 Subject: [PATCH 06/13] feat: add tests for wrap unwrap case --- .../evm/strategy_encoder/strategy_encoders.rs | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 65ea61c..ec19d1b 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -151,6 +151,19 @@ impl SplitSwapStrategyEncoder { given_token: &Bytes, checked_token: &Bytes, ) -> Result<(), EncodingError> { + // Special case: If given_token is ETH or checked_token is ETH, treat it as WETH for path + // validation + let eth_address = Bytes::from_str("0x0000000000000000000000000000000000000000") + .map_err(|_| EncodingError::FatalError("Invalid ETH address".to_string()))?; + let weth_address = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") + .map_err(|_| EncodingError::FatalError("Invalid WETH address".to_string()))?; + + let validation_given = + if given_token == ð_address { &weth_address } else { given_token }; + + let validation_checked = + if checked_token == ð_address { &weth_address } else { checked_token }; + // Build directed graph of token flows let mut graph: HashMap<&Bytes, HashSet<&Bytes>> = HashMap::new(); for swap in swaps { @@ -160,10 +173,10 @@ impl SplitSwapStrategyEncoder { .insert(&swap.token_out); } - // BFS from given_token + // BFS from validation_given let mut visited = HashSet::new(); let mut queue = VecDeque::new(); - queue.push_back(given_token); + queue.push_back(validation_given); while let Some(token) = queue.pop_front() { if !visited.insert(token) { @@ -171,7 +184,7 @@ impl SplitSwapStrategyEncoder { } // Early success check - if token == checked_token && visited.len() == graph.len() + 1 { + if token == validation_checked && visited.len() == graph.len() + 1 { return Ok(()); } @@ -185,7 +198,7 @@ impl SplitSwapStrategyEncoder { } // If we get here, either checked_token wasn't reached or not all tokens were visited - if !visited.contains(checked_token) { + if !visited.contains(validation_checked) { Err(EncodingError::InvalidInput( "Checked token is not reachable through swap path".to_string(), )) @@ -1077,4 +1090,50 @@ mod tests { Err(EncodingError::InvalidInput(msg)) if msg.contains("must be <100%") )); } + + #[test] + fn test_validate_token_path_connectivity_wrap_eth_i() { + let encoder = get_mock_split_swap_strategy_encoder(); + + let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); + + let swaps = vec![Swap { + component: ProtocolComponent { + id: "pool1".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: usdc.clone(), + split: 0f64, + }]; + + let result = encoder.validate_token_path_connectivity(&swaps, ð, &usdc); + assert_eq!(result, Ok(())); + } + + #[test] + fn test_validate_token_path_connectivity_wrap_eth_checked_token() { + let encoder = get_mock_split_swap_strategy_encoder(); + + let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let weth = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); + + let swaps = vec![Swap { + component: ProtocolComponent { + id: "pool1".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: usdc.clone(), + token_out: weth.clone(), + split: 0f64, + }]; + + let result = encoder.validate_token_path_connectivity(&swaps, &usdc, ð); + assert_eq!(result, Ok(())); + } } From 4da50ddc9e7d7cb35ed37403b4a745ba9a87cc0b Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 4 Feb 2025 22:32:05 +0530 Subject: [PATCH 07/13] chore: naming --- .../evm/strategy_encoder/strategy_encoders.rs | 68 ++++++++----------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index ec19d1b..c0f422a 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -13,7 +13,7 @@ use crate::encoding::{ errors::EncodingError, evm::{ approvals::permit2::Permit2, - constants::WETH_ADDRESS, + constants::{NATIVE_ADDRESS, WETH_ADDRESS}, swap_encoder::SWAP_ENCODER_REGISTRY, utils::{biguint_to_u256, bytes_to_address, encode_input, percentage_to_uint24}, }, @@ -68,7 +68,7 @@ impl SplitSwapStrategyEncoder { Ok(Self { permit2: Permit2::new(signer_pk, chain)?, selector }) } - fn validate_swaps(&self, swaps: &[Swap]) -> Result<(), EncodingError> { + fn validate_split_percentages(&self, swaps: &[Swap]) -> Result<(), EncodingError> { let mut swaps_by_token: HashMap> = HashMap::new(); for swap in swaps { if swap.split >= 1.0 { @@ -145,7 +145,7 @@ impl SplitSwapStrategyEncoder { Ok(()) } - fn validate_token_path_connectivity( + fn validate_swap_path( &self, swaps: &[Swap], given_token: &Bytes, @@ -153,16 +153,10 @@ impl SplitSwapStrategyEncoder { ) -> Result<(), EncodingError> { // Special case: If given_token is ETH or checked_token is ETH, treat it as WETH for path // validation - let eth_address = Bytes::from_str("0x0000000000000000000000000000000000000000") - .map_err(|_| EncodingError::FatalError("Invalid ETH address".to_string()))?; - let weth_address = Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") - .map_err(|_| EncodingError::FatalError("Invalid WETH address".to_string()))?; + let given_token = if *given_token == *NATIVE_ADDRESS { &WETH_ADDRESS } else { given_token }; - let validation_given = - if given_token == ð_address { &weth_address } else { given_token }; - - let validation_checked = - if checked_token == ð_address { &weth_address } else { checked_token }; + let checked_token = + if *checked_token == *NATIVE_ADDRESS { &WETH_ADDRESS } else { checked_token }; // Build directed graph of token flows let mut graph: HashMap<&Bytes, HashSet<&Bytes>> = HashMap::new(); @@ -176,7 +170,7 @@ impl SplitSwapStrategyEncoder { // BFS from validation_given let mut visited = HashSet::new(); let mut queue = VecDeque::new(); - queue.push_back(validation_given); + queue.push_back(given_token); while let Some(token) = queue.pop_front() { if !visited.insert(token) { @@ -184,7 +178,7 @@ impl SplitSwapStrategyEncoder { } // Early success check - if token == validation_checked && visited.len() == graph.len() + 1 { + if token == checked_token && visited.len() == graph.len() + 1 { return Ok(()); } @@ -198,7 +192,7 @@ impl SplitSwapStrategyEncoder { } // If we get here, either checked_token wasn't reached or not all tokens were visited - if !visited.contains(validation_checked) { + if !visited.contains(checked_token) { Err(EncodingError::InvalidInput( "Checked token is not reachable through swap path".to_string(), )) @@ -216,12 +210,8 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { solution: Solution, router_address: Bytes, ) -> Result<(Vec, Bytes), EncodingError> { - self.validate_swaps(&solution.swaps)?; - self.validate_token_path_connectivity( - &solution.swaps, - &solution.given_token, - &solution.checked_token, - )?; + self.validate_split_percentages(&solution.swaps)?; + self.validate_swap_path(&solution.swaps, &solution.given_token, &solution.checked_token)?; let (permit, signature) = self.permit2.get_permit( &router_address, &solution.sender, @@ -790,7 +780,7 @@ mod tests { } #[test] - fn test_validate_token_path_connectivity_single_swap() { + fn test_validate_path_single_swap() { let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); @@ -804,12 +794,12 @@ mod tests { token_out: dai.clone(), split: 0f64, }]; - let result = encoder.validate_token_path_connectivity(&swaps, &weth, &dai); + let result = encoder.validate_swap_path(&swaps, &weth, &dai); assert_eq!(result, Ok(())); } #[test] - fn test_validate_token_path_connectivity_multiple_swaps() { + fn test_validate_path_multiple_swaps() { let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); @@ -836,12 +826,12 @@ mod tests { split: 0f64, }, ]; - let result = encoder.validate_token_path_connectivity(&swaps, &weth, &usdc); + let result = encoder.validate_swap_path(&swaps, &weth, &usdc); assert_eq!(result, Ok(())); } #[test] - fn test_validate_token_path_connectivity_disconnected_path() { + fn test_validate_path_disconnected() { let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); @@ -871,7 +861,7 @@ mod tests { split: 0.0, }, ]; - let result = encoder.validate_token_path_connectivity(&disconnected_swaps, &weth, &usdc); + let result = encoder.validate_swap_path(&disconnected_swaps, &weth, &usdc); assert!(matches!( result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") @@ -879,7 +869,7 @@ mod tests { } #[test] - fn test_validate_token_path_connectivity_unreachable_checked_token() { + fn test_validate_path_unreachable_checked_token() { let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); @@ -895,7 +885,7 @@ mod tests { token_out: dai.clone(), split: 1.0, }]; - let result = encoder.validate_token_path_connectivity(&unreachable_swaps, &weth, &usdc); + let result = encoder.validate_swap_path(&unreachable_swaps, &weth, &usdc); assert!(matches!( result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") @@ -903,13 +893,13 @@ mod tests { } #[test] - fn test_validate_token_path_connectivity_empty_swaps() { + fn test_validate_path_empty_swaps() { let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let empty_swaps: Vec = vec![]; - let result = encoder.validate_token_path_connectivity(&empty_swaps, &weth, &usdc); + let result = encoder.validate_swap_path(&empty_swaps, &weth, &usdc); assert!(matches!( result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") @@ -931,7 +921,7 @@ mod tests { token_out: dai.clone(), split: 0f64, }]; - let result = encoder.validate_swaps(&swaps); + let result = encoder.validate_split_percentages(&swaps); assert_eq!(result, Ok(())); } @@ -975,7 +965,7 @@ mod tests { ]; let encoder = get_mock_split_swap_strategy_encoder(); assert!(encoder - .validate_swaps(&valid_swaps) + .validate_split_percentages(&valid_swaps) .is_ok()); } @@ -1008,7 +998,7 @@ mod tests { ]; let encoder = get_mock_split_swap_strategy_encoder(); assert!(matches!( - encoder.validate_swaps(&invalid_total_swaps), + encoder.validate_split_percentages(&invalid_total_swaps), Err(EncodingError::InvalidInput(msg)) if msg.contains("must have exactly one 0% split") )); } @@ -1042,7 +1032,7 @@ mod tests { ]; let encoder = get_mock_split_swap_strategy_encoder(); assert!(matches!( - encoder.validate_swaps(&invalid_zero_position_swaps), + encoder.validate_split_percentages(&invalid_zero_position_swaps), Err(EncodingError::InvalidInput(msg)) if msg.contains("must be the last swap") )); } @@ -1086,13 +1076,13 @@ mod tests { ]; let encoder = get_mock_split_swap_strategy_encoder(); assert!(matches!( - encoder.validate_swaps(&invalid_overflow_swaps), + encoder.validate_split_percentages(&invalid_overflow_swaps), Err(EncodingError::InvalidInput(msg)) if msg.contains("must be <100%") )); } #[test] - fn test_validate_token_path_connectivity_wrap_eth_i() { + fn test_validate_path_wrap_eth_given_token() { let encoder = get_mock_split_swap_strategy_encoder(); let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(); @@ -1110,7 +1100,7 @@ mod tests { split: 0f64, }]; - let result = encoder.validate_token_path_connectivity(&swaps, ð, &usdc); + let result = encoder.validate_swap_path(&swaps, ð, &usdc); assert_eq!(result, Ok(())); } @@ -1133,7 +1123,7 @@ mod tests { split: 0f64, }]; - let result = encoder.validate_token_path_connectivity(&swaps, &usdc, ð); + let result = encoder.validate_swap_path(&swaps, &usdc, ð); assert_eq!(result, Ok(())); } } From 2f0013a9344e18d3107d8a9ac51b9576e4338979 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Tue, 4 Feb 2025 22:33:34 +0530 Subject: [PATCH 08/13] chore: naming --- src/encoding/evm/strategy_encoder/strategy_encoders.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index c0f422a..620ecfa 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -907,7 +907,7 @@ mod tests { } #[test] - fn test_validate_swaps_single_swap() { + fn test_validate_swap_single() { let encoder = get_mock_split_swap_strategy_encoder(); let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); @@ -926,7 +926,7 @@ mod tests { } #[test] - fn test_validate_swaps_multiple_swaps() { + fn test_validate_swaps_multiple() { let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); From c787f5e722ad8a9f9a24e6ea09f59dfcf5f82239 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 00:14:09 +0530 Subject: [PATCH 09/13] fix: use native action to validate path --- .../evm/strategy_encoder/strategy_encoders.rs | 71 +++++++++++-------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 620ecfa..508f0c0 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -95,8 +95,8 @@ impl SplitSwapStrategyEncoder { continue; } - // Check if exactly one swap has 0% split and it's the last one let mut found_zero_split = false; + let mut total_percentage = 0.0; for (i, swap) in token_swaps.iter().enumerate() { match (swap.split == 0.0, i == token_swaps.len() - 1) { (true, false) => { @@ -106,7 +106,15 @@ impl SplitSwapStrategyEncoder { ))) } (true, true) => found_zero_split = true, - (false, _) => (), + (false, _) => { + if swap.split <= 0.0 { + return Err(EncodingError::InvalidInput(format!( + "Non-remainder splits must be >0% for token {:?}", + token + ))); + } + total_percentage += swap.split; + } } } @@ -117,21 +125,6 @@ impl SplitSwapStrategyEncoder { ))); } - // Sum non-zero splits and validate each is >0% and <100% - let mut total_percentage = 0.0; - for swap in token_swaps - .iter() - .take(token_swaps.len() - 1) - { - if swap.split <= 0.0 { - return Err(EncodingError::InvalidInput(format!( - "Non-remainder splits must be >0% for token {:?}", - token - ))); - } - total_percentage += swap.split; - } - // Total must be <100% to leave room for remainder if total_percentage >= 1.0 { return Err(EncodingError::InvalidInput(format!( @@ -150,13 +143,26 @@ impl SplitSwapStrategyEncoder { swaps: &[Swap], given_token: &Bytes, checked_token: &Bytes, + native_action: &Option, ) -> Result<(), EncodingError> { - // Special case: If given_token is ETH or checked_token is ETH, treat it as WETH for path - // validation - let given_token = if *given_token == *NATIVE_ADDRESS { &WETH_ADDRESS } else { given_token }; + // Convert ETH to WETH only if there's a corresponding wrap/unwrap action + let given_token = if *given_token == *NATIVE_ADDRESS { + match native_action { + Some(NativeAction::Wrap) => &WETH_ADDRESS, + _ => given_token, + } + } else { + given_token + }; - let checked_token = - if *checked_token == *NATIVE_ADDRESS { &WETH_ADDRESS } else { checked_token }; + let checked_token = if *checked_token == *NATIVE_ADDRESS { + match native_action { + Some(NativeAction::Unwrap) => &WETH_ADDRESS, + _ => checked_token, + } + } else { + checked_token + }; // Build directed graph of token flows let mut graph: HashMap<&Bytes, HashSet<&Bytes>> = HashMap::new(); @@ -211,7 +217,12 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { router_address: Bytes, ) -> Result<(Vec, Bytes), EncodingError> { self.validate_split_percentages(&solution.swaps)?; - self.validate_swap_path(&solution.swaps, &solution.given_token, &solution.checked_token)?; + self.validate_swap_path( + &solution.swaps, + &solution.given_token, + &solution.checked_token, + &solution.native_action, + )?; let (permit, signature) = self.permit2.get_permit( &router_address, &solution.sender, @@ -794,7 +805,7 @@ mod tests { token_out: dai.clone(), split: 0f64, }]; - let result = encoder.validate_swap_path(&swaps, &weth, &dai); + let result = encoder.validate_swap_path(&swaps, &weth, &dai, &None); assert_eq!(result, Ok(())); } @@ -826,7 +837,7 @@ mod tests { split: 0f64, }, ]; - let result = encoder.validate_swap_path(&swaps, &weth, &usdc); + let result = encoder.validate_swap_path(&swaps, &weth, &usdc, &None); assert_eq!(result, Ok(())); } @@ -861,7 +872,7 @@ mod tests { split: 0.0, }, ]; - let result = encoder.validate_swap_path(&disconnected_swaps, &weth, &usdc); + let result = encoder.validate_swap_path(&disconnected_swaps, &weth, &usdc, &None); assert!(matches!( result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") @@ -885,7 +896,7 @@ mod tests { token_out: dai.clone(), split: 1.0, }]; - let result = encoder.validate_swap_path(&unreachable_swaps, &weth, &usdc); + let result = encoder.validate_swap_path(&unreachable_swaps, &weth, &usdc, &None); assert!(matches!( result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") @@ -899,7 +910,7 @@ mod tests { let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); let empty_swaps: Vec = vec![]; - let result = encoder.validate_swap_path(&empty_swaps, &weth, &usdc); + let result = encoder.validate_swap_path(&empty_swaps, &weth, &usdc, &None); assert!(matches!( result, Err(EncodingError::InvalidInput(msg)) if msg.contains("not reachable through swap path") @@ -1100,7 +1111,7 @@ mod tests { split: 0f64, }]; - let result = encoder.validate_swap_path(&swaps, ð, &usdc); + let result = encoder.validate_swap_path(&swaps, ð, &usdc, &Some(NativeAction::Wrap)); assert_eq!(result, Ok(())); } @@ -1123,7 +1134,7 @@ mod tests { split: 0f64, }]; - let result = encoder.validate_swap_path(&swaps, &usdc, ð); + let result = encoder.validate_swap_path(&swaps, &usdc, ð, &Some(NativeAction::Unwrap)); assert_eq!(result, Ok(())); } } From 7b7226356d3bde61da987946dbc10ae3eec33722 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Wed, 5 Feb 2025 00:18:26 +0530 Subject: [PATCH 10/13] fix: get_mock_split_swap_strategy_encoder --- src/encoding/evm/strategy_encoder/strategy_encoders.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 5d43a96..f310a58 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -818,7 +818,8 @@ mod tests { fn get_mock_split_swap_strategy_encoder() -> SplitSwapStrategyEncoder { let private_key = "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); - SplitSwapStrategyEncoder::new(private_key, Chain::Ethereum).unwrap() + let swap_encoder_registry = get_swap_encoder_registry(); + SplitSwapStrategyEncoder::new(private_key, Chain::Ethereum, swap_encoder_registry).unwrap() } #[test] From 3dcb8aee7b07d47b91a477dfa07f04dac605ece8 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Wed, 5 Feb 2025 10:10:43 +0000 Subject: [PATCH 11/13] chore: Use tycho_core::dto objects instead of tycho_core::models --- don't change below this line --- ENG-4169 Took 11 minutes --- examples/quickstart/main.rs | 5 ++++- src/encoding/evm/approvals/permit2.rs | 2 +- src/encoding/evm/models.rs | 2 +- .../evm/strategy_encoder/strategy_encoder_registry.rs | 2 +- src/encoding/evm/strategy_encoder/strategy_encoders.rs | 2 +- src/encoding/evm/swap_encoder/swap_encoder_registry.rs | 2 +- src/encoding/evm/tycho_encoder.rs | 2 +- src/encoding/strategy_encoder.rs | 2 +- 8 files changed, 11 insertions(+), 8 deletions(-) diff --git a/examples/quickstart/main.rs b/examples/quickstart/main.rs index ee89104..bef3e1a 100644 --- a/examples/quickstart/main.rs +++ b/examples/quickstart/main.rs @@ -1,7 +1,10 @@ use std::str::FromStr; use num_bigint::BigUint; -use tycho_core::{dto::ProtocolComponent, models::Chain, Bytes}; +use tycho_core::{ + dto::{Chain, ProtocolComponent}, + Bytes, +}; use tycho_execution::encoding::{ evm::{ strategy_encoder::strategy_encoder_registry::EVMStrategyEncoderRegistry, diff --git a/src/encoding/evm/approvals/permit2.rs b/src/encoding/evm/approvals/permit2.rs index 7460cae..b189d08 100644 --- a/src/encoding/evm/approvals/permit2.rs +++ b/src/encoding/evm/approvals/permit2.rs @@ -12,7 +12,7 @@ use alloy_sol_types::{eip712_domain, sol, SolStruct, SolValue}; use chrono::Utc; use num_bigint::BigUint; use tokio::runtime::Runtime; -use tycho_core::{models::Chain, Bytes}; +use tycho_core::{dto::Chain, Bytes}; use crate::encoding::{ errors::EncodingError, diff --git a/src/encoding/evm/models.rs b/src/encoding/evm/models.rs index 68121bf..706bc46 100644 --- a/src/encoding/evm/models.rs +++ b/src/encoding/evm/models.rs @@ -1,4 +1,4 @@ -use tycho_core::models::Chain; +use tycho_core::dto::Chain; pub struct ChainId(u64); diff --git a/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs b/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs index 5554c9d..94a8253 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoder_registry.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use tycho_core::models::Chain; +use tycho_core::dto::Chain; use crate::encoding::{ errors::EncodingError, diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 62a873e..df08515 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -3,7 +3,7 @@ use std::{cmp::max, collections::HashSet, str::FromStr}; use alloy_primitives::{aliases::U24, FixedBytes, U256, U8}; use alloy_sol_types::SolValue; use num_bigint::BigUint; -use tycho_core::{keccak256, models::Chain, Bytes}; +use tycho_core::{dto::Chain, keccak256, Bytes}; use crate::encoding::{ errors::EncodingError, diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index 639dfc0..b04e580 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, fs}; -use tycho_core::models::Chain; +use tycho_core::dto::Chain; use crate::encoding::{ errors::EncodingError, evm::swap_encoder::builder::SwapEncoderBuilder, diff --git a/src/encoding/evm/tycho_encoder.rs b/src/encoding/evm/tycho_encoder.rs index 1a93400..23afbbf 100644 --- a/src/encoding/evm/tycho_encoder.rs +++ b/src/encoding/evm/tycho_encoder.rs @@ -104,7 +104,7 @@ impl TychoEncoder for EVMTychoEncoder { #[cfg(test)] mod tests { - use tycho_core::{dto::ProtocolComponent, models::Chain}; + use tycho_core::dto::{Chain, ProtocolComponent}; use super::*; use crate::encoding::{ diff --git a/src/encoding/strategy_encoder.rs b/src/encoding/strategy_encoder.rs index 04d23e7..1b5013b 100644 --- a/src/encoding/strategy_encoder.rs +++ b/src/encoding/strategy_encoder.rs @@ -1,4 +1,4 @@ -use tycho_core::{models::Chain, Bytes}; +use tycho_core::{dto::Chain, Bytes}; use crate::encoding::{errors::EncodingError, models::Solution, swap_encoder::SwapEncoder}; From 9e61261a51506372786707cec4b035e84b665912 Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Roy <42067944+royvardhan@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:23:10 +0530 Subject: [PATCH 12/13] Update strategy_encoders.rs Co-authored-by: Tamara --- src/encoding/evm/strategy_encoder/strategy_encoders.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 31343f9..7a33c85 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -113,9 +113,9 @@ impl SplitSwapStrategyEncoder { } (true, true) => found_zero_split = true, (false, _) => { - if swap.split <= 0.0 { + if swap.split < 0.0 { return Err(EncodingError::InvalidInput(format!( - "Non-remainder splits must be >0% for token {:?}", + "All splits must be >= 0% for token {:?}", token ))); } From cb01f95af9b55c0eeb694bf5fd015e5d3e82973f Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 5 Feb 2025 15:03:14 +0000 Subject: [PATCH 13/13] chore(release): 0.31.0 [skip ci] ## [0.31.0](https://github.com/propeller-heads/tycho-execution/compare/0.30.1...0.31.0) (2025-02-05) ### Features * add tests for split swap validations ([b69aef9](https://github.com/propeller-heads/tycho-execution/commit/b69aef9b8f1d253bb465a39669bd18aa5f355aa5)) * add tests for wrap unwrap case ([4d97c3f](https://github.com/propeller-heads/tycho-execution/commit/4d97c3f16d263c975f1f42bebae9666af789eb10)) * add validation for split swap ([f80ffa9](https://github.com/propeller-heads/tycho-execution/commit/f80ffa924f1da626bef0751c92c09fb133d2ba85)) ### Bug Fixes * checks in validations ([95edd5b](https://github.com/propeller-heads/tycho-execution/commit/95edd5b1fe99fd96163dcf74c2a570a7c8a480a1)) * get_mock_split_swap_strategy_encoder ([7b72263](https://github.com/propeller-heads/tycho-execution/commit/7b7226356d3bde61da987946dbc10ae3eec33722)) * use native action to validate path ([c787f5e](https://github.com/propeller-heads/tycho-execution/commit/c787f5e722ad8a9f9a24e6ea09f59dfcf5f82239)) --- CHANGELOG.md | 16 ++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 960b704..98553bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## [0.31.0](https://github.com/propeller-heads/tycho-execution/compare/0.30.1...0.31.0) (2025-02-05) + + +### Features + +* add tests for split swap validations ([b69aef9](https://github.com/propeller-heads/tycho-execution/commit/b69aef9b8f1d253bb465a39669bd18aa5f355aa5)) +* add tests for wrap unwrap case ([4d97c3f](https://github.com/propeller-heads/tycho-execution/commit/4d97c3f16d263c975f1f42bebae9666af789eb10)) +* add validation for split swap ([f80ffa9](https://github.com/propeller-heads/tycho-execution/commit/f80ffa924f1da626bef0751c92c09fb133d2ba85)) + + +### Bug Fixes + +* checks in validations ([95edd5b](https://github.com/propeller-heads/tycho-execution/commit/95edd5b1fe99fd96163dcf74c2a570a7c8a480a1)) +* get_mock_split_swap_strategy_encoder ([7b72263](https://github.com/propeller-heads/tycho-execution/commit/7b7226356d3bde61da987946dbc10ae3eec33722)) +* use native action to validate path ([c787f5e](https://github.com/propeller-heads/tycho-execution/commit/c787f5e722ad8a9f9a24e6ea09f59dfcf5f82239)) + ## [0.30.1](https://github.com/propeller-heads/tycho-execution/compare/0.30.0...0.30.1) (2025-02-04) diff --git a/Cargo.lock b/Cargo.lock index e35c23e..21b6199 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4231,7 +4231,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "0.30.1" +version = "0.31.0" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 1c7e8f7..8a363ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "0.30.1" +version = "0.31.0" edition = "2021" [dependencies]