This will block an attempt to transfer from the user when we expect the funds to already be in the router. chores: - add docs - in EncodingContext, rename transfer to transfer_type Took 58 minutes
258 lines
9.7 KiB
Rust
258 lines
9.7 KiB
Rust
use std::str::FromStr;
|
|
|
|
use tycho_common::Bytes;
|
|
|
|
use crate::encoding::{
|
|
errors::EncodingError,
|
|
evm::{
|
|
constants::{CALLBACK_CONSTRAINED_PROTOCOLS, IN_TRANSFER_REQUIRED_PROTOCOLS},
|
|
group_swaps::SwapGroup,
|
|
},
|
|
models::TransferType,
|
|
};
|
|
|
|
/// 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,
|
|
token_in_already_in_router: bool,
|
|
router_address: Bytes,
|
|
}
|
|
|
|
impl TransferOptimization {
|
|
pub fn new(
|
|
native_token: Bytes,
|
|
wrapped_token: Bytes,
|
|
token_in_already_in_router: bool,
|
|
router_address: Bytes,
|
|
) -> Self {
|
|
TransferOptimization {
|
|
native_token,
|
|
wrapped_token,
|
|
token_in_already_in_router,
|
|
router_address,
|
|
}
|
|
}
|
|
|
|
/// Returns the transfer type that should be used for the current transfer.
|
|
pub fn get_transfers(
|
|
&self,
|
|
swap: SwapGroup,
|
|
given_token: Bytes,
|
|
wrap: bool,
|
|
in_between_swap_optimization: bool,
|
|
) -> TransferType {
|
|
let is_first_swap = swap.token_in == given_token;
|
|
let in_transfer_required: bool =
|
|
IN_TRANSFER_REQUIRED_PROTOCOLS.contains(&swap.protocol_system.as_str());
|
|
|
|
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 == self.wrapped_token) && wrap {
|
|
// Wrapping already happened in the router so, we just do a normal transfer.
|
|
TransferType::Transfer
|
|
} else if is_first_swap {
|
|
if in_transfer_required {
|
|
if self.token_in_already_in_router {
|
|
// Transfer from router to pool.
|
|
TransferType::Transfer
|
|
} else {
|
|
// Transfer from swapper to pool
|
|
TransferType::TransferFrom
|
|
}
|
|
// in transfer is not necessary for these protocols. Only make a transfer from the
|
|
// swapper to the router if the tokens are not already in the router
|
|
} else if !self.token_in_already_in_router {
|
|
// Transfer from swapper to router using.
|
|
TransferType::TransferFrom
|
|
} else {
|
|
TransferType::None
|
|
}
|
|
// all other swaps that not the first one
|
|
} else if !in_transfer_required || in_between_swap_optimization {
|
|
// funds should already be in the router or in the next pool
|
|
TransferType::None
|
|
} else {
|
|
TransferType::Transfer
|
|
}
|
|
}
|
|
|
|
// 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 rstest::rstest;
|
|
use tycho_common::models::protocol::ProtocolComponent;
|
|
|
|
use super::*;
|
|
use crate::encoding::models::Swap;
|
|
|
|
fn weth() -> Bytes {
|
|
Bytes::from(hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").to_vec())
|
|
}
|
|
|
|
fn eth() -> Bytes {
|
|
Bytes::from(hex!("0000000000000000000000000000000000000000").to_vec())
|
|
}
|
|
|
|
fn dai() -> Bytes {
|
|
Bytes::from(hex!("6b175474e89094c44da98b954eedeac495271d0f").to_vec())
|
|
}
|
|
|
|
fn usdc() -> Bytes {
|
|
Bytes::from(hex!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").to_vec())
|
|
}
|
|
|
|
fn router_address() -> Bytes {
|
|
Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f")
|
|
}
|
|
|
|
#[rstest]
|
|
// First swap tests
|
|
// WETH -(univ2)-> DAI we expect a transfer from the user to the protocol
|
|
#[case(weth(), weth(), "uniswap_v2".to_string(), false, false,false, TransferType::TransferFrom)]
|
|
// Native token swap. No transfer is needed
|
|
#[case(eth(), eth(), "uniswap_v2".to_string(),false, false,false, TransferType::None)]
|
|
// ETH -(wrap)-> WETH -(univ2)-> DAI. Only a transfer from the router into the protocol is
|
|
// needed
|
|
#[case(eth(), weth(), "uniswap_v2".to_string(),true, false,false,TransferType::Transfer)]
|
|
// USDC -(univ2)-> DAI and the tokens are already in the router. Only a transfer from the router
|
|
// to the protocol is needed
|
|
#[case(usdc(), usdc(), "uniswap_v2".to_string(),false, true,false, TransferType::Transfer)]
|
|
// USDC -(curve)-> DAI and the tokens are already in the router. No transfer is needed
|
|
#[case(usdc(), usdc(), "vm:curve".to_string(),false, true, false,TransferType::None)]
|
|
// other swaps tests
|
|
// tokens need to be transferred into the pool
|
|
#[case(weth(), usdc(), "uniswap_v2".to_string(), false, false,false, TransferType::Transfer)]
|
|
// tokens are already in the pool (optimization)
|
|
#[case(weth(), usdc(), "uniswap_v2".to_string(), false, false, true, TransferType::None)]
|
|
// tokens are already in the router and don't need a transfer
|
|
#[case(weth(), usdc(), "vm:curve".to_string(), false, false, false, TransferType::None)]
|
|
fn test_get_transfers(
|
|
#[case] given_token: Bytes,
|
|
#[case] swap_token_in: Bytes,
|
|
#[case] protocol: String,
|
|
#[case] wrap: bool,
|
|
#[case] token_in_already_in_router: bool,
|
|
#[case] in_between_swap_optimization: bool,
|
|
#[case] expected_transfer: TransferType,
|
|
) {
|
|
// The swap token is the same as the given token, which is not the native token
|
|
let swaps = vec![Swap {
|
|
component: ProtocolComponent {
|
|
protocol_system: "uniswap_v2".to_string(),
|
|
id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(),
|
|
..Default::default()
|
|
},
|
|
token_in: swap_token_in.clone(),
|
|
token_out: dai(),
|
|
split: 0f64,
|
|
}];
|
|
let swap = SwapGroup {
|
|
protocol_system: protocol,
|
|
token_in: swap_token_in,
|
|
token_out: dai(),
|
|
split: 0f64,
|
|
swaps,
|
|
};
|
|
let optimization =
|
|
TransferOptimization::new(eth(), weth(), token_in_already_in_router, router_address());
|
|
let transfer = optimization.get_transfers(
|
|
swap.clone(),
|
|
given_token,
|
|
wrap,
|
|
in_between_swap_optimization,
|
|
);
|
|
assert_eq!(transfer, expected_transfer);
|
|
}
|
|
|
|
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, 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);
|
|
}
|
|
}
|