From 0d8150e22f2a354a41583a722bd4eba60ab1de59 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Thu, 27 Mar 2025 11:19:00 +0100 Subject: [PATCH] feat: sequential swap solution validation - Basically reuse all methods of split swap validation, minus those specific to splits. --- .../evm/strategy_encoder/strategy_encoders.rs | 32 +++- .../strategy_encoder/strategy_validators.rs | 180 +++++++++--------- 2 files changed, 124 insertions(+), 88 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 6821e61..4f49eee 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -12,7 +12,10 @@ use crate::encoding::{ evm::{ approvals::permit2::Permit2, constants::DEFAULT_ROUTERS_JSON, - strategy_encoder::{group_swaps::group_swaps, strategy_validators::SplitSwapValidator}, + strategy_encoder::{ + group_swaps::group_swaps, + strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator}, + }, swap_encoder::swap_encoder_registry::SwapEncoderRegistry, utils::{ biguint_to_u256, bytes_to_address, encode_input, get_min_amount_for_solution, @@ -209,12 +212,17 @@ impl StrategyEncoder for SingleSwapStrategyEncoder { /// * `native_address`: Address of the chain's native token /// * `wrapped_address`: Address of the chain's wrapped token /// * `router_address`: Address of the router to be used to execute swaps +/// * `sequential_swap_validator`: SequentialSwapValidator, responsible for checking validity of +/// sequential swap solutions #[derive(Clone)] pub struct SequentialSwapStrategyEncoder { swap_encoder_registry: SwapEncoderRegistry, permit2: Option, selector: String, router_address: Bytes, + native_address: Bytes, + wrapped_address: Bytes, + sequential_swap_validator: SequentialSwapValidator, } impl SequentialSwapStrategyEncoder { @@ -234,7 +242,15 @@ impl SequentialSwapStrategyEncoder { .to_string(), ) }; - Ok(Self { permit2, selector, swap_encoder_registry, router_address }) + Ok(Self { + permit2, + selector, + swap_encoder_registry, + router_address, + native_address: chain.native_token()?, + wrapped_address: chain.wrapped_token()?, + sequential_swap_validator: SequentialSwapValidator, + }) } /// Encodes information necessary for performing a single swap against a given executor for @@ -251,7 +267,17 @@ impl EVMStrategyEncoder for SequentialSwapStrategyEncoder {} impl StrategyEncoder for SequentialSwapStrategyEncoder { fn encode_strategy(&self, solution: Solution) -> Result<(Vec, Bytes), EncodingError> { - // TODO validate sequential swaps: check valid cycles, empty swaps, etc. + self.sequential_swap_validator + .validate_solution_min_amounts(&solution)?; + self.sequential_swap_validator + .validate_swap_path( + &solution.swaps, + &solution.given_token, + &solution.checked_token, + &solution.native_action, + &self.native_address, + &self.wrapped_address, + )?; let min_amount_out = get_min_amount_for_solution(solution.clone()); let grouped_swaps = group_swaps(solution.swaps); diff --git a/src/encoding/evm/strategy_encoder/strategy_validators.rs b/src/encoding/evm/strategy_encoder/strategy_validators.rs index e7ae722..04396de 100644 --- a/src/encoding/evm/strategy_encoder/strategy_validators.rs +++ b/src/encoding/evm/strategy_encoder/strategy_validators.rs @@ -7,92 +7,10 @@ use crate::encoding::{ models::{NativeAction, Solution, Swap}, }; -/// Validates whether a sequence of split swaps represents a valid solution. -#[derive(Clone)] -pub struct SplitSwapValidator; - -impl SplitSwapValidator { - /// Raises an error if the split percentages are invalid. - /// - /// Split percentages are considered valid if all the following conditions are met: - /// * Each split amount is < 1 (100%) - /// * There is exactly one 0% split for each token, and it's the last swap specified, signifying - /// to the router to send the remainder of the token to the designated protocol - /// * The sum of all non-remainder splits for each token is < 1 (100%) - /// * There are no negative split amounts - pub 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 { - return Err(EncodingError::InvalidInput(format!( - "Split percentage must be less than 1 (100%), got {}", - swap.split - ))); - } - swaps_by_token - .entry(swap.token_in.clone()) - .or_default() - .push(swap); - } - - for (token, token_swaps) in swaps_by_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; - } - - 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) => { - return Err(EncodingError::InvalidInput(format!( - "The 0% split for token {:?} must be the last swap", - token - ))) - } - (true, true) => found_zero_split = true, - (false, _) => { - if swap.split < 0.0 { - return Err(EncodingError::InvalidInput(format!( - "All splits must be >= 0% for token {:?}", - token - ))); - } - total_percentage += swap.split; - } - } - } - - if !found_zero_split { - return Err(EncodingError::InvalidInput(format!( - "Token {:?} must have exactly one 0% split for remainder handling", - token - ))); - } - - // 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(()) - } - +pub trait SwapValidator { /// Raises an error if the solution does not have checked amount set or slippage with checked /// amount set. - pub fn validate_solution_min_amounts(&self, solution: &Solution) -> Result<(), EncodingError> { + fn validate_solution_min_amounts(&self, solution: &Solution) -> Result<(), EncodingError> { if solution.checked_amount.is_none() && (solution.slippage.is_none() || solution.expected_amount.is_none()) { @@ -113,7 +31,7 @@ impl SplitSwapValidator { /// If the given token is the native token and the native action is WRAP, it will be converted /// to the wrapped token before validating the swap path. The same principle applies for the /// checked token and the UNWRAP action. - pub fn validate_swap_path( + fn validate_swap_path( &self, swaps: &[Swap], given_token: &Bytes, @@ -197,6 +115,98 @@ impl SplitSwapValidator { } } +/// Validates whether a sequence of split swaps represents a valid solution. +#[derive(Clone)] +pub struct SplitSwapValidator; + +impl SwapValidator for SplitSwapValidator {} + +impl SplitSwapValidator { + /// Raises an error if the split percentages are invalid. + /// + /// Split percentages are considered valid if all the following conditions are met: + /// * Each split amount is < 1 (100%) + /// * There is exactly one 0% split for each token, and it's the last swap specified, signifying + /// to the router to send the remainder of the token to the designated protocol + /// * The sum of all non-remainder splits for each token is < 1 (100%) + /// * There are no negative split amounts + pub 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 { + return Err(EncodingError::InvalidInput(format!( + "Split percentage must be less than 1 (100%), got {}", + swap.split + ))); + } + swaps_by_token + .entry(swap.token_in.clone()) + .or_default() + .push(swap); + } + + for (token, token_swaps) in swaps_by_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; + } + + 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) => { + return Err(EncodingError::InvalidInput(format!( + "The 0% split for token {:?} must be the last swap", + token + ))) + } + (true, true) => found_zero_split = true, + (false, _) => { + if swap.split < 0.0 { + return Err(EncodingError::InvalidInput(format!( + "All splits must be >= 0% for token {:?}", + token + ))); + } + total_percentage += swap.split; + } + } + } + + if !found_zero_split { + return Err(EncodingError::InvalidInput(format!( + "Token {:?} must have exactly one 0% split for remainder handling", + token + ))); + } + + // 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(()) + } +} + +/// Validates whether a sequence of sequential swaps represents a valid solution. +#[derive(Clone)] +pub struct SequentialSwapValidator; + +impl SwapValidator for SequentialSwapValidator {} + #[cfg(test)] mod tests { use std::str::FromStr;