Merge branch 'main' into router/tnl/more-protocol-integration

This commit is contained in:
dianacarvalho1
2025-04-23 09:44:22 +01:00
committed by Diana Carvalho
20 changed files with 805 additions and 314 deletions

View File

@@ -8,7 +8,6 @@ use crate::encoding::{
errors::EncodingError,
evm::{
approvals::permit2::Permit2,
constants::{CALLBACK_CONSTRAINED_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS},
group_swaps::group_swaps,
strategy_encoder::{
strategy_validators::{SequentialSwapValidator, SplitSwapValidator, SwapValidator},
@@ -32,17 +31,15 @@ use crate::encoding::{
/// * `permit2`: Permit2, responsible for managing permit2 operations and providing necessary
/// signatures and permit2 objects for calling the router
/// * `selector`: String, the selector for the swap function in the router contract
/// * `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
/// * `transfer_optimization`: TransferOptimization, responsible for optimizing the token transfers
#[derive(Clone)]
pub struct SingleSwapStrategyEncoder {
swap_encoder_registry: SwapEncoderRegistry,
permit2: Option<Permit2>,
selector: String,
native_address: Bytes,
wrapped_address: Bytes,
router_address: Bytes,
transfer_optimization: TransferOptimization,
}
impl SingleSwapStrategyEncoder {
@@ -51,6 +48,7 @@ impl SingleSwapStrategyEncoder {
swap_encoder_registry: SwapEncoderRegistry,
swapper_pk: Option<String>,
router_address: Bytes,
token_in_already_in_router: bool,
) -> Result<Self, EncodingError> {
let (permit2, selector) = if let Some(swapper_pk) = swapper_pk {
(Some(Permit2::new(swapper_pk, chain.clone())?), "singleSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string())
@@ -60,13 +58,19 @@ impl SingleSwapStrategyEncoder {
"singleSwap(uint256,address,address,uint256,bool,bool,address,bytes)".to_string(),
)
};
let permit2_is_active = permit2.is_some();
Ok(Self {
permit2,
selector,
swap_encoder_registry,
native_address: chain.native_token()?,
wrapped_address: chain.wrapped_token()?,
router_address,
router_address: router_address.clone(),
transfer_optimization: TransferOptimization::new(
chain.native_token()?,
chain.wrapped_token()?,
permit2_is_active,
token_in_already_in_router,
router_address,
),
})
}
@@ -80,8 +84,6 @@ impl SingleSwapStrategyEncoder {
}
}
impl TransferOptimization for SingleSwapStrategyEncoder {}
impl StrategyEncoder for SingleSwapStrategyEncoder {
fn encode_strategy(&self, solution: Solution) -> Result<(Vec<u8>, Bytes), EncodingError> {
let grouped_swaps = group_swaps(solution.clone().swaps);
@@ -125,26 +127,20 @@ impl StrategyEncoder for SingleSwapStrategyEncoder {
let swap_receiver =
if !unwrap { solution.receiver.clone() } else { self.router_address.clone() };
let transfer_type = self
.transfer_optimization
.get_transfer_type(grouped_swap.clone(), solution.given_token.clone(), wrap, false);
let encoding_context = EncodingContext {
receiver: swap_receiver.clone(),
exact_out: solution.exact_out,
router_address: Some(self.router_address.clone()),
group_token_in: grouped_swap.token_in.clone(),
group_token_out: grouped_swap.token_out.clone(),
transfer_type: transfer_type.clone(),
};
let mut grouped_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() {
let transfer_type = self.get_transfer_type(
swap.clone(),
solution.given_token.clone(),
self.native_address.clone(),
self.wrapped_address.clone(),
self.permit2.clone().is_some(),
wrap,
false,
);
let encoding_context = EncodingContext {
receiver: swap_receiver.clone(),
exact_out: solution.exact_out,
router_address: Some(self.router_address.clone()),
group_token_in: grouped_swap.input_token.clone(),
group_token_out: grouped_swap.output_token.clone(),
transfer_type: transfer_type.clone(),
};
let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?;
grouped_protocol_data.extend(protocol_data);
}
@@ -215,6 +211,7 @@ impl StrategyEncoder for SingleSwapStrategyEncoder {
/// * `router_address`: Address of the router to be used to execute swaps
/// * `sequential_swap_validator`: SequentialSwapValidator, responsible for checking validity of
/// sequential swap solutions
/// * `transfer_optimization`: TransferOptimization, responsible for optimizing the token transfers
#[derive(Clone)]
pub struct SequentialSwapStrategyEncoder {
swap_encoder_registry: SwapEncoderRegistry,
@@ -224,16 +221,16 @@ pub struct SequentialSwapStrategyEncoder {
native_address: Bytes,
wrapped_address: Bytes,
sequential_swap_validator: SequentialSwapValidator,
transfer_optimization: TransferOptimization,
}
impl TransferOptimization for SequentialSwapStrategyEncoder {}
impl SequentialSwapStrategyEncoder {
pub fn new(
chain: Chain,
swap_encoder_registry: SwapEncoderRegistry,
swapper_pk: Option<String>,
router_address: Bytes,
token_in_already_in_router: bool,
) -> Result<Self, EncodingError> {
let (permit2, selector) = if let Some(swapper_pk) = swapper_pk {
(Some(Permit2::new(swapper_pk, chain.clone())?), "sequentialSwapPermit2(uint256,address,address,uint256,bool,bool,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string())
@@ -244,14 +241,22 @@ impl SequentialSwapStrategyEncoder {
.to_string(),
)
};
let permit2_is_active = permit2.is_some();
Ok(Self {
permit2,
selector,
swap_encoder_registry,
router_address,
router_address: router_address.clone(),
native_address: chain.native_token()?,
wrapped_address: chain.wrapped_token()?,
sequential_swap_validator: SequentialSwapValidator,
transfer_optimization: TransferOptimization::new(
chain.native_token()?,
chain.wrapped_token()?,
permit2_is_active,
token_in_already_in_router,
router_address,
),
})
}
@@ -291,7 +296,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
}
let mut swaps = vec![];
let mut next_in_between_swap_optimization = true;
let mut next_in_between_swap_optimization_allowed = true;
for (i, grouped_swap) in grouped_swaps.iter().enumerate() {
let protocol = grouped_swap.protocol_system.clone();
let swap_encoder = self
@@ -303,50 +308,31 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
))
})?;
let in_between_swap_optimization = next_in_between_swap_optimization;
let in_between_swap_optimization_allowed = next_in_between_swap_optimization_allowed;
let next_swap = grouped_swaps.get(i + 1);
// if there is a next swap
let swap_receiver = if let Some(next) = next_swap {
// if the protocol of the next swap supports transfer in optimization
if IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&next.protocol_system.as_str()) {
// if the protocol does not allow for chained swaps, we can't optimize the
// receiver of this swap nor the transfer in of the next swap
if CALLBACK_CONSTRAINED_PROTOCOLS.contains(&next.protocol_system.as_str()) {
next_in_between_swap_optimization = false;
self.router_address.clone()
} else {
Bytes::from_str(&next.swaps[0].component.id.clone()).map_err(|_| {
EncodingError::FatalError("Invalid component id".to_string())
})?
}
} else {
// the protocol of the next swap does not support transfer in optimization
self.router_address.clone()
}
} else {
solution.receiver.clone() // last swap - there is not next swap
let (swap_receiver, next_swap_optimization) = self
.transfer_optimization
.get_receiver(solution.receiver.clone(), next_swap)?;
next_in_between_swap_optimization_allowed = next_swap_optimization;
let transfer_type = self
.transfer_optimization
.get_transfer_type(
grouped_swap.clone(),
solution.given_token.clone(),
wrap,
in_between_swap_optimization_allowed,
);
let encoding_context = EncodingContext {
receiver: swap_receiver.clone(),
exact_out: solution.exact_out,
router_address: Some(self.router_address.clone()),
group_token_in: grouped_swap.token_in.clone(),
group_token_out: grouped_swap.token_out.clone(),
transfer_type: transfer_type.clone(),
};
let mut grouped_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() {
let transfer_type = self.get_transfer_type(
swap.clone(),
solution.given_token.clone(),
self.native_address.clone(),
self.wrapped_address.clone(),
self.permit2.clone().is_some(),
wrap,
in_between_swap_optimization,
);
let encoding_context = EncodingContext {
receiver: swap_receiver.clone(),
exact_out: solution.exact_out,
router_address: Some(self.router_address.clone()),
group_token_in: grouped_swap.input_token.clone(),
group_token_out: grouped_swap.output_token.clone(),
transfer_type: transfer_type.clone(),
};
let protocol_data =
swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?;
grouped_protocol_data.extend(protocol_data);
@@ -422,6 +408,7 @@ impl StrategyEncoder for SequentialSwapStrategyEncoder {
/// * `split_swap_validator`: SplitSwapValidator, responsible for checking validity of split swap
/// solutions
/// * `router_address`: Address of the router to be used to execute swaps
/// * `transfer_optimization`: TransferOptimization, responsible for optimizing the token transfers
#[derive(Clone)]
pub struct SplitSwapStrategyEncoder {
swap_encoder_registry: SwapEncoderRegistry,
@@ -431,6 +418,7 @@ pub struct SplitSwapStrategyEncoder {
wrapped_address: Bytes,
split_swap_validator: SplitSwapValidator,
router_address: Bytes,
transfer_optimization: TransferOptimization,
}
impl SplitSwapStrategyEncoder {
@@ -439,6 +427,7 @@ impl SplitSwapStrategyEncoder {
swap_encoder_registry: SwapEncoderRegistry,
swapper_pk: Option<String>,
router_address: Bytes,
token_in_already_in_router: bool,
) -> Result<Self, EncodingError> {
let (permit2, selector) = if let Some(swapper_pk) = swapper_pk {
(Some(Permit2::new(swapper_pk, chain.clone())?), "splitSwapPermit2(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string())
@@ -449,7 +438,7 @@ impl SplitSwapStrategyEncoder {
.to_string(),
)
};
let permit2_is_active = permit2.is_some();
Ok(Self {
permit2,
selector,
@@ -457,7 +446,14 @@ impl SplitSwapStrategyEncoder {
native_address: chain.native_token()?,
wrapped_address: chain.wrapped_token()?,
split_swap_validator: SplitSwapValidator,
router_address,
router_address: router_address.clone(),
transfer_optimization: TransferOptimization::new(
chain.native_token()?,
chain.wrapped_token()?,
permit2_is_active,
token_in_already_in_router,
router_address,
),
})
}
@@ -481,8 +477,6 @@ impl SplitSwapStrategyEncoder {
}
}
impl TransferOptimization for SplitSwapStrategyEncoder {}
impl StrategyEncoder for SplitSwapStrategyEncoder {
fn encode_strategy(&self, solution: Solution) -> Result<(Vec<u8>, Bytes), EncodingError> {
self.split_swap_validator
@@ -513,7 +507,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
let intermediary_tokens: HashSet<Bytes> = grouped_swaps
.iter()
.flat_map(|grouped_swap| {
vec![grouped_swap.input_token.clone(), grouped_swap.output_token.clone()]
vec![grouped_swap.token_in.clone(), grouped_swap.token_out.clone()]
})
.collect();
let mut intermediary_tokens: Vec<Bytes> = intermediary_tokens
@@ -558,40 +552,33 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
))
})?;
let swap_receiver = if !unwrap && grouped_swap.output_token == solution.checked_token {
let swap_receiver = if !unwrap && grouped_swap.token_out == solution.checked_token {
solution.receiver.clone()
} else {
self.router_address.clone()
};
let transfer_type = self
.transfer_optimization
.get_transfer_type(grouped_swap.clone(), solution.given_token.clone(), wrap, false);
let encoding_context = EncodingContext {
receiver: swap_receiver.clone(),
exact_out: solution.exact_out,
router_address: Some(self.router_address.clone()),
group_token_in: grouped_swap.token_in.clone(),
group_token_out: grouped_swap.token_out.clone(),
transfer_type: transfer_type.clone(),
};
let mut grouped_protocol_data: Vec<u8> = vec![];
for swap in grouped_swap.swaps.iter() {
let transfer_type = self.get_transfer_type(
swap.clone(),
solution.given_token.clone(),
self.native_address.clone(),
self.wrapped_address.clone(),
self.permit2.clone().is_some(),
wrap,
false,
);
let encoding_context = EncodingContext {
receiver: swap_receiver.clone(),
exact_out: solution.exact_out,
router_address: Some(self.router_address.clone()),
group_token_in: grouped_swap.input_token.clone(),
group_token_out: grouped_swap.output_token.clone(),
transfer_type: transfer_type.clone(),
};
let protocol_data =
swap_encoder.encode_swap(swap.clone(), encoding_context.clone())?;
grouped_protocol_data.extend(protocol_data);
}
let swap_data = self.encode_swap_header(
get_token_position(tokens.clone(), grouped_swap.input_token.clone())?,
get_token_position(tokens.clone(), grouped_swap.output_token.clone())?,
get_token_position(tokens.clone(), grouped_swap.token_in.clone())?,
get_token_position(tokens.clone(), grouped_swap.token_out.clone())?,
percentage_to_uint24(grouped_swap.split),
Bytes::from_str(swap_encoder.executor_address()).map_err(|_| {
EncodingError::FatalError("Invalid executor address".to_string())
@@ -744,6 +731,7 @@ mod tests {
swap_encoder_registry,
Some(private_key),
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
false,
)
.unwrap();
let solution = Solution {
@@ -827,6 +815,7 @@ mod tests {
swap_encoder_registry,
None,
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
false,
)
.unwrap();
let solution = Solution {
@@ -876,6 +865,85 @@ mod tests {
println!("test_single_swap_strategy_encoder_no_permit2: {}", hex_calldata);
}
#[test]
fn test_single_swap_strategy_encoder_no_transfer_in() {
// Performs a single swap from WETH to DAI on a USV2 pool assuming that the tokens are
// already in the router
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap();
let expected_amount = Some(BigUint::from_str("2_650_000000000000000000").unwrap());
let slippage = Some(0.01f64);
let checked_amount = Some(BigUint::from_str("2_640_000000000000000000").unwrap());
let expected_min_amount = U256::from_str("2_640_000000000000000000").unwrap();
let swap = 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 swap_encoder_registry = get_swap_encoder_registry();
let encoder = SingleSwapStrategyEncoder::new(
eth_chain(),
swap_encoder_registry,
None,
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
true,
)
.unwrap();
let solution = Solution {
exact_out: false,
given_token: weth,
given_amount: BigUint::from_str("1_000000000000000000").unwrap(),
checked_token: dai,
expected_amount,
slippage,
checked_amount,
sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
swaps: vec![swap],
..Default::default()
};
let (calldata, _) = encoder
.encode_strategy(solution)
.unwrap();
let expected_min_amount_encoded = hex::encode(U256::abi_encode(&expected_min_amount));
let expected_input = [
"20144a07", // Function selector
"0000000000000000000000000000000000000000000000000de0b6b3a7640000", // amount in
"000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in
"0000000000000000000000006b175474e89094c44da98b954eedeac495271d0f", // token out
&expected_min_amount_encoded, // min amount out
"0000000000000000000000000000000000000000000000000000000000000000", // wrap
"0000000000000000000000000000000000000000000000000000000000000000", // unwrap
"000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver
"0000000000000000000000000000000000000000000000000000000000000100", // offset of swap bytes
"0000000000000000000000000000000000000000000000000000000000000052", // length of swap bytes without padding
// Swap data
"5615deb798bb3e4dfa0139dfa1b3d433cc23b72f", // executor address
"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // token in
"a478c2975ab1ea89e8196811f51a7b7ade33eb11", // component id
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver
"00", // zero2one
"00", // transfer type
"0000000000000000000000000000", // padding
]
.join("");
let hex_calldata = encode(&calldata);
assert_eq!(hex_calldata, expected_input);
println!("test_single_swap_strategy_encoder_no_transfer_in: {}", hex_calldata);
}
#[test]
fn test_single_swap_strategy_encoder_wrap() {
// Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH
@@ -904,6 +972,7 @@ mod tests {
swap_encoder_registry,
Some(private_key),
Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"),
false,
)
.unwrap();
let solution = Solution {
@@ -956,6 +1025,7 @@ mod tests {
swap_encoder_registry,
Some(private_key),
Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"),
false,
)
.unwrap();
let solution = Solution {
@@ -1027,6 +1097,7 @@ mod tests {
swap_encoder_registry,
Some(private_key),
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
false,
)
.unwrap();
let solution = Solution {
@@ -1086,6 +1157,7 @@ mod tests {
swap_encoder_registry,
None,
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
false,
)
.unwrap();
let solution = Solution {
@@ -1204,6 +1276,7 @@ mod tests {
swap_encoder_registry,
Some(private_key),
Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"),
false,
)
.unwrap();
@@ -1321,6 +1394,7 @@ mod tests {
swap_encoder_registry,
None,
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
false,
)
.unwrap();
let solution = Solution {
@@ -1402,6 +1476,7 @@ mod tests {
swap_encoder_registry,
None,
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
false,
)
.unwrap();
let solution = Solution {
@@ -1487,6 +1562,7 @@ mod tests {
swap_encoder_registry,
None,
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
false,
)
.unwrap();
let solution = Solution {
@@ -1553,6 +1629,7 @@ mod tests {
swap_encoder_registry,
None,
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
false,
)
.unwrap();
let solution = Solution {
@@ -1800,6 +1877,7 @@ mod tests {
swap_encoder_registry,
Some(private_key),
Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"),
false,
)
.unwrap();
let solution = Solution {
@@ -1909,6 +1987,7 @@ mod tests {
swap_encoder_registry,
Some(private_key.clone()),
Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"),
false,
)
.unwrap();
@@ -2071,6 +2150,7 @@ mod tests {
swap_encoder_registry,
Some(private_key.clone()),
Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"),
false,
)
.unwrap();
@@ -2196,6 +2276,7 @@ mod tests {
swap_encoder_registry,
None,
Bytes::from_str("0xA4AD4f68d0b91CFD19687c881e50f3A00242828c").unwrap(),
false,
)
.unwrap();
@@ -2261,6 +2342,7 @@ mod tests {
swap_encoder_registry,
Some(private_key),
Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"),
false,
)
.unwrap();
@@ -2329,6 +2411,7 @@ mod tests {
swap_encoder_registry,
Some(private_key),
Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"),
false,
)
.unwrap();
@@ -2416,6 +2499,7 @@ mod tests {
swap_encoder_registry,
Some(private_key),
Bytes::from("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395"),
false,
)
.unwrap();
let solution = Solution {
@@ -2518,6 +2602,7 @@ mod tests {
swap_encoder_registry,
None,
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
false,
)
.unwrap();
@@ -2580,6 +2665,7 @@ mod tests {
swap_encoder_registry,
None,
Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
false,
)
.unwrap();

View File

@@ -1,50 +1,86 @@
use std::str::FromStr;
use tycho_common::Bytes;
use crate::encoding::{
evm::constants::IN_TRANSFER_REQUIRED_PROTOCOLS,
models::{Swap, TransferType},
errors::EncodingError,
evm::{
constants::{CALLBACK_CONSTRAINED_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS},
group_swaps::SwapGroup,
},
models::TransferType,
};
/// A trait that defines how the tokens will be transferred into the given pool given the solution.
pub trait TransferOptimization {
/// Returns the transfer method that should be used for the given swap and solution.
#[allow(clippy::too_many_arguments)]
fn get_transfer_type(
&self,
swap: Swap,
given_token: Bytes,
/// A struct that defines how the tokens will be transferred into the given pool given the solution.
#[derive(Clone)]
pub struct TransferOptimization {
native_token: Bytes,
wrapped_token: Bytes,
permit2: bool,
token_in_already_in_router: bool,
router_address: Bytes,
}
impl TransferOptimization {
pub fn new(
native_token: Bytes,
wrapped_token: Bytes,
permit2: bool,
token_in_already_in_router: bool,
router_address: Bytes,
) -> Self {
TransferOptimization {
native_token,
wrapped_token,
permit2,
token_in_already_in_router,
router_address,
}
}
/// Returns the transfer method that should be used for the given swap and solution.
pub fn get_transfer_type(
&self,
swap: SwapGroup,
given_token: Bytes,
wrap: bool,
in_between_swap_optimization: bool,
) -> TransferType {
let in_transfer_required: bool =
IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&swap.component.protocol_system.as_str());
IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&swap.protocol_system.as_str());
let is_first_swap = swap.token_in == given_token;
if swap.token_in == native_token {
if swap.token_in == self.native_token {
// Funds are already in router. All protocols currently take care of native transfers.
TransferType::None
} else if (swap.token_in == wrapped_token) && wrap {
} else if (swap.token_in == self.wrapped_token) && wrap {
// Wrapping already happened in the router so we can just use a normal transfer.
TransferType::TransferToProtocol
} else if is_first_swap {
if in_transfer_required {
if permit2 {
if self.token_in_already_in_router {
// Transfer from router to pool.
TransferType::TransferToProtocol
} else if self.permit2 {
// Transfer from swapper to pool using permit2.
TransferType::TransferPermit2ToProtocol
} else {
// Transfer from swapper to pool.
TransferType::TransferFromToProtocol
}
} else if permit2 {
// Transfer from swapper to router using permit2.
TransferType::TransferPermit2ToRouter
// in transfer is not necessary for these protocols. Only make a transfer if the
// tokens are not already in the router
} else if !self.token_in_already_in_router {
if self.permit2 {
// Transfer from swapper to router using permit2.
TransferType::TransferPermit2ToRouter
} else {
// Transfer from swapper to router.
TransferType::TransferFromToRouter
}
} else {
// Transfer from swapper to router.
TransferType::TransferFromToRouter
TransferType::None
}
// all other swaps
} else if !in_transfer_required || in_between_swap_optimization {
@@ -54,17 +90,50 @@ pub trait TransferOptimization {
TransferType::TransferToProtocol
}
}
// Returns the optimized receiver of the swap. This is used to chain swaps together and avoid
// unnecessary token transfers.
// Returns the receiver address and a boolean indicating whether the receiver is optimized (this
// is necessary for the next swap transfer type decision).
pub fn get_receiver(
&self,
solution_receiver: Bytes,
next_swap: Option<&SwapGroup>,
) -> Result<(Bytes, bool), EncodingError> {
if let Some(next) = next_swap {
// if the protocol of the next swap supports transfer in optimization
if IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&next.protocol_system.as_str()) {
// if the protocol does not allow for chained swaps, we can't optimize the
// receiver of this swap nor the transfer in of the next swap
if CALLBACK_CONSTRAINED_PROTOCOLS.contains(&next.protocol_system.as_str()) {
Ok((self.router_address.clone(), false))
} else {
Ok((
Bytes::from_str(&next.swaps[0].component.id.clone()).map_err(|_| {
EncodingError::FatalError("Invalid component id".to_string())
})?,
true,
))
}
} else {
// the protocol of the next swap does not support transfer in optimization
Ok((self.router_address.clone(), false))
}
} else {
// last swap - there is no next swap
Ok((solution_receiver, false))
}
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::hex;
use tycho_common::{models::protocol::ProtocolComponent, Bytes};
use rstest::rstest;
use tycho_common::models::protocol::ProtocolComponent;
use super::*;
struct MockStrategy {}
impl TransferOptimization for MockStrategy {}
use crate::encoding::models::Swap;
fn weth() -> Bytes {
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
@@ -82,39 +151,37 @@ mod tests {
Bytes::from(hex!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").to_vec())
}
fn router_address() -> Bytes {
Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f")
}
#[test]
fn test_first_swap_transfer_from_permit2() {
// The swap token is the same as the given token, which is not the native token
let swap = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: weth(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let strategy = MockStrategy {};
let transfer_method =
strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), true, false, false);
let optimization = TransferOptimization::new(eth(), weth(), true, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
assert_eq!(transfer_method, TransferType::TransferPermit2ToProtocol);
}
#[test]
fn test_first_swap_transfer_from() {
// The swap token is the same as the given token, which is not the native token
let swap = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: weth(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let strategy = MockStrategy {};
let transfer_method =
strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, false);
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
assert_eq!(transfer_method, TransferType::TransferFromToProtocol);
}
@@ -122,18 +189,15 @@ mod tests {
fn test_first_swap_native() {
// The swap token is the same as the given token, and it's the native token.
// No transfer action is needed.
let swap = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: eth(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let strategy = MockStrategy {};
let transfer_method =
strategy.get_transfer_type(swap.clone(), eth(), eth(), weth(), false, false, false);
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), false, false);
assert_eq!(transfer_method, TransferType::None);
}
@@ -141,18 +205,15 @@ mod tests {
fn test_first_swap_wrapped() {
// The swap token is NOT the same as the given token, but we are wrapping.
// Since the swap's token in is the wrapped token - this is the first swap.
let swap = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: weth(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let strategy = MockStrategy {};
let transfer_method =
strategy.get_transfer_type(swap.clone(), eth(), eth(), weth(), false, true, false);
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), eth(), true, false);
assert_eq!(transfer_method, TransferType::TransferToProtocol);
}
@@ -160,18 +221,15 @@ mod tests {
fn test_not_first_swap() {
// The swap token is NOT the same as the given token, and we are NOT wrapping.
// Thus, this is not the first swap.
let swap = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let strategy = MockStrategy {};
let transfer_method =
strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, false);
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
assert_eq!(transfer_method, TransferType::TransferToProtocol);
}
@@ -179,18 +237,15 @@ mod tests {
fn test_not_first_swap_funds_in_router() {
// Not the first swap and the protocol requires the funds to be in the router (which they
// already are, so the transfer type is None)
let swap = Swap {
component: ProtocolComponent {
protocol_system: "vm:curve".to_string(),
..Default::default()
},
let swap = SwapGroup {
protocol_system: "vm:curve".to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let strategy = MockStrategy {};
let transfer_method =
strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, false);
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, false);
assert_eq!(transfer_method, TransferType::None);
}
@@ -198,18 +253,100 @@ mod tests {
fn test_not_first_swap_in_between_swap_optimization() {
// Not the first swap and the in between swaps are optimized. The funds should already be in
// the next pool or in the router
let swap = Swap {
component: ProtocolComponent {
protocol_system: "uniswap_v2".to_string(),
..Default::default()
},
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let strategy = MockStrategy {};
let transfer_method =
strategy.get_transfer_type(swap.clone(), weth(), eth(), weth(), false, false, true);
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), weth(), false, true);
assert_eq!(transfer_method, TransferType::None);
}
#[test]
fn test_first_swap_tokens_already_in_router_optimization() {
// It is the first swap, tokens are already in the router and the protocol requires the
// transfer in
let swap = SwapGroup {
protocol_system: "uniswap_v2".to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), false, true, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), usdc(), false, false);
assert_eq!(transfer_method, TransferType::TransferToProtocol);
}
#[test]
fn test_first_swap_tokens_already_in_router_no_transfer_needed_optimization() {
// It is the first swap, tokens are already in the router and the protocol does not require
// the transfer in
let swap = SwapGroup {
protocol_system: "vm:curve".to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![],
};
let optimization = TransferOptimization::new(eth(), weth(), false, true, router_address());
let transfer_method = optimization.get_transfer_type(swap.clone(), usdc(), false, false);
assert_eq!(transfer_method, TransferType::None);
}
fn receiver() -> Bytes {
Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2")
}
fn component_id() -> Bytes {
Bytes::from("0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11")
}
#[rstest]
// there is no next swap -> receiver is the solution receiver
#[case(None, receiver(), false)]
// protocol of next swap supports transfer in optimization
#[case(Some("uniswap_v2"), component_id(), true)]
// protocol of next swap supports transfer in optimization but is callback constrained
#[case(Some("uniswap_v3"), router_address(), false)]
// protocol of next swap does not support transfer in optimization
#[case(Some("vm:curve"), router_address(), false)]
fn test_get_receiver(
#[case] protocol: Option<&str>,
#[case] expected_receiver: Bytes,
#[case] expected_optimization: bool,
) {
let optimization = TransferOptimization::new(eth(), weth(), false, false, router_address());
let next_swap = if protocol.is_none() {
None
} else {
Some(SwapGroup {
protocol_system: protocol.unwrap().to_string(),
token_in: usdc(),
token_out: dai(),
split: 0f64,
swaps: vec![Swap {
component: ProtocolComponent {
protocol_system: protocol.unwrap().to_string(),
id: component_id().to_string(),
..Default::default()
},
token_in: usdc(),
token_out: dai(),
split: 0f64,
}],
})
};
let result = optimization.get_receiver(receiver(), next_swap.as_ref());
assert!(result.is_ok());
let (actual_receiver, optimization_flag) = result.unwrap();
assert_eq!(actual_receiver, expected_receiver);
assert_eq!(optimization_flag, expected_optimization);
}
}