feat: Add validation for cyclical trades

We only support cyclical trades when the repeated token is the first and the last token in the swap. We don't support cycles in between

--- don't change below this line ---
ENG-4331 Took 2 hours 4 minutes
This commit is contained in:
Diana Carvalho
2025-03-13 16:13:28 +00:00
parent d7244ada8c
commit 55ffa4eb45
2 changed files with 169 additions and 1 deletions

View File

@@ -25,7 +25,6 @@ impl SwapEncoderBuilder {
"uniswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address))), "uniswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address))),
"vm:balancer_v2" => Ok(Box::new(BalancerV2SwapEncoder::new(self.executor_address))), "vm:balancer_v2" => Ok(Box::new(BalancerV2SwapEncoder::new(self.executor_address))),
"uniswap_v3" => Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address))), "uniswap_v3" => Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address))),
// TODO replace this with V4 encoder once implemented
"uniswap_v4" => Ok(Box::new(UniswapV4SwapEncoder::new(self.executor_address))), "uniswap_v4" => Ok(Box::new(UniswapV4SwapEncoder::new(self.executor_address))),
_ => Err(EncodingError::FatalError(format!( _ => Err(EncodingError::FatalError(format!(
"Unknown protocol system: {}", "Unknown protocol system: {}",

View File

@@ -1,3 +1,5 @@
use std::collections::HashSet;
use num_bigint::BigUint; use num_bigint::BigUint;
use tycho_core::Bytes; use tycho_core::Bytes;
@@ -92,6 +94,44 @@ impl EVMTychoEncoder {
} }
} }
} }
let mut solution_tokens = vec![];
let mut split_tokens_already_considered = HashSet::new();
for (i, swap) in solution.swaps.iter().enumerate() {
// so we don't count the split tokens more than once
if swap.split != 0.0 {
if !split_tokens_already_considered.contains(&swap.token_in) {
solution_tokens.push(swap.token_in.clone());
split_tokens_already_considered.insert(swap.token_in.clone());
}
} else {
// it's the last swap of the split
if split_tokens_already_considered.contains(&swap.token_in) {
split_tokens_already_considered.remove(&swap.token_in);
} else {
solution_tokens.push(swap.token_in.clone());
}
}
if i == solution.swaps.len() - 1 {
solution_tokens.push(swap.token_out.clone());
}
}
if solution_tokens.len() !=
solution_tokens
.iter()
.cloned()
.collect::<HashSet<Bytes>>()
.len()
{
if let Some(last_swap) = solution.swaps.last() {
if solution.swaps[0].token_in != last_swap.token_out {
return Err(EncodingError::FatalError(
"Cyclical swaps are only allowed if they are the first and last token of a solution".to_string(),
));
}
}
}
Ok(()) Ok(())
} }
} }
@@ -148,6 +188,14 @@ mod tests {
Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap() Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap()
} }
fn usdc() -> Bytes {
Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap()
}
fn wbtc() -> Bytes {
Bytes::from_str("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap()
}
#[derive(Clone)] #[derive(Clone)]
struct MockStrategy; struct MockStrategy;
@@ -436,4 +484,125 @@ mod tests {
) )
); );
} }
#[test]
fn test_validate_cyclical_swap() {
// This validation passes because the cyclical swap is the first and last token
// 50% -> WETH
// DAI -> DAI
// 50% -> WETH
let encoder = get_mocked_tycho_encoder();
let swaps = vec![
Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0.5f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0.5f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth(),
token_out: dai(),
split: 0f64,
},
];
let solution = Solution {
exact_out: false,
given_token: dai(),
checked_token: dai(),
swaps,
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_ok());
}
#[test]
fn test_validate_cyclical_swap_fail() {
// This test should fail because the cyclical swap is not the first and last token
// DAI -> WETH -> USDC -> DAI -> WBTC
// (some of the pool addresses in this test are fake)
let encoder = get_mocked_tycho_encoder();
let swaps = vec![
Swap {
component: ProtocolComponent {
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: weth(),
split: 0f64,
},
Swap {
component: ProtocolComponent {
id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: weth(),
token_out: usdc(),
split: 0f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: usdc(),
token_out: dai(),
split: 0f64,
},
Swap {
component: ProtocolComponent {
id: "0x0000000000000000000000000000000000000000".to_string(),
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
token_in: dai(),
token_out: wbtc(),
split: 0f64,
},
];
let solution = Solution {
exact_out: false,
given_token: dai(),
checked_token: wbtc(),
swaps,
..Default::default()
};
let result = encoder.validate_solution(&solution);
assert!(result.is_err());
assert_eq!(
result.err().unwrap(),
EncodingError::FatalError(
"Cyclical swaps are only allowed if they are the first and last token of a solution".to_string()
)
);
}
} }