Merge branch 'refs/heads/feature/gas-optimization' into router/dc/ENG-4411-refactor-callback-transient-storage

# Conflicts:
#	foundry/test/TychoRouter.t.sol
#	src/encoding/evm/strategy_encoder/strategy_encoders.rs

Took 5 minutes

Took 35 seconds
This commit is contained in:
Diana Carvalho
2025-04-10 10:24:33 +01:00
26 changed files with 4342 additions and 2672 deletions

View File

@@ -1,322 +0,0 @@
use tycho_common::Bytes;
use crate::encoding::{evm::constants::GROUPABLE_PROTOCOLS, models::Swap};
/// Represents a group of swaps that can be encoded into a single swap execution for gas
/// optimization.
///
/// # Fields
/// * `input_token`: Bytes, the input token of the first swap
/// * `output_token`: Bytes, the output token of the final swap
/// * `protocol_system`: String, the protocol system of the swaps
/// * `swaps`: Vec<Swap>, the sequence of swaps to be executed as a group
/// * `split`: f64, the split percentage of the first swap in the group
#[derive(Clone, PartialEq, Debug)]
pub struct SwapGroup {
pub input_token: Bytes,
pub output_token: Bytes,
pub protocol_system: String,
pub swaps: Vec<Swap>,
pub split: f64,
}
/// Group consecutive swaps which can be encoded into one swap execution for gas optimization.
///
/// An example where this applies is the case of USV4, which uses a PoolManager contract
/// to save token transfers on consecutive swaps.
pub fn group_swaps(swaps: Vec<Swap>) -> Vec<SwapGroup> {
let mut grouped_swaps: Vec<SwapGroup> = Vec::new();
let mut current_group: Option<SwapGroup> = None;
let mut last_swap_protocol = "".to_string();
let mut groupable_protocol;
let mut last_swap_out_token = Bytes::default();
for swap in swaps {
let current_swap_protocol = swap.component.protocol_system.clone();
groupable_protocol = GROUPABLE_PROTOCOLS.contains(&current_swap_protocol.as_str());
// Split 0 can also mean that the swap is the remaining part of a branch of splits,
// so we need to check the last swap's out token as well
let no_split = swap.split == 0.0 && swap.token_in == last_swap_out_token;
if current_swap_protocol == last_swap_protocol && groupable_protocol && no_split {
// Second or later groupable pool in a sequence of groupable pools. Merge to the
// current group.
if let Some(group) = current_group.as_mut() {
group.swaps.push(swap.clone());
// Update the output token of the current group.
group.output_token = swap.token_out.clone();
}
} else {
// Not second or later USV4 pool. Push the current group (if it exists) and then
// create a new group.
if let Some(group) = current_group.as_mut() {
grouped_swaps.push(group.clone());
}
current_group = Some(SwapGroup {
input_token: swap.token_in.clone(),
output_token: swap.token_out.clone(),
protocol_system: current_swap_protocol.clone(),
swaps: vec![swap.clone()],
split: swap.split,
});
}
last_swap_protocol = current_swap_protocol;
last_swap_out_token = swap.token_out.clone();
}
if let Some(group) = current_group.as_mut() {
grouped_swaps.push(group.clone());
}
grouped_swaps
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use alloy_primitives::hex;
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use super::*;
use crate::encoding::models::Swap;
fn weth() -> Bytes {
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
}
#[test]
fn test_group_swaps_simple() {
// The first and second swaps can be grouped since there is no split, and they are
// both USV4.
//
// WETH ──(USV4)──> WBTC ───(USV4)──> USDC ───(USV2)──> DAI
let weth = weth();
let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap_weth_wbtc = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v4".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: wbtc.clone(),
// This represents the remaining 50%, but to avoid any rounding errors we set this to
// 0 to signify "the remainder of the WETH value". It should still be very close to 50%
split: 0f64,
};
let swap_wbtc_usdc = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v4".to_string(),
..Default::default()
},
token_in: wbtc.clone(),
token_out: usdc.clone(),
split: 0f64,
};
let swap_usdc_dai = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: usdc.clone(),
token_out: dai.clone(),
split: 0f64,
};
let grouped_swaps = group_swaps(vec![
swap_weth_wbtc.clone(),
swap_wbtc_usdc.clone(),
swap_usdc_dai.clone(),
]);
assert_eq!(
grouped_swaps,
vec![
SwapGroup {
swaps: vec![swap_weth_wbtc, swap_wbtc_usdc],
input_token: weth,
output_token: usdc.clone(),
protocol_system: "uniswap_v4".to_string(),
split: 0f64,
},
SwapGroup {
swaps: vec![swap_usdc_dai],
input_token: usdc,
output_token: dai,
protocol_system: "uniswap_v2".to_string(),
split: 0f64,
}
]
);
}
#[test]
fn test_group_swaps_complex_split() {
// There is a split in the solution, but it's possible to combine two of the USV4 splits.
// The WETH -> USDC swap cannot get grouped with anything, but the WETH -> DAI and
// DAI -> USDC swaps can be grouped.
//
// ┌──(USV4)──> USDC
// WBTC ──> (USV4)──> WETH ─┤
// └──(USV4)──> DAI ───(USV4)──> USDC
let weth = weth();
let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap_wbtc_weth = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v4".to_string(),
..Default::default()
},
token_in: wbtc.clone(),
token_out: weth.clone(),
split: 0f64,
};
let swap_weth_usdc = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v4".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: usdc.clone(),
split: 0.5f64,
};
let swap_weth_dai = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v4".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
// This represents the remaining 50%, but to avoid any rounding errors we set this to
// 0 to signify "the remainder of the WETH value". It should still be very close to 50%
split: 0f64,
};
let swap_dai_usdc = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v4".to_string(),
..Default::default()
},
token_in: dai.clone(),
token_out: usdc.clone(),
split: 0f64,
};
let grouped_swaps = group_swaps(vec![
swap_wbtc_weth.clone(),
swap_weth_usdc.clone(),
swap_weth_dai.clone(),
swap_dai_usdc.clone(),
]);
assert_eq!(
grouped_swaps,
vec![
SwapGroup {
swaps: vec![swap_wbtc_weth],
input_token: wbtc.clone(),
output_token: weth.clone(),
protocol_system: "uniswap_v4".to_string(),
split: 0f64,
},
SwapGroup {
swaps: vec![swap_weth_usdc],
input_token: weth.clone(),
output_token: usdc.clone(),
protocol_system: "uniswap_v4".to_string(),
split: 0.5f64,
},
SwapGroup {
swaps: vec![swap_weth_dai, swap_dai_usdc],
input_token: weth,
output_token: usdc,
protocol_system: "uniswap_v4".to_string(),
split: 0f64,
}
]
);
}
#[test]
fn test_group_swaps_complex_split_multi_protocol() {
// There is a split in the solution, but it's possible to group the USV4 splits with each
// other and the Balancer V3 swaps with each other.
//
// ┌──(BalancerV3)──> WBTC ──(BalancerV3)──> USDC
// WETH ─┤
// └──(USV4)──> DAI ───(USV4)──> USDC
let weth = weth();
let wbtc = Bytes::from_str("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599").unwrap();
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let swap_weth_wbtc = Swap {
component: ProtocolComponent {
protocol_system: "balancer_v3".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: wbtc.clone(),
split: 0.5f64,
};
let swap_wbtc_usdc = Swap {
component: ProtocolComponent {
protocol_system: "balancer_v3".to_string(),
..Default::default()
},
token_in: wbtc.clone(),
token_out: usdc.clone(),
split: 0f64,
};
let swap_weth_dai = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v4".to_string(),
..Default::default()
},
token_in: weth.clone(),
token_out: dai.clone(),
// This represents the remaining 50%, but to avoid any rounding errors we set this to
// 0 to signify "the remainder of the WETH value". It should still be very close to 50%
split: 0f64,
};
let swap_dai_usdc = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v4".to_string(),
..Default::default()
},
token_in: dai.clone(),
token_out: usdc.clone(),
split: 0f64,
};
let grouped_swaps = group_swaps(vec![
swap_weth_wbtc.clone(),
swap_wbtc_usdc.clone(),
swap_weth_dai.clone(),
swap_dai_usdc.clone(),
]);
assert_eq!(
grouped_swaps,
vec![
SwapGroup {
swaps: vec![swap_weth_wbtc, swap_wbtc_usdc],
input_token: weth.clone(),
output_token: usdc.clone(),
protocol_system: "balancer_v3".to_string(),
split: 0.5f64,
},
SwapGroup {
swaps: vec![swap_weth_dai, swap_dai_usdc],
input_token: weth,
output_token: usdc,
protocol_system: "uniswap_v4".to_string(),
split: 0f64,
}
]
);
}
}

View File

@@ -1,3 +1,2 @@
mod group_swaps;
pub mod strategy_encoders;
mod strategy_validators;

File diff suppressed because it is too large Load Diff

View File

@@ -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<Bytes, Vec<&Swap>> = 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<Bytes, Vec<&Swap>> = 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;