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:
@@ -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: {}",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user