diff --git a/Cargo.toml b/Cargo.toml index 62c3807..3f6057e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "1.0.0" +version = "0.1.0" edition = "2021" [dependencies] diff --git a/src/encoding/approvals_manager.rs b/src/encoding/approvals_manager.rs new file mode 100644 index 0000000..fb37bfb --- /dev/null +++ b/src/encoding/approvals_manager.rs @@ -0,0 +1,51 @@ +use std::{env, sync::Arc}; + +use alloy::{ + providers::{Provider, ProviderBuilder, RootProvider}, + transports::BoxTransport, +}; +use alloy_primitives::{Address, U256}; +use dotenv::dotenv; +use tycho_core::Bytes; + +pub struct TokenApprovalsManager { + client: Arc>, +} + +impl TokenApprovalsManager { + pub fn new(client: Arc>) -> Self { + Self { client } + } + + pub async fn approval_needed( + &self, + token: Bytes, + spender_address: Address, + router_address: Address, + ) -> bool { + // should be something like + // let allowance = self + // .client + // .call(token, "allowance(address,address)(uint256)", (router_address, spender_address)) + // .await; + // + // allowance == U256::ZERO // If allowance is 0, approval is needed + todo!() + } +} + +pub fn get_client() -> Arc> { + dotenv().ok(); + let eth_rpc_url = env::var("ETH_RPC_URL").expect("Missing ETH_RPC_URL in environment"); + let runtime = tokio::runtime::Handle::try_current() + .is_err() + .then(|| tokio::runtime::Runtime::new().unwrap()) + .unwrap(); + let client = runtime.block_on(async { + ProviderBuilder::new() + .on_builtin(ð_rpc_url) + .await + .unwrap() + }); + Arc::new(client) +} diff --git a/src/encoding/mod.rs b/src/encoding/mod.rs new file mode 100644 index 0000000..538b893 --- /dev/null +++ b/src/encoding/mod.rs @@ -0,0 +1,6 @@ +mod approvals_manager; +mod models; +mod router_encoder; +mod strategy_encoder; +mod swap_encoder; +mod utils; diff --git a/src/encoding/models.rs b/src/encoding/models.rs new file mode 100644 index 0000000..7076d49 --- /dev/null +++ b/src/encoding/models.rs @@ -0,0 +1,61 @@ +use alloy_primitives::Address; +use num_bigint::BigUint; +use tycho_core::{dto::ProtocolComponent, Bytes}; + +pub struct Solution { + pub orders: Vec, + // if not set, then the Propeller Router will be used + pub router_address: Option
, +} + +pub struct Order { + /// True if the order is an exact output order. + pub exact_out: bool, + /// The token being sold (exact in) or bought (exact out). + given_token: Bytes, + /// Amount of the given token. + pub given_amount: BigUint, + /// The token being bought (exact in) or sold (exact out). + checked_token: Bytes, + /// Amount of the checked token. + checked_amount: BigUint, + /// Address of the sender. + sender: Bytes, + /// Address of the receiver. + pub receiver: Bytes, + /// List of swaps to fulfill the order. + pub swaps: Vec, + /// Whether to include router calldata (true) or just swap data (false). + add_router_calldata: bool, + + pub slippage: f64, + pub min_checked_amount: Option, +} + +#[derive(Clone)] +pub struct Swap { + /// Protocol component from tycho indexer + pub component: ProtocolComponent, + /// Token being input into the pool. + pub token_in: Bytes, + /// Token being output from the pool. + pub token_out: Bytes, + /// Fraction of the amount to be swapped in this operation. + pub split: f64, +} + +// maybe this struct is useful - keeping it here for now (maybe we could collapse this with another +// struct) +pub struct EncodingContext { + pub receiver: Address, + pub exact_out: bool, + pub router_address: Address, +} + +pub enum ActionType { + SingleExactIn = 1, + SingleExactOut = 2, + SequentialExactIn = 3, + SequentialExactOut = 4, + SplitIn = 5, +} diff --git a/src/encoding/router_encoder.rs b/src/encoding/router_encoder.rs new file mode 100644 index 0000000..958c140 --- /dev/null +++ b/src/encoding/router_encoder.rs @@ -0,0 +1,61 @@ +use crate::encoding::models::{Order, Solution}; +use crate::encoding::strategy_encoder::{ + SequentialExactInStrategyEncoder, SingleSwapStrategyEncoder, SlipSwapStrategyEncoder, + StrategyEncoder, +}; +use crate::encoding::utils::{encode_input, ple_encode}; +use alloy_primitives::Address; +use alloy_sol_types::SolValue; +use anyhow::Error; +use std::env; +use std::str::FromStr; + +struct RouterEncoder { + router_address: Address, +} + +impl RouterEncoder { + pub fn new() -> Self { + let router_address = Address::from_str( + &env::var("ROUTER_ADDRESS").expect("Missing ROUTER_ADDRESS in environment"), + ) + .expect("Invalid ROUTER_ADDRESS"); + Self { router_address } + } + + pub fn encode_router_calldata(&self, solution: Solution) -> Result, Error> { + let mut calldata_list: Vec> = Vec::new(); + let encode_for_batch_execute = solution.orders.len() > 1; + for order in solution.orders { + let strategy = self.get_strategy(&order); + // TODO: handle native action?? + + let contract_interaction = strategy.encode_strategy( + order, + if solution.router_address.is_some() { + solution.router_address.unwrap() + } else { + self.router_address + }, + encode_for_batch_execute, + )?; + calldata_list.push(contract_interaction); + } + if encode_for_batch_execute { + let args = (false, ple_encode(calldata_list)); + Ok(encode_input("batchExecute(bytes)", args.abi_encode())) + } else { + Ok(calldata_list[0].clone()) + } + } + + fn get_strategy(&self, order: &Order) -> &dyn StrategyEncoder { + if order.swaps.len() == 1 { + &SingleSwapStrategyEncoder {} + } else if order.swaps.iter().all(|s| s.split == 0.0) { + &SequentialExactInStrategyEncoder {} + } else { + &SlipSwapStrategyEncoder {} + } + } +} diff --git a/src/encoding/strategy_encoder.rs b/src/encoding/strategy_encoder.rs new file mode 100644 index 0000000..dafd1c4 --- /dev/null +++ b/src/encoding/strategy_encoder.rs @@ -0,0 +1,130 @@ +use alloy_primitives::Address; +use alloy_sol_types::SolValue; +use anyhow::Error; +use num_bigint::BigUint; +use std::cmp::min; + +use crate::encoding::models::{ActionType, EncodingContext, Order}; +use crate::encoding::swap_encoder::{get_swap_encoder, get_swap_executor_address}; +use crate::encoding::utils::{biguint_to_u256, bytes_to_address, encode_input, ple_encode}; + +pub trait StrategyEncoder { + fn encode_strategy( + &self, + to_encode: Order, + router_address: Address, + encode_for_batch_execute: bool, + ) -> Result, Error>; + + fn encode_protocol_header( + &self, + protocol_data: Vec, + protocol_system: String, + // Token indices, split, and token inclusion are only used for split swaps + token_in: u16, + token_out: u16, + split: u16, // not sure what should be the type of this :/ + ) -> Vec { + let executor_address = get_swap_executor_address(&protocol_system); + let args = (executor_address, token_in, token_out, split, protocol_data); + args.abi_encode() + } +} + +pub struct SingleSwapStrategyEncoder {} + +impl StrategyEncoder for SingleSwapStrategyEncoder { + fn encode_strategy( + &self, + order: Order, + router_address: Address, + encode_for_batch_execute: bool, + ) -> Result, Error> { + todo!() + } +} + +pub struct SequentialExactInStrategyEncoder {} + +impl StrategyEncoder for SequentialExactInStrategyEncoder { + fn encode_strategy( + &self, + order: Order, + router_address: Address, + encode_for_batch_execute: bool, + ) -> Result, Error> { + let one_hundred = BigUint::from(100u32); + let slippage_percent = BigUint::from((order.slippage * 100.0) as u32); + let multiplier = &one_hundred - slippage_percent; + let slippage_buy_amount = (&order.given_amount * multiplier) / one_hundred; + + let min_checked_amount = if order.min_checked_amount.is_some() { + min(order.min_checked_amount.unwrap(), slippage_buy_amount) + } else { + slippage_buy_amount + }; + let mut swaps = vec![]; + for (index, swap) in order.swaps.iter().enumerate() { + let is_last = index == order.swaps.len() - 1; + let protocol_system = swap.component.protocol_system.clone(); + let swap_encoder = get_swap_encoder(&protocol_system); + let receiver = if is_last { + bytes_to_address(&order.receiver)? + } else { + router_address + }; + let encoding_context = EncodingContext { + receiver, + exact_out: order.exact_out, + router_address, + }; + let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context)?; + let swap_data = self.encode_protocol_header(protocol_data, protocol_system, 0, 0, 0); + swaps.push(swap_data); + } + let (selector, action_type) = if order.exact_out { + ( + "sequentialExactOut(uint256, uint256, bytes[])", + ActionType::SequentialExactOut, + ) + } else { + ( + "sequentialExactIn(uint256, uint256, bytes[])", + ActionType::SequentialExactIn, + ) + }; + let encoded_swaps = ple_encode(swaps); + if encode_for_batch_execute { + let args = ( + action_type as u16, + biguint_to_u256(&order.given_amount), + biguint_to_u256(&min_checked_amount), + encoded_swaps, + ); + Ok(args.abi_encode()) + } else { + Ok(encode_input( + selector, + ( + biguint_to_u256(&order.given_amount), + biguint_to_u256(&min_checked_amount), + encoded_swaps, + ) + .abi_encode(), + )) + } + } +} + +pub struct SlipSwapStrategyEncoder {} + +impl StrategyEncoder for SlipSwapStrategyEncoder { + fn encode_strategy( + &self, + order: Order, + router_address: Address, + encode_for_batch_execute: bool, + ) -> Result, Error> { + todo!() + } +} diff --git a/src/encoding/swap_encoder.rs b/src/encoding/swap_encoder.rs new file mode 100644 index 0000000..0c025d5 --- /dev/null +++ b/src/encoding/swap_encoder.rs @@ -0,0 +1,86 @@ +use alloy_primitives::Address; +use alloy_sol_types::SolValue; +use anyhow::Error; +use std::str::FromStr; + +use crate::encoding::utils::bytes_to_address; +use crate::encoding::{ + approvals_manager::{get_client, TokenApprovalsManager}, + models::{EncodingContext, Swap}, +}; + +pub trait SwapEncoder: Sync + Send { + fn encode_swap(&self, swap: Swap, encoding_context: EncodingContext) -> Result, Error>; +} + +struct UniswapV2SwapEncoder {} + +impl SwapEncoder for UniswapV2SwapEncoder { + fn encode_swap(&self, swap: Swap, encoding_context: EncodingContext) -> Result, Error> { + todo!() + } +} + +struct BalancerV2SwapEncoder { + vault_address: Option
, +} + +impl BalancerV2SwapEncoder { + pub fn new() -> Self { + Self { + vault_address: Some( + Address::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8") + .expect("Invalid string for balancer vault address"), + ), + } + } +} + +impl SwapEncoder for BalancerV2SwapEncoder { + fn encode_swap(&self, swap: Swap, encoding_context: EncodingContext) -> Result, Error> { + let client = get_client(); + let token_approvals_manager = TokenApprovalsManager::new(client); + let runtime = tokio::runtime::Handle::try_current() + .is_err() + .then(|| tokio::runtime::Runtime::new().unwrap()) + .unwrap(); + let approval_needed = runtime.block_on(async { + token_approvals_manager + .approval_needed( + swap.token_in.clone(), + encoding_context.router_address, + self.vault_address.unwrap(), + ) + .await + }); + // should we return gas estimation here too?? if there is an approval needed, gas will be + // higher. + let args = ( + bytes_to_address(&swap.token_in)?, + bytes_to_address(&swap.token_out)?, + swap.component.id, + encoding_context.receiver, + encoding_context.exact_out, + approval_needed, + ); + Ok(args.abi_encode()) + } +} + +pub fn get_swap_encoder(protocol_system: &str) -> Box { + match protocol_system { + "uniswap_v2" => Box::new(UniswapV2SwapEncoder {}), + "vm:balancer_v2" => Box::new(BalancerV2SwapEncoder::new()), + _ => panic!("Unknown protocol system: {}", protocol_system), + } +} + +pub fn get_swap_executor_address(protocol_system: &str) -> Address { + match protocol_system { + "uniswap_v2" => Address::from_str("0x5C2F5a71f67c01775180ADc06909288B4C329308") + .expect("Invalid address"), + "vm:balancer_v2" => Address::from_str("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4") + .expect("Invalid address"), + _ => panic!("Unknown protocol system: {}", protocol_system), + } +} diff --git a/src/encoding/utils.rs b/src/encoding/utils.rs new file mode 100644 index 0000000..a2c9683 --- /dev/null +++ b/src/encoding/utils.rs @@ -0,0 +1,56 @@ +use alloy_primitives::{Address, Keccak256, U256}; +use alloy_sol_types::SolValue; +use anyhow::Error; +use num_bigint::BigUint; +use tycho_core::Bytes; + +/// Safely converts a `Bytes` object to an `Address` object. +/// +/// Checks the length of the `Bytes` before attempting to convert, and returns a `SimulationError` +/// if not 20 bytes long. +pub fn bytes_to_address(address: &Bytes) -> Result { + if address.len() == 20 { + Ok(Address::from_slice(address)) + } else { + Err(anyhow::format_err!( + "Invalid ERC20 token address: {:?}", + address + )) + } +} +pub fn biguint_to_u256(value: &BigUint) -> U256 { + let bytes = value.to_bytes_be(); + U256::from_be_slice(&bytes) +} + +pub fn ple_encode(action_data_array: Vec>) -> Vec { + let mut encoded_action_data: Vec = Vec::new(); + + for action_data in action_data_array { + let args = (encoded_action_data, action_data.len() as u16, action_data); + encoded_action_data = args.abi_encode(); + } + + encoded_action_data +} + +pub fn encode_input(selector: &str, mut encoded_args: Vec) -> Vec { + let mut hasher = Keccak256::new(); + hasher.update(selector.as_bytes()); + let selector_bytes = &hasher.finalize()[..4]; + let mut call_data = selector_bytes.to_vec(); + // Remove extra prefix if present (32 bytes for dynamic data) + // Alloy encoding is including a prefix for dynamic data indicating the offset or length + // but at this point we don't want that + if encoded_args.len() > 32 + && encoded_args[..32] + == [0u8; 31] + .into_iter() + .chain([32].to_vec()) + .collect::>() + { + encoded_args = encoded_args[32..].to_vec(); + } + call_data.extend(encoded_args); + call_data +} diff --git a/src/lib.rs b/src/lib.rs index 5512a6a..b8120c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1 @@ -#[allow(dead_code)] -fn main() { - println!("Hello, world!"); -} - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} +mod encoding; \ No newline at end of file