diff --git a/crates/uniswapx-rs/src/order.rs b/crates/uniswapx-rs/src/order.rs index 47e91af..62d1a49 100644 --- a/crates/uniswapx-rs/src/order.rs +++ b/crates/uniswapx-rs/src/order.rs @@ -94,14 +94,14 @@ impl Order { pub fn resolve(&self, timestamp: u64, priority_fee: Uint<256, 4>) -> OrderResolution { match self { Order::V2DutchOrder(order) => order.resolve(timestamp), - Order::PriorityOrder(order) => order.resolve(priority_fee), + Order::PriorityOrder(order) => order.resolve(timestamp, priority_fee), } } pub fn encode(&self) -> Vec { match self { - Order::V2DutchOrder(order) => order._encode(), - Order::PriorityOrder(order) => order._encode(), + Order::V2DutchOrder(order) => order.encode_inner(), + Order::PriorityOrder(order) => order.encode_inner(), } } } @@ -133,11 +133,11 @@ pub enum OrderResolution { } impl V2DutchOrder { - pub fn _decode(order_hex: &[u8], validate: bool) -> Result> { + pub fn decode_inner(order_hex: &[u8], validate: bool) -> Result> { Ok(V2DutchOrder::decode_single(order_hex, validate)?) } - pub fn _encode(&self) -> Vec { + pub fn encode_inner(&self) -> Vec { V2DutchOrder::encode_single(self) } @@ -194,15 +194,21 @@ impl V2DutchOrder { } impl PriorityOrder { - pub fn _decode(order_hex: &[u8], validate: bool) -> Result> { + pub fn decode_inner(order_hex: &[u8], validate: bool) -> Result> { Ok(PriorityOrder::decode_single(order_hex, validate)?) } - pub fn _encode(&self) -> Vec { + pub fn encode_inner(&self) -> Vec { PriorityOrder::encode_single(self) } - pub fn resolve(&self, priority_fee: Uint<256, 4>) -> OrderResolution { + pub fn resolve(&self, timestamp: u64, priority_fee: Uint<256, 4>) -> OrderResolution { + let timestamp = Uint::from(timestamp); + + if self.info.deadline.lt(×tamp) { + return OrderResolution::Expired; + }; + let input = self.input.scale(priority_fee); let outputs = self .outputs diff --git a/src/collectors/uniswapx_order_collector.rs b/src/collectors/uniswapx_order_collector.rs index de52e26..5db121a 100644 --- a/src/collectors/uniswapx_order_collector.rs +++ b/src/collectors/uniswapx_order_collector.rs @@ -200,7 +200,7 @@ mod tests { }; let order_hex: Vec = hex::decode(encoded_order).unwrap(); - let result = V2DutchOrder::_decode(&order_hex, false); + let result = V2DutchOrder::decode_inner(&order_hex, false); match result { Err(e) => panic!("Error decoding order: {:?}", e), _ => (), diff --git a/src/executors/mod.rs b/src/executors/mod.rs index 280b1af..c17998e 100644 --- a/src/executors/mod.rs +++ b/src/executors/mod.rs @@ -1 +1,2 @@ pub mod protect_executor; +pub mod public_1559_executor; \ No newline at end of file diff --git a/src/executors/protect_executor.rs b/src/executors/protect_executor.rs index 03552e8..691a9e0 100644 --- a/src/executors/protect_executor.rs +++ b/src/executors/protect_executor.rs @@ -8,7 +8,7 @@ use anyhow::{Context, Result}; use artemis_core::executors::mempool_executor::SubmitTxToMempool; use artemis_core::types::Executor; use async_trait::async_trait; -use ethers::providers::Middleware; +use ethers::{providers::Middleware, types::U256}; /// An executor that sends transactions to the mempool. pub struct ProtectExecutor { @@ -35,7 +35,6 @@ where { /// Send a transaction to the mempool. async fn execute(&self, mut action: SubmitTxToMempool) -> Result<()> { - info!("Executing tx {:?}", action.tx); let gas_usage_result = self .client .estimate_gas(&action.tx, None) @@ -60,6 +59,9 @@ where .context("Error getting gas price: {}")?; } action.tx.set_gas_price(bid_gas_price); + // set max_priority_fee_per_gas to be 0 + action.tx.as_eip1559_mut().unwrap().max_priority_fee_per_gas = Some(U256::from(50)); + info!("Executing tx {:?}", action.tx); self.sender_client.send_transaction(action.tx, None).await?; Ok(()) } diff --git a/src/executors/public_1559_executor.rs b/src/executors/public_1559_executor.rs new file mode 100644 index 0000000..6b8b11a --- /dev/null +++ b/src/executors/public_1559_executor.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; +use tracing::info; + +use anyhow::{Context, Result}; +use artemis_core::types::Executor; +use async_trait::async_trait; +use ethers::{providers::Middleware, types::U256}; + +use crate::strategies::types::SubmitTxToMempoolWithAdvancedProfitCalculation; + +/// An executor that sends transactions to the public mempool. +pub struct Public1559Executor { + client: Arc, + sender_client: Arc, +} + +impl Public1559Executor { + pub fn new(client: Arc, sender_client: Arc) -> Self { + Self { + client, + sender_client, + } + } +} + +#[async_trait] +impl Executor for Public1559Executor +where + M: Middleware, + M::Error: 'static, + N: Middleware, + N::Error: 'static, +{ + /// Send a transaction to the mempool. + async fn execute(&self, mut action: SubmitTxToMempoolWithAdvancedProfitCalculation) -> Result<()> { + let gas_usage_result = self + .client + .estimate_gas(&action.execution.tx, None) + .await + .context("Error estimating gas usage: {}"); + info!("Gas Usage {:?}", gas_usage_result); + // let gas_usage = gas_usage_result?; + + let bid_priority_fee; + let base_fee: U256 = self + .client + .get_gas_price() + .await + .context("Error getting gas price: {}")?; + + if let Some(gas_bid_info) = action.execution.gas_bid_info { + // priority fee at which we'd break even, meaning 100% of profit goes to user in the form of price improvement + // TODO: use gas estimate here + bid_priority_fee = action.profit_calculation.calculate_priority_fee(gas_bid_info.bid_percentage) + } else { + bid_priority_fee = Some(U256::from(50)); + } + action.execution.tx.as_eip1559_mut().unwrap().max_fee_per_gas = Some(base_fee); + action.execution.tx.as_eip1559_mut().unwrap().max_priority_fee_per_gas = bid_priority_fee; + info!("Executing tx {:?}", action.execution.tx); + self.sender_client.send_transaction(action.execution.tx, None).await?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 4a90bf3..6fee032 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ use ethers::{ signers::{LocalWallet, Signer}, }; use executors::protect_executor::ProtectExecutor; +use executors::public_1559_executor::Public1559Executor; use std::sync::Arc; use strategies::priority_strategy::UniswapXPriorityFill; use strategies::{ @@ -158,7 +159,7 @@ async fn main() -> Result<()> { mevblocker_provider.clone(), )); - let public_tx_executor = Box::new(ProtectExecutor::new(provider.clone(), provider.clone())); + let public_tx_executor = Box::new(Public1559Executor::new(provider.clone(), provider.clone())); let protect_executor = ExecutorMap::new(protect_executor, |action| match action { Action::SubmitTx(tx) => Some(tx), @@ -167,7 +168,7 @@ async fn main() -> Result<()> { }); let public_tx_executor = ExecutorMap::new(public_tx_executor, |action| match action { - Action::SubmitPublicTx(tx) => Some(tx), + Action::SubmitPublicTx(execution) => Some(execution), // No op for protected transactions _ => None, }); diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index c9791f9..fe4768e 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -2,11 +2,11 @@ use super::{ shared::UniswapXStrategy, types::{Config, OrderStatus}, }; -use crate::collectors::{ +use crate::{collectors::{ block_collector::NewBlock, uniswapx_order_collector::UniswapXOrder, uniswapx_route_collector::{OrderBatchData, OrderData, PriorityOrderData, RoutedOrder}, -}; +}, strategies::types::SubmitTxToMempoolWithAdvancedProfitCalculation}; use alloy_primitives::Uint; use anyhow::Result; use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; @@ -28,11 +28,48 @@ use uniswapx_rs::order::{OrderResolution, PriorityOrder}; use super::types::{Action, Event}; +const BLOCK_TIME: u64 = 2; const DONE_EXPIRY: u64 = 300; // Base addresses const REACTOR_ADDRESS: &str = "0x000000001Ec5656dcdB24D90DFa42742738De729"; pub const WETH_ADDRESS: &str = "0x4200000000000000000000000000000000000006"; +#[derive(Debug, Clone)] +pub struct ProfitCalculation { + // amount of quote token we can get + quote: U256, + // amount of quote token needed to fill the order + amount_out_required: U256, +} + +impl ProfitCalculation { + pub fn new(quote: U256, amount_out_required: U256) -> Self { + Self { + quote, + amount_out_required, + } + } + + pub fn calculate_priority_fee(&self, bid_percentage: u64) -> Option { + let mps = U256::from(10_000_000); + + if self.quote.le(&self.amount_out_required) { + return None; + } + + let profit_quote = self.quote.saturating_sub(self.amount_out_required); + + let mps_of_improvement = profit_quote + .saturating_mul(mps) + .checked_div(self.amount_out_required)?; + info!("mps_of_improvement: {}", mps_of_improvement); + let priority_fee = mps_of_improvement + .checked_mul(U256::from(bid_percentage))? + .checked_div(U256::from(100))?; + return Some(priority_fee); + } +} + #[derive(Debug)] #[allow(dead_code)] pub struct UniswapXPriorityFill { @@ -106,7 +143,7 @@ impl UniswapXPriorityFill { }; let order_hex = hex::decode(encoded_order)?; - Ok(PriorityOrder::_decode(&order_hex, false)?) + Ok(PriorityOrder::decode_inner(&order_hex, false)?) } async fn process_order_event(&mut self, event: UniswapXOrder) -> Option { @@ -140,25 +177,27 @@ impl UniswapXPriorityFill { .. } = &event.request; - if let Some(profit) = self.get_profit_eth(&event) { + if let Some(profit) = self.get_profit_calculation(&event) { info!( - "Sending trade: num trades: {} routed quote: {}, batch needs: {}, profit: {} wei", + "Sending trade: num trades: {} routed quote: {}, batch needs: {}", orders.len(), event.route.quote, amount_out_required, - profit ); let signed_orders = self.get_signed_orders(orders.clone()).ok()?; - return Some(Action::SubmitPublicTx(SubmitTxToMempool { - tx: self - .build_fill(self.client.clone(), &self.executor_address, signed_orders, event) - .await - .ok()?, - gas_bid_info: Some(GasBidInfo { - bid_percentage: self.bid_percentage, - total_profit: profit, - }), + return Some(Action::SubmitPublicTx(SubmitTxToMempoolWithAdvancedProfitCalculation { + execution: SubmitTxToMempool { + tx: self + .build_fill(self.client.clone(), &self.executor_address, signed_orders, event) + .await + .ok()?, + gas_bid_info: Some(GasBidInfo { + bid_percentage: self.bid_percentage, + total_profit: U256::from(0), + }), + }, + profit_calculation: profit, })); } @@ -199,7 +238,7 @@ impl UniswapXPriorityFill { match batch { OrderData::PriorityOrderData(order) => { signed_orders.push(SignedOrder { - order: Bytes::from(order.order._encode()), + order: Bytes::from(order.order.encode_inner()), sig: Bytes::from_str(&order.signature)?, }); } @@ -264,29 +303,24 @@ impl UniswapXPriorityFill { /// - we have to bid at least the base fee /// - the priority fee set for the transaction is essentially total_profit_eth - base_fee /// - at 100% bid_percentage, our priority fee is total_profit_eth and thus gives the maximum amount to the user - fn get_profit_eth(&self, RoutedOrder { request, route }: &RoutedOrder) -> Option { + fn get_profit_calculation(&self, RoutedOrder { request, route }: &RoutedOrder) -> Option { let quote = U256::from_str_radix(&route.quote, 10).ok()?; let amount_out_required = U256::from_str_radix(&request.amount_out_required.to_string(), 10).ok()?; if quote.le(&amount_out_required) { return None; } - let profit_quote = quote.saturating_sub(amount_out_required); - if request.token_out.to_lowercase() == WETH_ADDRESS.to_lowercase() { - return Some(profit_quote); - } - - let gas_use_eth = U256::from_str_radix(&route.gas_use_estimate, 10) - .ok()? - .saturating_mul(U256::from_str_radix(&route.gas_price_wei, 10).ok()?); - profit_quote - .saturating_mul(gas_use_eth) - .checked_div(U256::from_str_radix(&route.gas_use_estimate_quote, 10).ok()?) + return Some({ + ProfitCalculation { + quote, + amount_out_required, + } + }) } fn update_order_state(&mut self, order: PriorityOrder, signature: String, order_hash: String) { - let resolved = order.resolve(Uint::from(0)); + let resolved = order.resolve( self.last_block_timestamp + BLOCK_TIME, Uint::from(0)); let order_status: OrderStatus = match resolved { OrderResolution::Expired => OrderStatus::Done, OrderResolution::Invalid => OrderStatus::Done, diff --git a/src/strategies/types.rs b/src/strategies/types.rs index 1eaf2da..3c0cf19 100644 --- a/src/strategies/types.rs +++ b/src/strategies/types.rs @@ -5,6 +5,8 @@ use crate::collectors::{ use artemis_core::executors::mempool_executor::SubmitTxToMempool; use uniswapx_rs::order::ResolvedOrder; +use super::priority_strategy::ProfitCalculation; + /// Core Event enum for the current strategy. #[derive(Debug, Clone)] pub enum Event { @@ -14,11 +16,17 @@ pub enum Event { UniswapXRoute(Box), } +#[derive(Debug, Clone)] +pub struct SubmitTxToMempoolWithAdvancedProfitCalculation { + pub execution: SubmitTxToMempool, + pub profit_calculation: ProfitCalculation, +} + /// Core Action enum for the current strategy. #[derive(Debug, Clone)] pub enum Action { SubmitTx(SubmitTxToMempool), - SubmitPublicTx(SubmitTxToMempool), + SubmitPublicTx(SubmitTxToMempoolWithAdvancedProfitCalculation), } /// Configuration for variables we need to pass to the strategy. diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 1aeab95..c71b61c 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -105,7 +105,7 @@ impl UniswapXUniswapFill { }; let order_hex: Vec = hex::decode(encoded_order)?; - Ok(V2DutchOrder::_decode(&order_hex, false)?) + Ok(V2DutchOrder::decode_inner(&order_hex, false)?) } // Process new orders as they come in. @@ -198,7 +198,7 @@ impl UniswapXUniswapFill { match batch { OrderData::V2DutchOrderData(order) => { signed_orders.push(SignedOrder { - order: Bytes::from(order.order._encode()), + order: Bytes::from(order.order.encode_inner()), sig: Bytes::from_str(&order.signature)?, }); }