From 2d49f380963f57f186b8a2b218b2de3ffc2b9d55 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Thu, 11 Jul 2024 14:30:58 -0400 Subject: [PATCH 01/31] Add new shared Order trait and add PriorityOrder structs --- crates/uniswapx-rs/src/order.rs | 104 ++++++++++++++++++++++++++-- src/strategies/uniswapx_strategy.rs | 4 +- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/crates/uniswapx-rs/src/order.rs b/crates/uniswapx-rs/src/order.rs index 9b8e9e1..7984e29 100644 --- a/crates/uniswapx-rs/src/order.rs +++ b/crates/uniswapx-rs/src/order.rs @@ -1,3 +1,5 @@ +use std::error::Error; + use alloy_primitives::Uint; use alloy_sol_types::{sol, SolType}; use anyhow::Result; @@ -38,9 +40,47 @@ sol! { DutchInput input; DutchOutput[] outputs; } + + #[derive(Debug)] + struct PriorityInput { + address token; + uint256 amount; + uint256 mpsPerPriorityFeeWei; + } + + #[derive(Debug)] + struct PriorityOutput { + address token; + uint256 amount; + uint256 mpsPerPriorityFeeWei; + address recipient; + } + + #[derive(Debug)] + struct PriorityCosignerData { + uint256 auctionTargetBlock; + } + + #[derive(Debug)] + struct PriorityOrder { + OrderInfo info; + address cosigner; + uint256 auctionStartBlock; + uint256 baselinePriorityFeeWei; + PriorityInput input; + PriorityOutput[] outputs; + PriorityCosignerData cosignerData; + bytes cosignature; + } } -pub fn decode_order(encoded_order: &str) -> Result { +pub trait Order: Sized { + fn _decode(order_hex: &[u8], validate: bool) -> Result>; + fn _encode(&self) -> Vec; +} + +// Generic function to decode orders +pub fn decode_order(encoded_order: &str) -> Result> { let encoded_order = if encoded_order.starts_with("0x") { &encoded_order[2..] } else { @@ -48,11 +88,12 @@ pub fn decode_order(encoded_order: &str) -> Result { }; let order_hex = hex::decode(encoded_order)?; - Ok(ExclusiveDutchOrder::decode(&order_hex, false)?) + T::_decode(&order_hex, false) } -pub fn encode_order(order: &ExclusiveDutchOrder) -> Vec { - ExclusiveDutchOrder::encode(order) +// Generic function to encode orders +pub fn encode_order(order: &T) -> Vec { + order._encode() } #[derive(Debug, Clone)] @@ -81,6 +122,16 @@ pub enum OrderResolution { Invalid, } +impl Order for ExclusiveDutchOrder { + fn _decode(order_hex: &[u8], validate: bool) -> Result> { + Ok(ExclusiveDutchOrder::decode(order_hex, validate)?) + } + + fn _encode(&self) -> Vec { + ExclusiveDutchOrder::encode(self) + } +} + impl ExclusiveDutchOrder { pub fn resolve(&self, timestamp: u64) -> OrderResolution { let timestamp = Uint::from(timestamp); @@ -133,6 +184,51 @@ impl ExclusiveDutchOrder { } } +impl Order for PriorityOrder { + fn _decode(order_hex: &[u8], validate: bool) -> Result> { + Ok(PriorityOrder::decode(order_hex, validate)?) + } + + fn _encode(&self) -> Vec { + PriorityOrder::encode(self) + } +} + +impl PriorityOrder { + pub fn resolve(&self, priority_fee: Uint<256, 4>) -> OrderResolution { + let input = self.input.scale(priority_fee); + let outputs = self + .outputs + .iter() + .map(|output| output.scale(priority_fee)) + .collect(); + + OrderResolution::Resolved(ResolvedOrder { input, outputs }) + } +} + +impl PriorityInput { + pub fn scale(&self, priority_fee: Uint<256, 4>) -> ResolvedInput { + let amount = self.amount.wrapping_mul(Uint::from(1e7).wrapping_add(priority_fee.wrapping_mul(self.mpsPerPriorityFeeWei))).wrapping_div(Uint::from(1e7)); + ResolvedInput { + token: self.token.to_string(), + amount, + } + } +} + +impl PriorityOutput { + pub fn scale(&self, priority_fee: Uint<256, 4>) -> ResolvedOutput { + let amount = self.amount.wrapping_mul(Uint::from(1e7).saturating_sub(priority_fee.wrapping_mul(self.mpsPerPriorityFeeWei))).wrapping_div(Uint::from(1e7)); + ResolvedOutput { + token: self.token.to_string(), + amount, + recipient: self.recipient.to_string(), + } + } + +} + fn resolve_decay( at_time: Uint<256, 4>, start_time: Uint<256, 4>, diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 97ad251..05a3773 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -24,7 +24,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::{error, info}; use uniswapx_rs::order::{ - decode_order, encode_order, ExclusiveDutchOrder, OrderResolution, ResolvedOrder, + decode_order, encode_order, ExclusiveDutchOrder, OrderResolution, ResolvedOrder }; use super::types::{Action, Event}; @@ -112,7 +112,7 @@ impl UniswapXUniswapFill { return None; } - let order = decode_order(&event.encoded_order) + let order: ExclusiveDutchOrder = decode_order(&event.encoded_order) .map_err(|e| error!("failed to decode: {}", e)) .ok()?; From a5e7f2cc29cca9883d37c5e15eee7b5898c796c2 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Thu, 11 Jul 2024 14:35:11 -0400 Subject: [PATCH 02/31] remove uneeded typing --- src/strategies/uniswapx_strategy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 05a3773..32a7d75 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -112,7 +112,7 @@ impl UniswapXUniswapFill { return None; } - let order: ExclusiveDutchOrder = decode_order(&event.encoded_order) + let order = decode_order(&event.encoded_order) .map_err(|e| error!("failed to decode: {}", e)) .ok()?; From b916ad9ae17f4fc6569fcca793a4b514daeae7c5 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Thu, 11 Jul 2024 14:47:15 -0400 Subject: [PATCH 03/31] fix build error --- src/strategies/uniswapx_strategy.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 32a7d75..18def21 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -204,9 +204,8 @@ impl UniswapXUniswapFill { Token::Array(vec![Token::Address(H160::from_str(&request.token_in)?)]), Token::Bytes(Bytes::from_str(&route.method_parameters.calldata)?.encode()), ]); - let mut call = reactor.execute_batch( + let mut call = reactor.execute_batch_with_callback( signed_orders, - H160::from_str(EXECUTOR_ADDRESS)?, Bytes::from(calldata), ); Ok(call.tx.set_chain_id(CHAIN_ID).clone()) From 6a7368585a8e04b1c9497d37cdbe8886161eacf5 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Thu, 11 Jul 2024 14:48:57 -0400 Subject: [PATCH 04/31] fix to use fill contract --- src/strategies/uniswapx_strategy.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 18def21..a698ef5 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -10,7 +10,7 @@ use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; use artemis_core::types::Strategy; use async_trait::async_trait; use bindings_uniswapx::{ - exclusive_dutch_order_reactor::ExclusiveDutchOrderReactor, shared_types::SignedOrder, + shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor, }; use ethers::{ abi::{ethabi, AbiEncode, Token}, @@ -187,8 +187,8 @@ impl UniswapXUniswapFill { // builds a transaction to fill an order fn build_fill(&self, RoutedOrder { request, route }: RoutedOrder) -> Result { - let reactor = - ExclusiveDutchOrderReactor::new(H160::from_str(REACTOR_ADDRESS)?, self.client.clone()); + let fill_contract = + SwapRouter02Executor::new(H160::from_str(EXECUTOR_ADDRESS)?, self.client.clone()); let mut signed_orders: Vec = Vec::new(); for batch in request.orders.iter() { let OrderData { @@ -204,7 +204,7 @@ impl UniswapXUniswapFill { Token::Array(vec![Token::Address(H160::from_str(&request.token_in)?)]), Token::Bytes(Bytes::from_str(&route.method_parameters.calldata)?.encode()), ]); - let mut call = reactor.execute_batch_with_callback( + let mut call = fill_contract.execute_batch( signed_orders, Bytes::from(calldata), ); From 6a37170ad51d9e72fc5608ddd91235727bcd2e58 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 23 Jul 2024 14:16:17 -0400 Subject: [PATCH 05/31] Add v2 order (#2) * Add new strategy and enums for Order and OrderData * cargo fmt * Add order type for order querying * cargo fmt * Add v2 order * use encode/decode single for order types --- crates/uniswapx-rs/src/order.rs | 101 +++--- src/collectors/uniswapx_order_collector.rs | 73 +++- src/collectors/uniswapx_route_collector.rs | 36 +- src/main.rs | 8 +- src/strategies/mod.rs | 1 + src/strategies/priority_strategy.rs | 374 +++++++++++++++++++++ src/strategies/types.rs | 14 + src/strategies/uniswapx_strategy.rs | 86 +++-- 8 files changed, 595 insertions(+), 98 deletions(-) create mode 100644 src/strategies/priority_strategy.rs diff --git a/crates/uniswapx-rs/src/order.rs b/crates/uniswapx-rs/src/order.rs index 7984e29..47e91af 100644 --- a/crates/uniswapx-rs/src/order.rs +++ b/crates/uniswapx-rs/src/order.rs @@ -1,7 +1,8 @@ use std::error::Error; +use alloy_dyn_abi::SolType; use alloy_primitives::Uint; -use alloy_sol_types::{sol, SolType}; +use alloy_sol_types::sol; use anyhow::Result; sol! { @@ -31,14 +32,23 @@ sol! { } #[derive(Debug)] - struct ExclusiveDutchOrder { - OrderInfo info; + struct CosignerData { uint256 decayStartTime; uint256 decayEndTime; address exclusiveFiller; uint256 exclusivityOverrideBps; - DutchInput input; - DutchOutput[] outputs; + uint256 inputAmount; + uint256[] outputAmounts; + } + + #[derive(Debug)] + struct V2DutchOrder { + OrderInfo info; + address cosigner; + DutchInput baseInput; + DutchOutput[] baseOutputs; + CosignerData cosignerData; + bytes cosignature; } #[derive(Debug)] @@ -74,26 +84,26 @@ sol! { } } -pub trait Order: Sized { - fn _decode(order_hex: &[u8], validate: bool) -> Result>; - fn _encode(&self) -> Vec; -} - -// Generic function to decode orders -pub fn decode_order(encoded_order: &str) -> Result> { - let encoded_order = if encoded_order.starts_with("0x") { - &encoded_order[2..] - } else { - encoded_order - }; - let order_hex = hex::decode(encoded_order)?; - T::_decode(&order_hex, false) +pub enum Order { + V2DutchOrder(V2DutchOrder), + PriorityOrder(PriorityOrder), } -// Generic function to encode orders -pub fn encode_order(order: &T) -> Vec { - order._encode() +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), + } + } + + pub fn encode(&self) -> Vec { + match self { + Order::V2DutchOrder(order) => order._encode(), + Order::PriorityOrder(order) => order._encode(), + } + } } #[derive(Debug, Clone)] @@ -122,17 +132,15 @@ pub enum OrderResolution { Invalid, } -impl Order for ExclusiveDutchOrder { - fn _decode(order_hex: &[u8], validate: bool) -> Result> { - Ok(ExclusiveDutchOrder::decode(order_hex, validate)?) +impl V2DutchOrder { + pub fn _decode(order_hex: &[u8], validate: bool) -> Result> { + Ok(V2DutchOrder::decode_single(order_hex, validate)?) } - fn _encode(&self) -> Vec { - ExclusiveDutchOrder::encode(self) + pub fn _encode(&self) -> Vec { + V2DutchOrder::encode_single(self) } -} -impl ExclusiveDutchOrder { pub fn resolve(&self, timestamp: u64) -> OrderResolution { let timestamp = Uint::from(timestamp); @@ -141,33 +149,34 @@ impl ExclusiveDutchOrder { }; // resolve over the decay curve + // TODO: apply cosigner logic - let input = ResolvedInput { - token: self.input.token.to_string(), + let input: ResolvedInput = ResolvedInput { + token: self.baseInput.token.to_string(), amount: resolve_decay( timestamp, - self.decayStartTime, - self.decayEndTime, - self.input.startAmount, - self.input.endAmount, + self.cosignerData.decayStartTime, + self.cosignerData.decayEndTime, + self.baseInput.startAmount, + self.baseInput.endAmount, ), }; let outputs = self - .outputs + .baseOutputs .iter() .map(|output| { let mut amount = resolve_decay( timestamp, - self.decayStartTime, - self.decayEndTime, + self.cosignerData.decayStartTime, + self.cosignerData.decayEndTime, output.startAmount, output.endAmount, ); // add exclusivity override to amount - if self.decayStartTime.gt(×tamp) && !self.exclusiveFiller.is_zero() { - let exclusivity = self.exclusivityOverrideBps.wrapping_add(Uint::from(10000)); + if self.cosignerData.decayStartTime.gt(×tamp) && !self.cosignerData.exclusiveFiller.is_zero() { + let exclusivity = self.cosignerData.exclusivityOverrideBps.wrapping_add(Uint::from(10000)); let exclusivity = exclusivity.wrapping_mul(amount); amount = exclusivity.wrapping_div(Uint::from(10000)); }; @@ -184,17 +193,15 @@ impl ExclusiveDutchOrder { } } -impl Order for PriorityOrder { - fn _decode(order_hex: &[u8], validate: bool) -> Result> { - Ok(PriorityOrder::decode(order_hex, validate)?) +impl PriorityOrder { + pub fn _decode(order_hex: &[u8], validate: bool) -> Result> { + Ok(PriorityOrder::decode_single(order_hex, validate)?) } - fn _encode(&self) -> Vec { - PriorityOrder::encode(self) + pub fn _encode(&self) -> Vec { + PriorityOrder::encode_single(self) } -} -impl PriorityOrder { pub fn resolve(&self, priority_fee: Uint<256, 4>) -> OrderResolution { let input = self.input.scale(priority_fee); let outputs = self diff --git a/src/collectors/uniswapx_order_collector.rs b/src/collectors/uniswapx_order_collector.rs index 3587135..4eda20e 100644 --- a/src/collectors/uniswapx_order_collector.rs +++ b/src/collectors/uniswapx_order_collector.rs @@ -11,6 +11,35 @@ static UNISWAPX_API_URL: &str = "https://api.uniswap.org/v2"; static POLL_INTERVAL_SECS: u64 = 5; pub const CHAIN_ID: u64 = 1; +#[derive(Debug, PartialEq, Eq)] +pub enum OrderType { + Dutch, + Priority, +} + +impl OrderType { + pub fn as_str(&self) -> &'static str { + match self { + OrderType::Dutch => "Dutch_V2", + OrderType::Priority => "Priority", + } + } + + pub fn from_str(s: &str) -> Option { + match s { + "Dutch_V2" => Some(OrderType::Dutch), + "Priority" => Some(OrderType::Priority), + _ => None, + } + } +} + +impl Default for OrderType { + fn default() -> Self { + OrderType::Dutch + } +} + #[derive(Debug, Clone, Deserialize)] pub struct UniswapXOrder { #[serde(rename = "encodedOrder")] @@ -38,13 +67,17 @@ pub struct UniswapXOrderResponse { pub struct UniswapXOrderCollector { pub client: Client, pub base_url: String, + pub chain_id: u64, + pub order_type: OrderType, } impl UniswapXOrderCollector { - pub fn new() -> Self { + pub fn new(chain_id: u64, order_type: OrderType) -> Self { Self { client: Client::new(), base_url: UNISWAPX_API_URL.to_string(), + chain_id, + order_type, } } } @@ -56,8 +89,10 @@ impl UniswapXOrderCollector { impl Collector for UniswapXOrderCollector { async fn get_event_stream(&self) -> Result> { let url = format!( - "{}/orders?orderStatus=open&chainId={}", - self.base_url, CHAIN_ID + "{}/orders?orderStatus=open&chainId={}&orderType={}", + self.base_url, + self.chain_id, + self.order_type.as_str() ); // stream that polls the UniswapX API every 5 seconds @@ -94,8 +129,10 @@ impl Collector for UniswapXOrderCollector { mod tests { use crate::collectors::uniswapx_order_collector::UniswapXOrderCollector; use artemis_core::types::Collector; + use ethers::utils::hex; use futures::StreamExt; use mockito::{Mock, Server, ServerGuard}; + use uniswapx_rs::order::V2DutchOrder; async fn get_collector(mock_response: &str) -> (UniswapXOrderCollector, ServerGuard, Mock) { let mut server = Server::new_async().await; @@ -112,6 +149,8 @@ mod tests { let res = UniswapXOrderCollector { client: reqwest::Client::new(), base_url: url.clone(), + chain_id: 1, + order_type: super::OrderType::Dutch, }; (res, server, mock) @@ -140,4 +179,32 @@ mod tests { ); mock.assert_async().await; } + + #[tokio::test] + async fn decodes_v2_order() { + let response = r#" +{"orders":[{"type":"Dutch_V2","orderStatus":"open","signature":"0x6eb32e7912d333e9c1ab162db02ed1656cdc8fbea2e21e70cd3634e8a3bd85d0582b46cacb584412ef3e035837b005b70f67897969426f9795128ea52de3a8cf1b","encodedOrder":"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001000000000000000000000000004449cd34d1eb1fedcf02a1be3834ffde8e6a61800000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000000422ca8b0a00a4250000000000000000000000000000000000000000000000000422ca8b0a00a42500000000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000011f84b9aa48e5f8aa8b9897600006289be000000000000000000000000c9838bbf85ad068136e8da07021e9e131201901904683298fe8b71446644eba514e387688690bde85b7bcaf8de44455a6aaf7a3000000000000000000000000000000000000000000000000000000000669adac5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c330a127f1ec70000000000000000000000000000000000000000000000000034be9ca1484989000000000000000000000000c9838bbf85ad068136e8da07021e9e131201901900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000269fc8de5047000000000000000000000000000000000000000000000000000021d754744fbe000000000000000000000000000000fee13a103a10d593b9ae06b3e05f2e7e1c00000000000000000000000000000000000000000000000000000000669ad9b600000000000000000000000000000000000000000000000000000000669ad9f20000000000000000000000006f1cdbbb4d53d226cf4b917bf768b94acbab61680000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000003c64146542c1fd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041d90e87f6f9e84487bfbb5170e856a332769359664c72f90250ee8917baf3a5920e87d331fcf97456e5d4d88761c552a9115569861aa96120b56d882339bbaac91c00000000000000000000000000000000000000000000000000000000000000","chainId":1,"nonce":"1993352701105935839386570705396248068916924096291549856616269381900329515568","orderHash":"0x382f612930c2121ed91fcdc00972f76b4adbef8d111830e1d135ac944a144876","swapper":"0xC9838Bbf85Ad068136E8DA07021E9e1312019019","input":{"token":"0x6982508145454Ce325dDbE47a25d4ec3d2311933","startAmount":"5000000000000000000000000","endAmount":"5000000000000000000000000"},"outputs":[{"token":"0x0000000000000000000000000000000000000000","startAmount":"16944616955649735","endAmount":"14846278718998921","recipient":"0xC9838Bbf85Ad068136E8DA07021E9e1312019019"},{"token":"0x0000000000000000000000000000000000000000","startAmount":"42467711668295","endAmount":"37208718593982","recipient":"0x000000fee13a103A10D593b9AE06b3e05F2E7E1c"}],"cosignerData":{"decayStartTime":1721424310,"decayEndTime":1721424370,"exclusiveFiller":"0x6F1cDbBb4d53d226CF4B917bF768B94acbAB6168","inputOverride":"0","outputOverrides":["16998537363636733","0"]},"cosignature":"0xd90e87f6f9e84487bfbb5170e856a332769359664c72f90250ee8917baf3a5920e87d331fcf97456e5d4d88761c552a9115569861aa96120b56d882339bbaac91c","quoteId":"221f421a-455d-4358-8376-6b4fb0ffb0f1","requestId":"775eea31-3173-4f1c-b7d2-bcd6fbcf2301","createdAt":1721424286}]} "#; + let (collector, _server, _) = get_collector(response).await; + // get event stream and parse events + let stream = collector.get_event_stream().await.unwrap(); + let (first_order, _) = stream.into_future().await; + assert!(first_order.is_some()); + assert_eq!( + first_order.clone().unwrap().order_hash, + "0x382f612930c2121ed91fcdc00972f76b4adbef8d111830e1d135ac944a144876" + ); + let encoded_order = &first_order.unwrap().encoded_order; + let encoded_order = if encoded_order.starts_with("0x") { + &encoded_order[2..] + } else { + encoded_order + }; + let order_hex: Vec = hex::decode(encoded_order).unwrap(); + + let result = V2DutchOrder::_decode(&order_hex, false); + match result { + Err(e) => panic!("Error decoding order: {:?}", e), + _ => (), + } + } } diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index be6721a..9c416e8 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -5,7 +5,7 @@ use reqwest::header::ORIGIN; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::info; -use uniswapx_rs::order::{ExclusiveDutchOrder, ResolvedOrder}; +use uniswapx_rs::order::{PriorityOrder, ResolvedOrder, V2DutchOrder}; use crate::strategies::uniswapx_strategy::EXECUTOR_ADDRESS; use artemis_core::types::{Collector, CollectorStream}; @@ -19,13 +19,43 @@ const SLIPPAGE_TOLERANCE: &str = "0.5"; const DEADLINE: u64 = 1000; #[derive(Debug, Clone)] -pub struct OrderData { - pub order: ExclusiveDutchOrder, +pub struct V2DutchOrderData { + pub order: V2DutchOrder, pub hash: String, pub signature: String, pub resolved: ResolvedOrder, } +#[derive(Debug, Clone)] +pub struct PriorityOrderData { + pub order: PriorityOrder, + pub hash: String, + pub signature: String, + pub resolved: ResolvedOrder, +} + +#[derive(Debug, Clone)] +pub enum OrderData { + V2DutchOrderData(V2DutchOrderData), + PriorityOrderData(PriorityOrderData), +} + +impl OrderData { + pub fn signature(&self) -> String { + match self { + OrderData::V2DutchOrderData(data) => data.signature.clone(), + OrderData::PriorityOrderData(data) => data.signature.clone(), + } + } + + pub fn hash(&self) -> String { + match self { + OrderData::V2DutchOrderData(data) => data.hash.clone(), + OrderData::PriorityOrderData(data) => data.hash.clone(), + } + } +} + #[derive(Clone, Debug)] pub struct OrderBatchData { pub orders: Vec, diff --git a/src/main.rs b/src/main.rs index d021a1d..6e8a244 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use clap::Parser; use artemis_core::engine::Engine; use artemis_core::types::{CollectorMap, ExecutorMap}; +use collectors::uniswapx_order_collector::OrderType; use collectors::{ block_collector::BlockCollector, uniswapx_order_collector::{UniswapXOrderCollector, CHAIN_ID}, @@ -91,11 +92,16 @@ async fn main() -> Result<()> { let (batch_sender, batch_receiver) = channel(512); let (route_sender, route_receiver) = channel(512); - let uniswapx_collector = Box::new(UniswapXOrderCollector::new()); + let uniswapx_collector = Box::new(UniswapXOrderCollector::new(1, OrderType::Dutch)); let uniswapx_collector = CollectorMap::new(uniswapx_collector, |e| Event::UniswapXOrder(Box::new(e))); engine.add_collector(Box::new(uniswapx_collector)); + let priority_collector = Box::new(UniswapXOrderCollector::new(1, OrderType::Priority)); + let priority_collector = + CollectorMap::new(priority_collector, |e| Event::PriorityOrder(Box::new(e))); + engine.add_collector(Box::new(priority_collector)); + let uniswapx_route_collector = Box::new(UniswapXRouteCollector::new(batch_receiver, route_sender)); let uniswapx_route_collector = CollectorMap::new(uniswapx_route_collector, |e| { diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 7ef947a..d3366f5 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -1,2 +1,3 @@ +pub mod priority_strategy; pub mod types; pub mod uniswapx_strategy; diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs new file mode 100644 index 0000000..daa514e --- /dev/null +++ b/src/strategies/priority_strategy.rs @@ -0,0 +1,374 @@ +use super::types::{Config, OrderStatus, TokenInTokenOut}; +use crate::collectors::{ + block_collector::NewBlock, + uniswapx_order_collector::{UniswapXOrder, CHAIN_ID}, + uniswapx_route_collector::{OrderBatchData, OrderData, PriorityOrderData, RoutedOrder}, +}; +use alloy_primitives::Uint; +use anyhow::Result; +use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; +use artemis_core::types::Strategy; +use async_trait::async_trait; +use bindings_uniswapx::{shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor}; +use ethers::{ + abi::{ethabi, AbiEncode, Token}, + providers::Middleware, + types::{transaction::eip2718::TypedTransaction, Address, Bytes, Filter, H160, U256}, + utils::hex, +}; +use std::collections::HashMap; +use std::error::Error; +use std::str::FromStr; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::sync::mpsc::{Receiver, Sender}; +use tracing::{error, info}; +use uniswapx_rs::order::{OrderResolution, PriorityOrder}; + +use super::types::{Action, Event}; + +const DONE_EXPIRY: u64 = 300; +const REACTOR_ADDRESS: &str = ""; +const EXECUTOR_ADDRESS: &str = ""; +pub const WETH_ADDRESS: &str = ""; + +#[derive(Debug)] +#[allow(dead_code)] +pub struct UniswapXPriorityFill { + /// Ethers client. + client: Arc, + /// Amount of profits to bid in gas + bid_percentage: u64, + last_block_number: u64, + last_block_timestamp: u64, + // map of open order hashes to order data + open_orders: HashMap, + // map of done order hashes to time at which we can safely prune them + done_orders: HashMap, + batch_sender: Sender>, + route_receiver: Receiver, +} + +impl UniswapXPriorityFill { + pub fn new( + client: Arc, + config: Config, + sender: Sender>, + receiver: Receiver, + ) -> Self { + info!("syncing state"); + + Self { + client, + bid_percentage: config.bid_percentage, + last_block_number: 0, + last_block_timestamp: 0, + open_orders: HashMap::new(), + done_orders: HashMap::new(), + batch_sender: sender, + route_receiver: receiver, + } + } +} + +#[async_trait] +impl Strategy for UniswapXPriorityFill { + // In order to sync this strategy, we need to get the current bid for all Sudo pools. + async fn sync_state(&mut self) -> Result<()> { + info!("syncing state"); + + Ok(()) + } + + // Process incoming events, seeing if we can arb new orders, and updating the internal state on new blocks. + async fn process_event(&mut self, event: Event) -> Option { + match event { + Event::PriorityOrder(order) => self.process_order_event(*order).await, + Event::NewBlock(block) => self.process_new_block_event(block).await, + Event::UniswapXRoute(route) => self.process_new_route(*route).await, + _ => None, + } + } +} + +impl UniswapXPriorityFill { + fn decode_order(&self, encoded_order: &str) -> Result> { + let encoded_order = if encoded_order.starts_with("0x") { + &encoded_order[2..] + } else { + encoded_order + }; + let order_hex = hex::decode(encoded_order)?; + + Ok(PriorityOrder::_decode(&order_hex, false)?) + } + + async fn process_order_event(&mut self, event: UniswapXOrder) -> Option { + if self.last_block_timestamp == 0 { + return None; + } + + let order = self + .decode_order(&event.encoded_order) + .map_err(|e| error!("failed to decode: {}", e)) + .ok()?; + + self.update_order_state(order, event.signature, event.order_hash); + None + } + + async fn process_new_route(&mut self, event: RoutedOrder) -> Option { + if event + .request + .orders + .iter() + .any(|o| self.done_orders.contains_key(&o.hash())) + { + return None; + } + + let OrderBatchData { + // orders, + orders, + amount_out_required, + .. + } = &event.request; + + if let Some(profit) = self.get_profit(&event) { + info!( + "Sending trade: num trades: {} routed quote: {}, batch needs: {}, profit: {} wei", + orders.len(), + event.route.quote, + amount_out_required, + profit + ); + + return Some(Action::SubmitTx(SubmitTxToMempool { + tx: self.build_fill(event).ok()?, + gas_bid_info: Some(GasBidInfo { + bid_percentage: self.bid_percentage, + total_profit: profit, + }), + })); + } + + None + } + + /// Process new block events, updating the internal state. + async fn process_new_block_event(&mut self, event: NewBlock) -> Option { + self.last_block_number = event.number.as_u64(); + self.last_block_timestamp = event.timestamp.as_u64(); + + info!( + "Processing block {} at {}, Order set sizes -- open: {}, done: {}", + event.number, + event.timestamp, + self.open_orders.len(), + self.done_orders.len() + ); + self.handle_fills() + .await + .map_err(|e| error!("Error handling fills {}", e)) + .ok()?; + self.update_open_orders(); + self.prune_done_orders(); + + self.batch_sender + .send(self.get_order_batches().values().cloned().collect()) + .await + .ok()?; + + None + } + + // builds a transaction to fill an order + fn build_fill(&self, RoutedOrder { request, route }: RoutedOrder) -> Result { + let fill_contract = + SwapRouter02Executor::new(H160::from_str(EXECUTOR_ADDRESS)?, self.client.clone()); + let mut signed_orders: Vec = Vec::new(); + for batch in request.orders.iter() { + match batch { + OrderData::PriorityOrderData(order) => { + signed_orders.push(SignedOrder { + order: Bytes::from(order.order._encode()), + sig: Bytes::from_str(&order.signature)?, + }); + } + _ => { + return Err(anyhow::anyhow!("Invalid order type")); + } + } + } + // abi encode as [tokens to approve, multicall data] + let calldata = ethabi::encode(&[ + Token::Array(vec![Token::Address(H160::from_str(&request.token_in)?)]), + Token::Bytes(Bytes::from_str(&route.method_parameters.calldata)?.encode()), + ]); + let mut call = fill_contract.execute_batch(signed_orders, Bytes::from(calldata)); + Ok(call.tx.set_chain_id(CHAIN_ID).clone()) + } + + fn get_order_batches(&self) -> HashMap { + let mut order_batches: HashMap = HashMap::new(); + + // group orders by token in and token out + self.open_orders.iter().for_each(|(_, order_data)| { + let token_in_token_out = TokenInTokenOut { + token_in: order_data.resolved.input.token.clone(), + token_out: order_data.resolved.outputs[0].token.clone(), + }; + + let amount_in = order_data.resolved.input.amount; + let amount_out = order_data + .resolved + .outputs + .iter() + .fold(Uint::from(0), |sum, output| sum.wrapping_add(output.amount)); + + // insert new order and update total amount out + if let std::collections::hash_map::Entry::Vacant(e) = + order_batches.entry(token_in_token_out.clone()) + { + e.insert(OrderBatchData { + orders: vec![OrderData::PriorityOrderData(order_data.clone())], + amount_in, + amount_out_required: amount_out, + token_in: order_data.resolved.input.token.clone(), + token_out: order_data.resolved.outputs[0].token.clone(), + }); + } else { + let order_batch_data = order_batches.get_mut(&token_in_token_out).unwrap(); + order_batch_data + .orders + .push(OrderData::PriorityOrderData(order_data.clone())); + order_batch_data.amount_in = order_batch_data.amount_in.wrapping_add(amount_in); + order_batch_data.amount_out_required = order_batch_data + .amount_out_required + .wrapping_add(amount_out); + } + }); + order_batches + } + + async fn handle_fills(&mut self) -> Result<()> { + let reactor_address = REACTOR_ADDRESS.parse::
().unwrap(); + let filter = Filter::new() + .select(self.last_block_number) + .address(reactor_address) + .event("Fill(bytes32,address,address,uint256)"); + + // early return on error + let logs = self.client.get_logs(&filter).await?; + for log in logs { + let order_hash = format!("0x{:x}", log.topics[1]); + // remove from open + info!("Removing filled order {}", order_hash); + self.open_orders.remove(&order_hash); + // add to done + self.done_orders.insert( + order_hash.to_string(), + self.current_timestamp()? + DONE_EXPIRY, + ); + } + + Ok(()) + } + + fn get_profit(&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 profit_quote + .checked_div(quote) + .map(|profit| profit * U256::from(10_000_000)); + } + + fn update_order_state(&mut self, order: PriorityOrder, signature: String, order_hash: String) { + let resolved = order.resolve(Uint::from(0)); + let order_status: OrderStatus = match resolved { + OrderResolution::Expired => OrderStatus::Done, + OrderResolution::Invalid => OrderStatus::Done, + OrderResolution::Resolved(resolved_order) => OrderStatus::Open(resolved_order), + }; + + match order_status { + OrderStatus::Done => { + self.mark_as_done(&order_hash); + } + OrderStatus::Open(resolved_order) => { + if self.done_orders.contains_key(&order_hash) { + info!("Order already done, skipping: {}", order_hash); + return; + } + if !self.open_orders.contains_key(&order_hash) { + info!("Adding new order {}", order_hash); + } + self.open_orders.insert( + order_hash.clone(), + PriorityOrderData { + order, + hash: order_hash, + signature, + resolved: resolved_order, + }, + ); + } + } + } + + fn prune_done_orders(&mut self) { + let mut to_remove = Vec::new(); + for (order_hash, deadline) in self.done_orders.iter() { + if *deadline < self.last_block_timestamp { + to_remove.push(order_hash.clone()); + } + } + for order_hash in to_remove { + self.done_orders.remove(&order_hash); + } + } + + fn update_open_orders(&mut self) { + // TODO: this is nasty, plz cleanup + let binding = self.open_orders.clone(); + let order_hashes: Vec<(&String, &PriorityOrderData)> = binding.iter().collect(); + for (order_hash, order_data) in order_hashes { + self.update_order_state( + order_data.order.clone(), + order_data.signature.clone(), + order_hash.clone().to_string(), + ); + } + } + + fn mark_as_done(&mut self, order: &str) { + if self.open_orders.contains_key(order) { + self.open_orders.remove(order); + } + if !self.done_orders.contains_key(order) { + self.done_orders + .insert(order.to_string(), self.last_block_timestamp + DONE_EXPIRY); + } + } + + fn current_timestamp(&self) -> Result { + let start = SystemTime::now(); + Ok(start.duration_since(UNIX_EPOCH)?.as_secs()) + } +} diff --git a/src/strategies/types.rs b/src/strategies/types.rs index ed78d5d..16d3b3b 100644 --- a/src/strategies/types.rs +++ b/src/strategies/types.rs @@ -3,12 +3,14 @@ use crate::collectors::{ uniswapx_route_collector::RoutedOrder, }; use artemis_core::executors::mempool_executor::SubmitTxToMempool; +use uniswapx_rs::order::ResolvedOrder; /// Core Event enum for the current strategy. #[derive(Debug, Clone)] pub enum Event { NewBlock(NewBlock), UniswapXOrder(Box), + PriorityOrder(Box), UniswapXRoute(Box), } @@ -23,3 +25,15 @@ pub enum Action { pub struct Config { pub bid_percentage: u64, } + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct TokenInTokenOut { + pub token_in: String, + pub token_out: String, +} + +#[derive(Debug, Clone)] +pub enum OrderStatus { + Open(ResolvedOrder), + Done, +} diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index a698ef5..8d1bd28 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -1,31 +1,29 @@ -use super::types::Config; +use super::types::{Config, OrderStatus, TokenInTokenOut}; use crate::collectors::{ block_collector::NewBlock, uniswapx_order_collector::{UniswapXOrder, CHAIN_ID}, - uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder}, + uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder, V2DutchOrderData}, }; use alloy_primitives::Uint; use anyhow::Result; use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; use artemis_core::types::Strategy; use async_trait::async_trait; -use bindings_uniswapx::{ - shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor, -}; +use bindings_uniswapx::{shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor}; use ethers::{ abi::{ethabi, AbiEncode, Token}, providers::Middleware, types::{transaction::eip2718::TypedTransaction, Address, Bytes, Filter, H160, U256}, + utils::hex, }; use std::collections::HashMap; +use std::error::Error; use std::str::FromStr; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::{error, info}; -use uniswapx_rs::order::{ - decode_order, encode_order, ExclusiveDutchOrder, OrderResolution, ResolvedOrder -}; +use uniswapx_rs::order::{OrderResolution, V2DutchOrder}; use super::types::{Action, Event}; @@ -35,18 +33,6 @@ const REACTOR_ADDRESS: &str = "0xe80bF394d190851E215D5F67B67f8F5A52783F1E"; pub const WETH_ADDRESS: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; pub const EXECUTOR_ADDRESS: &str = "TODO: Fill in swaprouter02 executor address"; -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -struct TokenInTokenOut { - token_in: String, - token_out: String, -} - -#[derive(Debug, Clone)] -enum OrderStatus { - Open(ResolvedOrder), - Done, -} - #[derive(Debug)] #[allow(dead_code)] pub struct UniswapXUniswapFill { @@ -57,7 +43,7 @@ pub struct UniswapXUniswapFill { last_block_number: u64, last_block_timestamp: u64, // map of open order hashes to order data - open_orders: HashMap, + open_orders: HashMap, // map of done order hashes to time at which we can safely prune them done_orders: HashMap, batch_sender: Sender>, @@ -101,18 +87,32 @@ impl Strategy for UniswapXUniswapFill Event::UniswapXOrder(order) => self.process_order_event(*order).await, Event::NewBlock(block) => self.process_new_block_event(block).await, Event::UniswapXRoute(route) => self.process_new_route(*route).await, + _ => None, } } } impl UniswapXUniswapFill { + fn decode_order(&self, encoded_order: &str) -> Result> { + info!("Decoding order: {}", encoded_order); + let encoded_order = if encoded_order.starts_with("0x") { + &encoded_order[2..] + } else { + encoded_order + }; + let order_hex: Vec = hex::decode(encoded_order)?; + + Ok(V2DutchOrder::_decode(&order_hex, false)?) + } + // Process new orders as they come in. async fn process_order_event(&mut self, event: UniswapXOrder) -> Option { if self.last_block_timestamp == 0 { return None; } - let order = decode_order(&event.encoded_order) + let order = self + .decode_order(&event.encoded_order) .map_err(|e| error!("failed to decode: {}", e)) .ok()?; @@ -125,7 +125,7 @@ impl UniswapXUniswapFill { .request .orders .iter() - .any(|o| self.done_orders.contains_key(&o.hash)) + .any(|o| self.done_orders.contains_key(&o.hash())) { return None; } @@ -191,23 +191,24 @@ impl UniswapXUniswapFill { SwapRouter02Executor::new(H160::from_str(EXECUTOR_ADDRESS)?, self.client.clone()); let mut signed_orders: Vec = Vec::new(); for batch in request.orders.iter() { - let OrderData { - order, signature, .. - } = batch; - signed_orders.push(SignedOrder { - order: Bytes::from(encode_order(order)), - sig: Bytes::from_str(signature)?, - }); + match batch { + OrderData::V2DutchOrderData(order) => { + signed_orders.push(SignedOrder { + order: Bytes::from(order.order._encode()), + sig: Bytes::from_str(&order.signature)?, + }); + } + _ => { + return Err(anyhow::anyhow!("Invalid order type")); + } + } } // abi encode as [tokens to approve, multicall data] let calldata = ethabi::encode(&[ Token::Array(vec![Token::Address(H160::from_str(&request.token_in)?)]), Token::Bytes(Bytes::from_str(&route.method_parameters.calldata)?.encode()), ]); - let mut call = fill_contract.execute_batch( - signed_orders, - Bytes::from(calldata), - ); + let mut call = fill_contract.execute_batch(signed_orders, Bytes::from(calldata)); Ok(call.tx.set_chain_id(CHAIN_ID).clone()) } @@ -233,7 +234,7 @@ impl UniswapXUniswapFill { order_batches.entry(token_in_token_out.clone()) { e.insert(OrderBatchData { - orders: vec![order_data.clone()], + orders: vec![OrderData::V2DutchOrderData(order_data.clone())], amount_in, amount_out_required: amount_out, token_in: order_data.resolved.input.token.clone(), @@ -241,7 +242,9 @@ impl UniswapXUniswapFill { }); } else { let order_batch_data = order_batches.get_mut(&token_in_token_out).unwrap(); - order_batch_data.orders.push(order_data.clone()); + order_batch_data + .orders + .push(OrderData::V2DutchOrderData(order_data.clone())); order_batch_data.amount_in = order_batch_data.amount_in.wrapping_add(amount_in); order_batch_data.amount_out_required = order_batch_data .amount_out_required @@ -290,7 +293,7 @@ impl UniswapXUniswapFill { fn update_open_orders(&mut self) { // TODO: this is nasty, plz cleanup let binding = self.open_orders.clone(); - let order_hashes: Vec<(&String, &OrderData)> = binding.iter().collect(); + let order_hashes: Vec<(&String, &V2DutchOrderData)> = binding.iter().collect(); for (order_hash, order_data) in order_hashes { self.update_order_state( order_data.order.clone(), @@ -310,12 +313,7 @@ impl UniswapXUniswapFill { } } - fn update_order_state( - &mut self, - order: ExclusiveDutchOrder, - signature: String, - order_hash: String, - ) { + fn update_order_state(&mut self, order: V2DutchOrder, signature: String, order_hash: String) { let resolved = order.resolve(self.last_block_timestamp + BLOCK_TIME); let order_status: OrderStatus = match resolved { OrderResolution::Expired => OrderStatus::Done, @@ -337,7 +335,7 @@ impl UniswapXUniswapFill { } self.open_orders.insert( order_hash.clone(), - OrderData { + V2DutchOrderData { order, hash: order_hash, signature, From cca40a55ecfdd162a83ec45def93b77e9a65ef8c Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 23 Jul 2024 14:18:16 -0400 Subject: [PATCH 06/31] feat: add priority strategy and new order enums/types (#1) * Add new strategy and enums for Order and OrderData * cargo fmt * Add order type for order querying * cargo fmt * Add v2 order (#3) * Add v2 order * use encode/decode single for order types From 5b1f0962a9a14c71d753dc87c8faf68c76f56303 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 23 Jul 2024 14:21:01 -0400 Subject: [PATCH 07/31] add header for request --- src/collectors/uniswapx_route_collector.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index 9c416e8..be79b5b 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -248,6 +248,7 @@ pub async fn route_order(params: RouteOrderParams) -> Result { Ok(client .get(format!("{}?{}", ROUTING_API, query_string)) .header(ORIGIN, "https://app.uniswap.org") + .header("x-request-source", "uniswap-web") .send() .await? .json::() From 56ca606f695a398c752797a28f6be7a38d2fd15b Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 23 Jul 2024 14:59:51 -0400 Subject: [PATCH 08/31] Add new collector plus strategies to engine --- src/main.rs | 26 +++++++++++++++++++++++--- src/strategies/priority_strategy.rs | 9 +++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6e8a244..e0b736f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ use ethers::{ }; use executors::protect_executor::ProtectExecutor; use std::sync::Arc; +use strategies::priority_strategy::UniswapXPriorityFill; use strategies::{ types::{Action, Config, Event}, uniswapx_strategy::UniswapXUniswapFill, @@ -90,7 +91,9 @@ async fn main() -> Result<()> { engine.add_collector(Box::new(block_collector)); let (batch_sender, batch_receiver) = channel(512); + let (priority_batch_sender, priority_batch_receiver) = channel(512); let (route_sender, route_receiver) = channel(512); + let (priority_route_sender, priority_route_receiver) = channel(512); let uniswapx_collector = Box::new(UniswapXOrderCollector::new(1, OrderType::Dutch)); let uniswapx_collector = @@ -109,17 +112,34 @@ async fn main() -> Result<()> { }); engine.add_collector(Box::new(uniswapx_route_collector)); + let priority_route_collector = Box::new(UniswapXRouteCollector::new( + priority_batch_receiver, + priority_route_sender, + )); + let priority_route_collector = CollectorMap::new(priority_route_collector, |e| { + Event::UniswapXRoute(Box::new(e)) + }); + engine.add_collector(Box::new(priority_route_collector)); + let config = Config { bid_percentage: args.bid_percentage, }; - let strategy = UniswapXUniswapFill::new( + let uniswapx_strategy = UniswapXUniswapFill::new( Arc::new(provider.clone()), - config, + config.clone(), batch_sender, route_receiver, ); - engine.add_strategy(Box::new(strategy)); + engine.add_strategy(Box::new(uniswapx_strategy)); + + let priority_strategy = UniswapXPriorityFill::new( + Arc::new(provider.clone()), + config.clone(), + priority_batch_sender, + priority_route_receiver, + ); + engine.add_strategy(Box::new(priority_strategy)); let executor = Box::new(ProtectExecutor::new( provider.clone(), diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index daa514e..1775d45 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -134,7 +134,7 @@ impl UniswapXPriorityFill { .. } = &event.request; - if let Some(profit) = self.get_profit(&event) { + if let Some(profit) = self.get_profit_eth(&event) { info!( "Sending trade: num trades: {} routed quote: {}, batch needs: {}, profit: {} wei", orders.len(), @@ -275,7 +275,7 @@ impl UniswapXPriorityFill { Ok(()) } - fn get_profit(&self, RoutedOrder { request, route }: &RoutedOrder) -> Option { + fn get_profit_eth(&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()?; @@ -293,10 +293,7 @@ impl UniswapXPriorityFill { .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 profit_quote - .checked_div(quote) - .map(|profit| profit * U256::from(10_000_000)); + .checked_div(U256::from_str_radix(&route.gas_use_estimate_quote, 10).ok()?) } fn update_order_state(&mut self, order: PriorityOrder, signature: String, order_hash: String) { From 3cef6b570712616c3b9e6da13254ee4f8823f582 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 23 Jul 2024 15:19:01 -0400 Subject: [PATCH 09/31] do not batch orders for prioroity --- src/strategies/priority_strategy.rs | 49 +++++++++++------------------ 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index 1775d45..f8bd041 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -1,4 +1,4 @@ -use super::types::{Config, OrderStatus, TokenInTokenOut}; +use super::types::{Config, OrderStatus}; use crate::collectors::{ block_collector::NewBlock, uniswapx_order_collector::{UniswapXOrder, CHAIN_ID}, @@ -175,7 +175,7 @@ impl UniswapXPriorityFill { self.prune_done_orders(); self.batch_sender - .send(self.get_order_batches().values().cloned().collect()) + .send(self.get_order_batches()) .await .ok()?; @@ -209,16 +209,12 @@ impl UniswapXPriorityFill { Ok(call.tx.set_chain_id(CHAIN_ID).clone()) } - fn get_order_batches(&self) -> HashMap { - let mut order_batches: HashMap = HashMap::new(); + /// We do not batch orders because priority fee is applied on the transaction level + fn get_order_batches(&self) -> Vec { + let mut order_batches: Vec = Vec::new(); - // group orders by token in and token out + // generate batches of size 1 self.open_orders.iter().for_each(|(_, order_data)| { - let token_in_token_out = TokenInTokenOut { - token_in: order_data.resolved.input.token.clone(), - token_out: order_data.resolved.outputs[0].token.clone(), - }; - let amount_in = order_data.resolved.input.amount; let amount_out = order_data .resolved @@ -226,27 +222,13 @@ impl UniswapXPriorityFill { .iter() .fold(Uint::from(0), |sum, output| sum.wrapping_add(output.amount)); - // insert new order and update total amount out - if let std::collections::hash_map::Entry::Vacant(e) = - order_batches.entry(token_in_token_out.clone()) - { - e.insert(OrderBatchData { - orders: vec![OrderData::PriorityOrderData(order_data.clone())], - amount_in, - amount_out_required: amount_out, - token_in: order_data.resolved.input.token.clone(), - token_out: order_data.resolved.outputs[0].token.clone(), - }); - } else { - let order_batch_data = order_batches.get_mut(&token_in_token_out).unwrap(); - order_batch_data - .orders - .push(OrderData::PriorityOrderData(order_data.clone())); - order_batch_data.amount_in = order_batch_data.amount_in.wrapping_add(amount_in); - order_batch_data.amount_out_required = order_batch_data - .amount_out_required - .wrapping_add(amount_out); - } + order_batches.push(OrderBatchData { + orders: vec![OrderData::PriorityOrderData(order_data.clone())], + amount_in, + amount_out_required: amount_out, + token_in: order_data.resolved.input.token.clone(), + token_out: order_data.resolved.outputs[0].token.clone(), + }); }); order_batches } @@ -275,6 +257,11 @@ impl UniswapXPriorityFill { Ok(()) } + /// We still calculate profit in terms of ETH for priority fee orders + /// Rationale: + /// - 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 { let quote = U256::from_str_radix(&route.quote, 10).ok()?; let amount_out_required = From e5b3f62706d2b327f7f4384733c1878ba94b2ea4 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 23 Jul 2024 15:24:54 -0400 Subject: [PATCH 10/31] add public tx executor --- src/main.rs | 17 ++++++++++++++--- src/strategies/priority_strategy.rs | 2 +- src/strategies/types.rs | 1 + 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index e0b736f..109341e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -141,16 +141,27 @@ async fn main() -> Result<()> { ); engine.add_strategy(Box::new(priority_strategy)); - let executor = Box::new(ProtectExecutor::new( + let protect_executor = Box::new(ProtectExecutor::new( provider.clone(), mevblocker_provider.clone(), )); - let executor = ExecutorMap::new(executor, |action| match action { + let public_tx_executor = Box::new(ProtectExecutor::new(provider.clone(), provider.clone())); + + let protect_executor = ExecutorMap::new(protect_executor, |action| match action { Action::SubmitTx(tx) => Some(tx), + // No op for public transactions + _ => None, + }); + + let public_tx_executor = ExecutorMap::new(public_tx_executor, |action| match action { + Action::SubmitPublicTx(tx) => Some(tx), + // No op for protected transactions + _ => None, }); - engine.add_executor(Box::new(executor)); + engine.add_executor(Box::new(protect_executor)); + engine.add_executor(Box::new(public_tx_executor)); // Start engine. if let Ok(mut set) = engine.run().await { while let Some(res) = set.join_next().await { diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index f8bd041..52926c7 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -143,7 +143,7 @@ impl UniswapXPriorityFill { profit ); - return Some(Action::SubmitTx(SubmitTxToMempool { + return Some(Action::SubmitPublicTx(SubmitTxToMempool { tx: self.build_fill(event).ok()?, gas_bid_info: Some(GasBidInfo { bid_percentage: self.bid_percentage, diff --git a/src/strategies/types.rs b/src/strategies/types.rs index 16d3b3b..af8a8de 100644 --- a/src/strategies/types.rs +++ b/src/strategies/types.rs @@ -18,6 +18,7 @@ pub enum Event { #[derive(Debug, Clone)] pub enum Action { SubmitTx(SubmitTxToMempool), + SubmitPublicTx(SubmitTxToMempool), } /// Configuration for variables we need to pass to the strategy. From 18e1df344b12b549936866828c2932e464832b8c Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 23 Jul 2024 17:51:03 -0400 Subject: [PATCH 11/31] support tokens to approve to router and reactor --- src/collectors/uniswapx_route_collector.rs | 1 + src/strategies/uniswapx_strategy.rs | 50 +++++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index be79b5b..7aa529f 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -139,6 +139,7 @@ pub struct MethodParameters { #[serde(rename_all = "camelCase")] pub struct OrderRoute { pub quote: String, + pub quote_gas_adjusted: String, pub gas_price_wei: String, pub gas_use_estimate_quote: String, pub gas_use_estimate: String, diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 8d1bd28..8d95275 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -9,7 +9,9 @@ use anyhow::Result; use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; use artemis_core::types::Strategy; use async_trait::async_trait; -use bindings_uniswapx::{shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor}; +use bindings_uniswapx::{ + erc20::ERC20, shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor, +}; use ethers::{ abi::{ethabi, AbiEncode, Token}, providers::Middleware, @@ -30,6 +32,7 @@ use super::types::{Action, Event}; const BLOCK_TIME: u64 = 12; const DONE_EXPIRY: u64 = 300; const REACTOR_ADDRESS: &str = "0xe80bF394d190851E215D5F67B67f8F5A52783F1E"; +const SWAPROUTER_02_ADDRESS: &str = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; pub const WETH_ADDRESS: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; pub const EXECUTOR_ADDRESS: &str = "TODO: Fill in swaprouter02 executor address"; @@ -141,13 +144,13 @@ impl UniswapXUniswapFill { info!( "Sending trade: num trades: {} routed quote: {}, batch needs: {}, profit: {} wei", orders.len(), - event.route.quote, + event.route.quote_gas_adjusted, amount_out_required, profit ); return Some(Action::SubmitTx(SubmitTxToMempool { - tx: self.build_fill(event).ok()?, + tx: self.build_fill(event).await.ok()?, gas_bid_info: Some(GasBidInfo { bid_percentage: self.bid_percentage, total_profit: profit, @@ -186,7 +189,10 @@ impl UniswapXUniswapFill { } // builds a transaction to fill an order - fn build_fill(&self, RoutedOrder { request, route }: RoutedOrder) -> Result { + async fn build_fill( + &self, + RoutedOrder { request, route }: RoutedOrder, + ) -> Result { let fill_contract = SwapRouter02Executor::new(H160::from_str(EXECUTOR_ADDRESS)?, self.client.clone()); let mut signed_orders: Vec = Vec::new(); @@ -203,9 +209,32 @@ impl UniswapXUniswapFill { } } } - // abi encode as [tokens to approve, multicall data] + + let token_in: H160 = H160::from_str(&request.token_in)?; + let token_out: H160 = H160::from_str(&request.token_out)?; + let swaprouter_02_approvals = if self + .needs_approval(token_in, EXECUTOR_ADDRESS, SWAPROUTER_02_ADDRESS) + .await + .is_some() + { + vec![Token::Address(token_in)] + } else { + vec![] + }; + let reactor_approvals = if self + .needs_approval(token_out, EXECUTOR_ADDRESS, REACTOR_ADDRESS) + .await + .is_some() + { + vec![Token::Address(token_out)] + } else { + vec![] + }; + + // abi encode as [tokens to approve to swap router 02, multicall data, tokens to approve to reactor] let calldata = ethabi::encode(&[ - Token::Array(vec![Token::Address(H160::from_str(&request.token_in)?)]), + Token::Array(swaprouter_02_approvals), + Token::Array(reactor_approvals), Token::Bytes(Bytes::from_str(&route.method_parameters.calldata)?.encode()), ]); let mut call = fill_contract.execute_batch(signed_orders, Bytes::from(calldata)); @@ -371,4 +400,13 @@ impl UniswapXUniswapFill { .saturating_mul(gas_use_eth) .checked_div(U256::from_str_radix(&route.gas_use_estimate_quote, 10).ok()?) } + + async fn needs_approval(&self, token: Address, from: &str, to: &str) -> Option { + let token_contract = ERC20::new(token, self.client.clone()); + let allowance = token_contract + .allowance(H160::from_str(from).ok()?, H160::from_str(to).ok()?) + .await + .ok()?; + Some(allowance < U256::MAX / 2) + } } From 96d876746a9a6aec3271d60921ec98c3f78b5e56 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Wed, 24 Jul 2024 15:41:22 -0400 Subject: [PATCH 12/31] fix routing calls --- src/collectors/uniswapx_route_collector.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index 7aa529f..0d26195 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -87,6 +87,8 @@ struct RoutingApiQuery { recipient: String, slippage_tolerance: String, deadline: u64, + #[serde(rename = "enableUniversalRouter")] + enable_universal_router: bool, } #[derive(Clone, Debug, Deserialize)] @@ -231,14 +233,15 @@ impl Collector for UniswapXRouteCollector { pub async fn route_order(params: RouteOrderParams) -> Result { // TODO: support exactOutput let query = RoutingApiQuery { - token_in_address: params.token_in, - token_out_address: params.token_out, + token_in_address: resolve_address(params.token_in), + token_out_address: resolve_address(params.token_out), token_in_chain_id: CHAIN_ID, token_out_chain_id: CHAIN_ID, trade_type: TradeType::ExactIn, amount: params.amount, recipient: EXECUTOR_ADDRESS.to_string(), slippage_tolerance: SLIPPAGE_TOLERANCE.to_string(), + enable_universal_router: false, deadline: DEADLINE, }; @@ -255,3 +258,11 @@ pub async fn route_order(params: RouteOrderParams) -> Result { .json::() .await?) } + +// our routing provider requires that "ETH" be used instead of the zero address +fn resolve_address(token: String) -> String { + if token == "0x0000000000000000000000000000000000000000" { + return "ETH".to_string(); + } + return token; +} \ No newline at end of file From eccc6dba33cc3931b03b36528fde868da34a04b0 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Wed, 24 Jul 2024 15:41:42 -0400 Subject: [PATCH 13/31] fix multicall decoding --- src/strategies/uniswapx_strategy.rs | 85 ++++++++++++++++++----------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 8d95275..0d9feeb 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -13,12 +13,12 @@ use bindings_uniswapx::{ erc20::ERC20, shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor, }; use ethers::{ - abi::{ethabi, AbiEncode, Token}, + abi::{ethabi, ParamType, Token}, providers::Middleware, types::{transaction::eip2718::TypedTransaction, Address, Bytes, Filter, H160, U256}, utils::hex, }; -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Debug}; use std::error::Error; use std::str::FromStr; use std::sync::Arc; @@ -31,10 +31,10 @@ use super::types::{Action, Event}; const BLOCK_TIME: u64 = 12; const DONE_EXPIRY: u64 = 300; -const REACTOR_ADDRESS: &str = "0xe80bF394d190851E215D5F67B67f8F5A52783F1E"; +const REACTOR_ADDRESS: &str = "0x00000011F84B9aa48e5f8aA8B9897600006289Be"; const SWAPROUTER_02_ADDRESS: &str = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; pub const WETH_ADDRESS: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; -pub const EXECUTOR_ADDRESS: &str = "TODO: Fill in swaprouter02 executor address"; +pub const EXECUTOR_ADDRESS: &str = "0xa6b19B30593F6e70eabf6c05f9C96d66da65a0A1"; #[derive(Debug)] #[allow(dead_code)] @@ -97,7 +97,6 @@ impl Strategy for UniswapXUniswapFill impl UniswapXUniswapFill { fn decode_order(&self, encoded_order: &str) -> Result> { - info!("Decoding order: {}", encoded_order); let encoded_order = if encoded_order.starts_with("0x") { &encoded_order[2..] } else { @@ -212,30 +211,42 @@ impl UniswapXUniswapFill { let token_in: H160 = H160::from_str(&request.token_in)?; let token_out: H160 = H160::from_str(&request.token_out)?; - let swaprouter_02_approvals = if self - .needs_approval(token_in, EXECUTOR_ADDRESS, SWAPROUTER_02_ADDRESS) - .await - .is_some() - { - vec![Token::Address(token_in)] - } else { - vec![] - }; - let reactor_approvals = if self - .needs_approval(token_out, EXECUTOR_ADDRESS, REACTOR_ADDRESS) - .await - .is_some() - { - vec![Token::Address(token_out)] - } else { - vec![] + + let swaprouter_02_approval = self.get_tokens_to_approve( + token_in, + EXECUTOR_ADDRESS, + SWAPROUTER_02_ADDRESS, + ).await?; + + let reactor_approval = self.get_tokens_to_approve( + token_out, + EXECUTOR_ADDRESS, + REACTOR_ADDRESS, + ).await?; + + // Strip off function selector + let multicall_bytes = &route.method_parameters.calldata[10..]; + println!("Multicall bytes: {}", multicall_bytes); + + // Decode multicall into [Uint256, bytes[]] (deadline, multicallData) + let decoded_multicall_bytes = ethabi::decode( + &[ParamType::Uint(256), ParamType::Array(Box::new(ParamType::Bytes))], + &Bytes::from_str(multicall_bytes).ok().expect("Failed to decode multicall bytes"), + ); + + let decoded_multicall_bytes = match decoded_multicall_bytes { + Ok(data) => data[1].clone(), // already in bytes[] + Err(e) => { + return Err(anyhow::anyhow!("Failed to decode multicall bytes: {}", e)); + } }; - // abi encode as [tokens to approve to swap router 02, multicall data, tokens to approve to reactor] + // abi encode as [tokens to approve to swap router 02, tokens to approve to reactor, multicall data] + // [address[], address[], bytes[]] let calldata = ethabi::encode(&[ - Token::Array(swaprouter_02_approvals), - Token::Array(reactor_approvals), - Token::Bytes(Bytes::from_str(&route.method_parameters.calldata)?.encode()), + Token::Array(swaprouter_02_approval), + Token::Array(reactor_approval), + decoded_multicall_bytes, ]); let mut call = fill_contract.execute_batch(signed_orders, Bytes::from(calldata)); Ok(call.tx.set_chain_id(CHAIN_ID).clone()) @@ -381,7 +392,7 @@ impl UniswapXUniswapFill { } fn get_profit_eth(&self, RoutedOrder { request, route }: &RoutedOrder) -> Option { - let quote = U256::from_str_radix(&route.quote, 10).ok()?; + let quote = U256::from_str_radix(&route.quote_gas_adjusted, 10).ok()?; let amount_out_required = U256::from_str_radix(&request.amount_out_required.to_string(), 10).ok()?; if quote.le(&amount_out_required) { @@ -401,12 +412,24 @@ impl UniswapXUniswapFill { .checked_div(U256::from_str_radix(&route.gas_use_estimate_quote, 10).ok()?) } - async fn needs_approval(&self, token: Address, from: &str, to: &str) -> Option { + async fn get_tokens_to_approve( + &self, + token: Address, + from: &str, + to: &str, + ) -> Result, anyhow::Error> { + if token == Address::zero() { + return Ok(vec![]); + } let token_contract = ERC20::new(token, self.client.clone()); let allowance = token_contract - .allowance(H160::from_str(from).ok()?, H160::from_str(to).ok()?) + .allowance(H160::from_str(from).ok().expect("Error encoding from address"), H160::from_str(to).ok().expect("Error encoding from address")) .await - .ok()?; - Some(allowance < U256::MAX / 2) + .ok().expect("Failed to get allowance"); + if allowance < U256::MAX / 2 { + Ok(vec![Token::Address(token)]) + } else { + Ok(vec![]) + } } } From 6a02f9a751f5a616048514773f7eca02d36d4826 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Wed, 24 Jul 2024 16:18:26 -0400 Subject: [PATCH 14/31] use quote not quote gas adjusted --- src/strategies/uniswapx_strategy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 0d9feeb..55820e1 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -392,7 +392,7 @@ impl UniswapXUniswapFill { } fn get_profit_eth(&self, RoutedOrder { request, route }: &RoutedOrder) -> Option { - let quote = U256::from_str_radix(&route.quote_gas_adjusted, 10).ok()?; + 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) { From 22fa4073b28edbfc377264fcfd08449cf0e82a0a Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Thu, 25 Jul 2024 14:54:43 -0400 Subject: [PATCH 15/31] Move strategy shared functions to shared --- src/collectors/uniswapx_route_collector.rs | 4 +- src/strategies/mod.rs | 1 + src/strategies/shared.rs | 116 +++++++++++++++++++++ src/strategies/uniswapx_strategy.rs | 109 ++++--------------- 4 files changed, 138 insertions(+), 92 deletions(-) create mode 100644 src/strategies/shared.rs diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index 0d26195..cb41655 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -1,4 +1,5 @@ use crate::collectors::uniswapx_order_collector::CHAIN_ID; +use crate::strategies::shared::EXECUTOR_ADDRESS; use alloy_primitives::Uint; use anyhow::Result; use reqwest::header::ORIGIN; @@ -7,7 +8,6 @@ use tokio::sync::mpsc::{Receiver, Sender}; use tracing::info; use uniswapx_rs::order::{PriorityOrder, ResolvedOrder, V2DutchOrder}; -use crate::strategies::uniswapx_strategy::EXECUTOR_ADDRESS; use artemis_core::types::{Collector, CollectorStream}; use async_trait::async_trait; use futures::lock::Mutex; @@ -265,4 +265,4 @@ fn resolve_address(token: String) -> String { return "ETH".to_string(); } return token; -} \ No newline at end of file +} diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index d3366f5..850329d 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -1,3 +1,4 @@ pub mod priority_strategy; +pub mod shared; pub mod types; pub mod uniswapx_strategy; diff --git a/src/strategies/shared.rs b/src/strategies/shared.rs new file mode 100644 index 0000000..13fba43 --- /dev/null +++ b/src/strategies/shared.rs @@ -0,0 +1,116 @@ +use crate::collectors::{ + uniswapx_order_collector::CHAIN_ID, uniswapx_route_collector::RoutedOrder, +}; +use anyhow::Result; +use async_trait::async_trait; +use bindings_uniswapx::{ + erc20::ERC20, shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor, +}; +use ethers::{ + abi::{ethabi, ParamType, Token}, + providers::Middleware, + types::{transaction::eip2718::TypedTransaction, Address, Bytes, H160, U256}, +}; +use std::{str::FromStr, time::{SystemTime, UNIX_EPOCH}}; +use std::sync::Arc; + +const REACTOR_ADDRESS: &str = "0x00000011F84B9aa48e5f8aA8B9897600006289Be"; +const SWAPROUTER_02_ADDRESS: &str = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; +pub const WETH_ADDRESS: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; +pub const EXECUTOR_ADDRESS: &str = "0xa6b19B30593F6e70eabf6c05f9C96d66da65a0A1"; + +#[async_trait] +pub trait UniswapXStrategy { + // builds a transaction to fill an order + async fn build_fill( + &self, + client: Arc, + signed_orders: Vec, + RoutedOrder { request, route }: RoutedOrder, + ) -> Result { + let fill_contract = + SwapRouter02Executor::new(H160::from_str(EXECUTOR_ADDRESS)?, client.clone()); + + let token_in: H160 = H160::from_str(&request.token_in)?; + let token_out: H160 = H160::from_str(&request.token_out)?; + + let swaprouter_02_approval = self + .get_tokens_to_approve( + client.clone(), + token_in, + EXECUTOR_ADDRESS, + SWAPROUTER_02_ADDRESS, + ) + .await?; + + let reactor_approval = self + .get_tokens_to_approve(client.clone(), token_out, EXECUTOR_ADDRESS, REACTOR_ADDRESS) + .await?; + + // Strip off function selector + let multicall_bytes = &route.method_parameters.calldata[10..]; + + // Decode multicall into [Uint256, bytes[]] (deadline, multicallData) + let decoded_multicall_bytes = ethabi::decode( + &[ + ParamType::Uint(256), + ParamType::Array(Box::new(ParamType::Bytes)), + ], + &Bytes::from_str(multicall_bytes) + .ok() + .expect("Failed to decode multicall bytes"), + ); + + let decoded_multicall_bytes = match decoded_multicall_bytes { + Ok(data) => data[1].clone(), // already in bytes[] + Err(e) => { + return Err(anyhow::anyhow!("Failed to decode multicall bytes: {}", e)); + } + }; + + // abi encode as [tokens to approve to swap router 02, tokens to approve to reactor, multicall data] + // [address[], address[], bytes[]] + let calldata = ethabi::encode(&[ + Token::Array(swaprouter_02_approval), + Token::Array(reactor_approval), + decoded_multicall_bytes, + ]); + let mut call = fill_contract.execute_batch(signed_orders, Bytes::from(calldata)); + Ok(call.tx.set_chain_id(CHAIN_ID).clone()) + } + + fn current_timestamp(&self) -> Result { + let start = SystemTime::now(); + Ok(start.duration_since(UNIX_EPOCH)?.as_secs()) + } + + async fn get_tokens_to_approve( + &self, + client: Arc, + token: Address, + from: &str, + to: &str, + ) -> Result, anyhow::Error> { + if token == Address::zero() { + return Ok(vec![]); + } + let token_contract = ERC20::new(token, client.clone()); + let allowance = token_contract + .allowance( + H160::from_str(from) + .ok() + .expect("Error encoding from address"), + H160::from_str(to) + .ok() + .expect("Error encoding from address"), + ) + .await + .ok() + .expect("Failed to get allowance"); + if allowance < U256::MAX / 2 { + Ok(vec![Token::Address(token)]) + } else { + Ok(vec![]) + } + } +} diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 55820e1..c9308bb 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -1,7 +1,10 @@ -use super::types::{Config, OrderStatus, TokenInTokenOut}; +use super::{ + shared::{UniswapXStrategy, WETH_ADDRESS}, + types::{Config, OrderStatus, TokenInTokenOut}, +}; use crate::collectors::{ block_collector::NewBlock, - uniswapx_order_collector::{UniswapXOrder, CHAIN_ID}, + uniswapx_order_collector::UniswapXOrder, uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder, V2DutchOrderData}, }; use alloy_primitives::Uint; @@ -9,20 +12,16 @@ use anyhow::Result; use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; use artemis_core::types::Strategy; use async_trait::async_trait; -use bindings_uniswapx::{ - erc20::ERC20, shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor, -}; +use bindings_uniswapx::shared_types::SignedOrder; use ethers::{ - abi::{ethabi, ParamType, Token}, providers::Middleware, - types::{transaction::eip2718::TypedTransaction, Address, Bytes, Filter, H160, U256}, + types::{Address, Bytes, Filter, U256}, utils::hex, }; -use std::{collections::HashMap, fmt::Debug}; use std::error::Error; use std::str::FromStr; use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{collections::HashMap, fmt::Debug}; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::{error, info}; use uniswapx_rs::order::{OrderResolution, V2DutchOrder}; @@ -32,9 +31,6 @@ use super::types::{Action, Event}; const BLOCK_TIME: u64 = 12; const DONE_EXPIRY: u64 = 300; const REACTOR_ADDRESS: &str = "0x00000011F84B9aa48e5f8aA8B9897600006289Be"; -const SWAPROUTER_02_ADDRESS: &str = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; -pub const WETH_ADDRESS: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; -pub const EXECUTOR_ADDRESS: &str = "0xa6b19B30593F6e70eabf6c05f9C96d66da65a0A1"; #[derive(Debug)] #[allow(dead_code)] @@ -95,6 +91,8 @@ impl Strategy for UniswapXUniswapFill } } +impl UniswapXStrategy for UniswapXUniswapFill {} + impl UniswapXUniswapFill { fn decode_order(&self, encoded_order: &str) -> Result> { let encoded_order = if encoded_order.starts_with("0x") { @@ -147,9 +145,12 @@ impl UniswapXUniswapFill { amount_out_required, profit ); - + let signed_orders = self.get_signed_orders(orders.clone()).ok()?; return Some(Action::SubmitTx(SubmitTxToMempool { - tx: self.build_fill(event).await.ok()?, + tx: self + .build_fill(self.client.clone(), signed_orders, event) + .await + .ok()?, gas_bid_info: Some(GasBidInfo { bid_percentage: self.bid_percentage, total_profit: profit, @@ -187,15 +188,10 @@ impl UniswapXUniswapFill { None } - // builds a transaction to fill an order - async fn build_fill( - &self, - RoutedOrder { request, route }: RoutedOrder, - ) -> Result { - let fill_contract = - SwapRouter02Executor::new(H160::from_str(EXECUTOR_ADDRESS)?, self.client.clone()); + /// encode orders into generic signed orders + fn get_signed_orders(&self, orders: Vec) -> Result> { let mut signed_orders: Vec = Vec::new(); - for batch in request.orders.iter() { + for batch in orders.iter() { match batch { OrderData::V2DutchOrderData(order) => { signed_orders.push(SignedOrder { @@ -208,48 +204,7 @@ impl UniswapXUniswapFill { } } } - - let token_in: H160 = H160::from_str(&request.token_in)?; - let token_out: H160 = H160::from_str(&request.token_out)?; - - let swaprouter_02_approval = self.get_tokens_to_approve( - token_in, - EXECUTOR_ADDRESS, - SWAPROUTER_02_ADDRESS, - ).await?; - - let reactor_approval = self.get_tokens_to_approve( - token_out, - EXECUTOR_ADDRESS, - REACTOR_ADDRESS, - ).await?; - - // Strip off function selector - let multicall_bytes = &route.method_parameters.calldata[10..]; - println!("Multicall bytes: {}", multicall_bytes); - - // Decode multicall into [Uint256, bytes[]] (deadline, multicallData) - let decoded_multicall_bytes = ethabi::decode( - &[ParamType::Uint(256), ParamType::Array(Box::new(ParamType::Bytes))], - &Bytes::from_str(multicall_bytes).ok().expect("Failed to decode multicall bytes"), - ); - - let decoded_multicall_bytes = match decoded_multicall_bytes { - Ok(data) => data[1].clone(), // already in bytes[] - Err(e) => { - return Err(anyhow::anyhow!("Failed to decode multicall bytes: {}", e)); - } - }; - - // abi encode as [tokens to approve to swap router 02, tokens to approve to reactor, multicall data] - // [address[], address[], bytes[]] - let calldata = ethabi::encode(&[ - Token::Array(swaprouter_02_approval), - Token::Array(reactor_approval), - decoded_multicall_bytes, - ]); - let mut call = fill_contract.execute_batch(signed_orders, Bytes::from(calldata)); - Ok(call.tx.set_chain_id(CHAIN_ID).clone()) + Ok(signed_orders) } fn get_order_batches(&self) -> HashMap { @@ -386,11 +341,6 @@ impl UniswapXUniswapFill { } } - fn current_timestamp(&self) -> Result { - let start = SystemTime::now(); - Ok(start.duration_since(UNIX_EPOCH)?.as_secs()) - } - fn get_profit_eth(&self, RoutedOrder { request, route }: &RoutedOrder) -> Option { let quote = U256::from_str_radix(&route.quote, 10).ok()?; let amount_out_required = @@ -411,25 +361,4 @@ impl UniswapXUniswapFill { .saturating_mul(gas_use_eth) .checked_div(U256::from_str_radix(&route.gas_use_estimate_quote, 10).ok()?) } - - async fn get_tokens_to_approve( - &self, - token: Address, - from: &str, - to: &str, - ) -> Result, anyhow::Error> { - if token == Address::zero() { - return Ok(vec![]); - } - let token_contract = ERC20::new(token, self.client.clone()); - let allowance = token_contract - .allowance(H160::from_str(from).ok().expect("Error encoding from address"), H160::from_str(to).ok().expect("Error encoding from address")) - .await - .ok().expect("Failed to get allowance"); - if allowance < U256::MAX / 2 { - Ok(vec![Token::Address(token)]) - } else { - Ok(vec![]) - } - } } From 30fadaaf2e2689ddac385f9cab408229bdebea2a Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Thu, 25 Jul 2024 15:01:01 -0400 Subject: [PATCH 16/31] use shraed logic in priority strategy --- src/strategies/priority_strategy.rs | 43 ++++++++++++----------------- src/strategies/shared.rs | 5 +++- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index 52926c7..ef9d46f 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -1,7 +1,10 @@ -use super::types::{Config, OrderStatus}; +use super::{ + shared::UniswapXStrategy, + types::{Config, OrderStatus}, +}; use crate::collectors::{ block_collector::NewBlock, - uniswapx_order_collector::{UniswapXOrder, CHAIN_ID}, + uniswapx_order_collector::UniswapXOrder, uniswapx_route_collector::{OrderBatchData, OrderData, PriorityOrderData, RoutedOrder}, }; use alloy_primitives::Uint; @@ -9,18 +12,16 @@ use anyhow::Result; use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; use artemis_core::types::Strategy; use async_trait::async_trait; -use bindings_uniswapx::{shared_types::SignedOrder, swap_router_02_executor::SwapRouter02Executor}; +use bindings_uniswapx::shared_types::SignedOrder; use ethers::{ - abi::{ethabi, AbiEncode, Token}, providers::Middleware, - types::{transaction::eip2718::TypedTransaction, Address, Bytes, Filter, H160, U256}, + types::{Address, Bytes, Filter, U256}, utils::hex, }; use std::collections::HashMap; use std::error::Error; use std::str::FromStr; use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::{error, info}; use uniswapx_rs::order::{OrderResolution, PriorityOrder}; @@ -29,7 +30,6 @@ use super::types::{Action, Event}; const DONE_EXPIRY: u64 = 300; const REACTOR_ADDRESS: &str = ""; -const EXECUTOR_ADDRESS: &str = ""; pub const WETH_ADDRESS: &str = ""; #[derive(Debug)] @@ -91,6 +91,8 @@ impl Strategy for UniswapXPriorityFill UniswapXStrategy for UniswapXPriorityFill {} + impl UniswapXPriorityFill { fn decode_order(&self, encoded_order: &str) -> Result> { let encoded_order = if encoded_order.starts_with("0x") { @@ -143,8 +145,12 @@ impl UniswapXPriorityFill { profit ); + let signed_orders = self.get_signed_orders(orders.clone()).ok()?; return Some(Action::SubmitPublicTx(SubmitTxToMempool { - tx: self.build_fill(event).ok()?, + tx: self + .build_fill(self.client.clone(), signed_orders, event) + .await + .ok()?, gas_bid_info: Some(GasBidInfo { bid_percentage: self.bid_percentage, total_profit: profit, @@ -182,12 +188,10 @@ impl UniswapXPriorityFill { None } - // builds a transaction to fill an order - fn build_fill(&self, RoutedOrder { request, route }: RoutedOrder) -> Result { - let fill_contract = - SwapRouter02Executor::new(H160::from_str(EXECUTOR_ADDRESS)?, self.client.clone()); + /// encode orders into generic signed orders + fn get_signed_orders(&self, orders: Vec) -> Result> { let mut signed_orders: Vec = Vec::new(); - for batch in request.orders.iter() { + for batch in orders.iter() { match batch { OrderData::PriorityOrderData(order) => { signed_orders.push(SignedOrder { @@ -200,13 +204,7 @@ impl UniswapXPriorityFill { } } } - // abi encode as [tokens to approve, multicall data] - let calldata = ethabi::encode(&[ - Token::Array(vec![Token::Address(H160::from_str(&request.token_in)?)]), - Token::Bytes(Bytes::from_str(&route.method_parameters.calldata)?.encode()), - ]); - let mut call = fill_contract.execute_batch(signed_orders, Bytes::from(calldata)); - Ok(call.tx.set_chain_id(CHAIN_ID).clone()) + Ok(signed_orders) } /// We do not batch orders because priority fee is applied on the transaction level @@ -350,9 +348,4 @@ impl UniswapXPriorityFill { .insert(order.to_string(), self.last_block_timestamp + DONE_EXPIRY); } } - - fn current_timestamp(&self) -> Result { - let start = SystemTime::now(); - Ok(start.duration_since(UNIX_EPOCH)?.as_secs()) - } } diff --git a/src/strategies/shared.rs b/src/strategies/shared.rs index 13fba43..a34cfcf 100644 --- a/src/strategies/shared.rs +++ b/src/strategies/shared.rs @@ -11,8 +11,11 @@ use ethers::{ providers::Middleware, types::{transaction::eip2718::TypedTransaction, Address, Bytes, H160, U256}, }; -use std::{str::FromStr, time::{SystemTime, UNIX_EPOCH}}; use std::sync::Arc; +use std::{ + str::FromStr, + time::{SystemTime, UNIX_EPOCH}, +}; const REACTOR_ADDRESS: &str = "0x00000011F84B9aa48e5f8aA8B9897600006289Be"; const SWAPROUTER_02_ADDRESS: &str = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; From 30d582b820265ab891457023d55f77954de5290a Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Mon, 5 Aug 2024 13:34:43 -0400 Subject: [PATCH 17/31] Add base addresses --- src/strategies/priority_strategy.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index ef9d46f..39e8126 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -29,8 +29,9 @@ use uniswapx_rs::order::{OrderResolution, PriorityOrder}; use super::types::{Action, Event}; const DONE_EXPIRY: u64 = 300; -const REACTOR_ADDRESS: &str = ""; -pub const WETH_ADDRESS: &str = ""; +// Base addresses +const REACTOR_ADDRESS: &str = "0x000000001Ec5656dcdB24D90DFa42742738De729"; +pub const WETH_ADDRESS: &str = "0x4200000000000000000000000000000000000006"; #[derive(Debug)] #[allow(dead_code)] From 1259f7b50e5443a522bb66dba0de91079e102f75 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Mon, 5 Aug 2024 16:20:14 -0400 Subject: [PATCH 18/31] add chain_id as an argument --- src/collectors/uniswapx_order_collector.rs | 1 - src/collectors/uniswapx_route_collector.rs | 10 +++++++--- src/main.rs | 16 +++++++++++----- src/strategies/shared.rs | 7 +++---- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/collectors/uniswapx_order_collector.rs b/src/collectors/uniswapx_order_collector.rs index 4eda20e..de52e26 100644 --- a/src/collectors/uniswapx_order_collector.rs +++ b/src/collectors/uniswapx_order_collector.rs @@ -9,7 +9,6 @@ use tokio_stream::wrappers::IntervalStream; static UNISWAPX_API_URL: &str = "https://api.uniswap.org/v2"; static POLL_INTERVAL_SECS: u64 = 5; -pub const CHAIN_ID: u64 = 1; #[derive(Debug, PartialEq, Eq)] pub enum OrderType { diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index cb41655..7799624 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -1,4 +1,3 @@ -use crate::collectors::uniswapx_order_collector::CHAIN_ID; use crate::strategies::shared::EXECUTOR_ADDRESS; use alloy_primitives::Uint; use anyhow::Result; @@ -150,6 +149,7 @@ pub struct OrderRoute { } pub struct RouteOrderParams { + pub chain_id: u64, pub token_in: String, pub token_out: String, pub amount: String, @@ -171,17 +171,20 @@ pub struct RouteResponse { /// [events](Route) which contain the order. pub struct UniswapXRouteCollector { pub client: Client, + pub chain_id: u64, pub route_request_receiver: Mutex>>, pub route_sender: Sender, } impl UniswapXRouteCollector { pub fn new( + chain_id: u64, route_request_receiver: Receiver>, route_sender: Sender, ) -> Self { Self { client: Client::new(), + chain_id, route_request_receiver: Mutex::new(route_request_receiver), route_sender, } @@ -207,6 +210,7 @@ impl Collector for UniswapXRouteCollector { async move { (batch, route_order(RouteOrderParams { + chain_id: self.chain_id, token_in: token_in.clone(), token_out: token_out.clone(), amount: amount_in.to_string(), @@ -235,8 +239,8 @@ pub async fn route_order(params: RouteOrderParams) -> Result { let query = RoutingApiQuery { token_in_address: resolve_address(params.token_in), token_out_address: resolve_address(params.token_out), - token_in_chain_id: CHAIN_ID, - token_out_chain_id: CHAIN_ID, + token_in_chain_id: params.chain_id, + token_out_chain_id: params.chain_id, trade_type: TradeType::ExactIn, amount: params.amount, recipient: EXECUTOR_ADDRESS.to_string(), diff --git a/src/main.rs b/src/main.rs index 109341e..3fce1ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use artemis_core::types::{CollectorMap, ExecutorMap}; use collectors::uniswapx_order_collector::OrderType; use collectors::{ block_collector::BlockCollector, - uniswapx_order_collector::{UniswapXOrderCollector, CHAIN_ID}, + uniswapx_order_collector::UniswapXOrderCollector, uniswapx_route_collector::UniswapXRouteCollector, }; use ethers::{ @@ -45,6 +45,10 @@ pub struct Args { /// Percentage of profit to pay in gas. #[arg(long)] pub bid_percentage: u64, + + /// chain id + #[arg(long)] + pub chain_id: u64, } #[tokio::main] @@ -64,6 +68,7 @@ async fn main() -> Result<()> { // Set up ethers provider. let ws = Ws::connect(args.wss).await?; let provider = Provider::new(ws); + let chain_id = args.chain_id; let mevblocker_provider = Provider::::try_from(MEV_BLOCKER).expect("could not instantiate HTTP Provider"); @@ -72,7 +77,7 @@ async fn main() -> Result<()> { .private_key .parse::() .unwrap() - .with_chain_id(CHAIN_ID); + .with_chain_id(chain_id); let address = wallet.address(); let provider = Arc::new(provider.nonce_manager(address).with_signer(wallet.clone())); @@ -95,24 +100,25 @@ async fn main() -> Result<()> { let (route_sender, route_receiver) = channel(512); let (priority_route_sender, priority_route_receiver) = channel(512); - let uniswapx_collector = Box::new(UniswapXOrderCollector::new(1, OrderType::Dutch)); + let uniswapx_collector = Box::new(UniswapXOrderCollector::new(chain_id, OrderType::Dutch)); let uniswapx_collector = CollectorMap::new(uniswapx_collector, |e| Event::UniswapXOrder(Box::new(e))); engine.add_collector(Box::new(uniswapx_collector)); - let priority_collector = Box::new(UniswapXOrderCollector::new(1, OrderType::Priority)); + let priority_collector = Box::new(UniswapXOrderCollector::new(chain_id, OrderType::Priority)); let priority_collector = CollectorMap::new(priority_collector, |e| Event::PriorityOrder(Box::new(e))); engine.add_collector(Box::new(priority_collector)); let uniswapx_route_collector = - Box::new(UniswapXRouteCollector::new(batch_receiver, route_sender)); + Box::new(UniswapXRouteCollector::new(chain_id, batch_receiver, route_sender)); let uniswapx_route_collector = CollectorMap::new(uniswapx_route_collector, |e| { Event::UniswapXRoute(Box::new(e)) }); engine.add_collector(Box::new(uniswapx_route_collector)); let priority_route_collector = Box::new(UniswapXRouteCollector::new( + chain_id, priority_batch_receiver, priority_route_sender, )); diff --git a/src/strategies/shared.rs b/src/strategies/shared.rs index a34cfcf..1b37b35 100644 --- a/src/strategies/shared.rs +++ b/src/strategies/shared.rs @@ -1,6 +1,4 @@ -use crate::collectors::{ - uniswapx_order_collector::CHAIN_ID, uniswapx_route_collector::RoutedOrder, -}; +use crate::collectors::uniswapx_route_collector::RoutedOrder; use anyhow::Result; use async_trait::async_trait; use bindings_uniswapx::{ @@ -31,6 +29,7 @@ pub trait UniswapXStrategy { signed_orders: Vec, RoutedOrder { request, route }: RoutedOrder, ) -> Result { + let chain_id: U256 = client.get_chainid().await?; let fill_contract = SwapRouter02Executor::new(H160::from_str(EXECUTOR_ADDRESS)?, client.clone()); @@ -79,7 +78,7 @@ pub trait UniswapXStrategy { decoded_multicall_bytes, ]); let mut call = fill_contract.execute_batch(signed_orders, Bytes::from(calldata)); - Ok(call.tx.set_chain_id(CHAIN_ID).clone()) + Ok(call.tx.set_chain_id(chain_id.as_u64()).clone()) } fn current_timestamp(&self) -> Result { From 452c826dcebd0a411408e552a53391b8a14fa9c9 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 13 Aug 2024 11:50:45 -0400 Subject: [PATCH 19/31] pass in executor address as arg --- src/collectors/uniswapx_route_collector.rs | 8 ++++++-- src/main.rs | 8 +++++++- src/strategies/priority_strategy.rs | 5 ++++- src/strategies/shared.rs | 8 ++++---- src/strategies/types.rs | 1 + src/strategies/uniswapx_strategy.rs | 5 ++++- 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index 7799624..1aa615d 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -1,4 +1,3 @@ -use crate::strategies::shared::EXECUTOR_ADDRESS; use alloy_primitives::Uint; use anyhow::Result; use reqwest::header::ORIGIN; @@ -153,6 +152,7 @@ pub struct RouteOrderParams { pub token_in: String, pub token_out: String, pub amount: String, + pub recipient: String, } #[derive(Clone, Debug)] @@ -174,6 +174,7 @@ pub struct UniswapXRouteCollector { pub chain_id: u64, pub route_request_receiver: Mutex>>, pub route_sender: Sender, + pub executor_address: String, } impl UniswapXRouteCollector { @@ -181,12 +182,14 @@ impl UniswapXRouteCollector { chain_id: u64, route_request_receiver: Receiver>, route_sender: Sender, + executor_address: String ) -> Self { Self { client: Client::new(), chain_id, route_request_receiver: Mutex::new(route_request_receiver), route_sender, + executor_address } } } @@ -214,6 +217,7 @@ impl Collector for UniswapXRouteCollector { token_in: token_in.clone(), token_out: token_out.clone(), amount: amount_in.to_string(), + recipient: self.executor_address.clone(), }).await) } }).collect(); @@ -243,7 +247,7 @@ pub async fn route_order(params: RouteOrderParams) -> Result { token_out_chain_id: params.chain_id, trade_type: TradeType::ExactIn, amount: params.amount, - recipient: EXECUTOR_ADDRESS.to_string(), + recipient: params.recipient, slippage_tolerance: SLIPPAGE_TOLERANCE.to_string(), enable_universal_router: false, deadline: DEADLINE, diff --git a/src/main.rs b/src/main.rs index 3fce1ea..4a90bf3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,6 +46,10 @@ pub struct Args { #[arg(long)] pub bid_percentage: u64, + /// Private key for sending txs. + #[arg(long)] + pub executor_address: String, + /// chain id #[arg(long)] pub chain_id: u64, @@ -111,7 +115,7 @@ async fn main() -> Result<()> { engine.add_collector(Box::new(priority_collector)); let uniswapx_route_collector = - Box::new(UniswapXRouteCollector::new(chain_id, batch_receiver, route_sender)); + Box::new(UniswapXRouteCollector::new(chain_id, batch_receiver, route_sender, args.executor_address.clone())); let uniswapx_route_collector = CollectorMap::new(uniswapx_route_collector, |e| { Event::UniswapXRoute(Box::new(e)) }); @@ -121,6 +125,7 @@ async fn main() -> Result<()> { chain_id, priority_batch_receiver, priority_route_sender, + args.executor_address.clone(), )); let priority_route_collector = CollectorMap::new(priority_route_collector, |e| { Event::UniswapXRoute(Box::new(e)) @@ -129,6 +134,7 @@ async fn main() -> Result<()> { let config = Config { bid_percentage: args.bid_percentage, + executor_address: args.executor_address, }; let uniswapx_strategy = UniswapXUniswapFill::new( diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index 39e8126..c9791f9 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -38,6 +38,8 @@ pub const WETH_ADDRESS: &str = "0x4200000000000000000000000000000000000006"; pub struct UniswapXPriorityFill { /// Ethers client. client: Arc, + /// executor address + executor_address: String, /// Amount of profits to bid in gas bid_percentage: u64, last_block_number: u64, @@ -61,6 +63,7 @@ impl UniswapXPriorityFill { Self { client, + executor_address: config.executor_address, bid_percentage: config.bid_percentage, last_block_number: 0, last_block_timestamp: 0, @@ -149,7 +152,7 @@ impl UniswapXPriorityFill { let signed_orders = self.get_signed_orders(orders.clone()).ok()?; return Some(Action::SubmitPublicTx(SubmitTxToMempool { tx: self - .build_fill(self.client.clone(), signed_orders, event) + .build_fill(self.client.clone(), &self.executor_address, signed_orders, event) .await .ok()?, gas_bid_info: Some(GasBidInfo { diff --git a/src/strategies/shared.rs b/src/strategies/shared.rs index 1b37b35..3d743ff 100644 --- a/src/strategies/shared.rs +++ b/src/strategies/shared.rs @@ -18,7 +18,6 @@ use std::{ const REACTOR_ADDRESS: &str = "0x00000011F84B9aa48e5f8aA8B9897600006289Be"; const SWAPROUTER_02_ADDRESS: &str = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; pub const WETH_ADDRESS: &str = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; -pub const EXECUTOR_ADDRESS: &str = "0xa6b19B30593F6e70eabf6c05f9C96d66da65a0A1"; #[async_trait] pub trait UniswapXStrategy { @@ -26,12 +25,13 @@ pub trait UniswapXStrategy { async fn build_fill( &self, client: Arc, + executor_address: &str, signed_orders: Vec, RoutedOrder { request, route }: RoutedOrder, ) -> Result { let chain_id: U256 = client.get_chainid().await?; let fill_contract = - SwapRouter02Executor::new(H160::from_str(EXECUTOR_ADDRESS)?, client.clone()); + SwapRouter02Executor::new(H160::from_str(executor_address)?, client.clone()); let token_in: H160 = H160::from_str(&request.token_in)?; let token_out: H160 = H160::from_str(&request.token_out)?; @@ -40,13 +40,13 @@ pub trait UniswapXStrategy { .get_tokens_to_approve( client.clone(), token_in, - EXECUTOR_ADDRESS, + &executor_address, SWAPROUTER_02_ADDRESS, ) .await?; let reactor_approval = self - .get_tokens_to_approve(client.clone(), token_out, EXECUTOR_ADDRESS, REACTOR_ADDRESS) + .get_tokens_to_approve(client.clone(), token_out, &executor_address, REACTOR_ADDRESS) .await?; // Strip off function selector diff --git a/src/strategies/types.rs b/src/strategies/types.rs index af8a8de..1eaf2da 100644 --- a/src/strategies/types.rs +++ b/src/strategies/types.rs @@ -25,6 +25,7 @@ pub enum Action { #[derive(Debug, Clone)] pub struct Config { pub bid_percentage: u64, + pub executor_address: String } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index c9308bb..1aeab95 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -37,6 +37,8 @@ const REACTOR_ADDRESS: &str = "0x00000011F84B9aa48e5f8aA8B9897600006289Be"; pub struct UniswapXUniswapFill { /// Ethers client. client: Arc, + /// executor address + executor_address: String, /// Amount of profits to bid in gas bid_percentage: u64, last_block_number: u64, @@ -60,6 +62,7 @@ impl UniswapXUniswapFill { Self { client, + executor_address: config.executor_address, bid_percentage: config.bid_percentage, last_block_number: 0, last_block_timestamp: 0, @@ -148,7 +151,7 @@ impl UniswapXUniswapFill { let signed_orders = self.get_signed_orders(orders.clone()).ok()?; return Some(Action::SubmitTx(SubmitTxToMempool { tx: self - .build_fill(self.client.clone(), signed_orders, event) + .build_fill(self.client.clone(), &self.executor_address, signed_orders, event) .await .ok()?, gas_bid_info: Some(GasBidInfo { From ece59a3a2cc31dcac6db9f5858a8f5cee9431f6c Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Wed, 14 Aug 2024 14:34:41 -0400 Subject: [PATCH 20/31] Add new public 1559 executor --- crates/uniswapx-rs/src/order.rs | 22 ++++-- src/collectors/uniswapx_order_collector.rs | 2 +- src/executors/mod.rs | 1 + src/executors/protect_executor.rs | 6 +- src/executors/public_1559_executor.rs | 64 +++++++++++++++ src/main.rs | 5 +- src/strategies/priority_strategy.rs | 92 +++++++++++++++------- src/strategies/types.rs | 10 ++- src/strategies/uniswapx_strategy.rs | 4 +- 9 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 src/executors/public_1559_executor.rs 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)?, }); } From 996b485d96377367878bc47e46ff4eb80e059d62 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Fri, 16 Aug 2024 11:19:50 -0400 Subject: [PATCH 21/31] resolve priority order based on block number --- crates/uniswapx-rs/src/order.rs | 11 ++++++++--- src/executors/public_1559_executor.rs | 8 ++++---- src/strategies/priority_strategy.rs | 28 ++++++++++++++------------- src/strategies/types.rs | 8 ++++---- src/strategies/uniswapx_strategy.rs | 1 + 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/crates/uniswapx-rs/src/order.rs b/crates/uniswapx-rs/src/order.rs index 62d1a49..b1effcd 100644 --- a/crates/uniswapx-rs/src/order.rs +++ b/crates/uniswapx-rs/src/order.rs @@ -91,10 +91,10 @@ pub enum Order { } impl Order { - pub fn resolve(&self, timestamp: u64, priority_fee: Uint<256, 4>) -> OrderResolution { + pub fn resolve(&self, block_number: u64, timestamp: u64, priority_fee: Uint<256, 4>) -> OrderResolution { match self { Order::V2DutchOrder(order) => order.resolve(timestamp), - Order::PriorityOrder(order) => order.resolve(timestamp, priority_fee), + Order::PriorityOrder(order) => order.resolve(block_number, timestamp, priority_fee), } } @@ -130,6 +130,7 @@ pub enum OrderResolution { Resolved(ResolvedOrder), Expired, Invalid, + NotFillableYet(ResolvedOrder) } impl V2DutchOrder { @@ -202,7 +203,7 @@ impl PriorityOrder { PriorityOrder::encode_single(self) } - pub fn resolve(&self, timestamp: u64, priority_fee: Uint<256, 4>) -> OrderResolution { + pub fn resolve(&self, block_number: u64, timestamp: u64, priority_fee: Uint<256, 4>) -> OrderResolution { let timestamp = Uint::from(timestamp); if self.info.deadline.lt(×tamp) { @@ -216,6 +217,10 @@ impl PriorityOrder { .map(|output| output.scale(priority_fee)) .collect(); + if self.cosignerData.auctionTargetBlock.lt(&Uint::from(block_number)) { + return OrderResolution::NotFillableYet(ResolvedOrder { input, outputs }); + }; + OrderResolution::Resolved(ResolvedOrder { input, outputs }) } } diff --git a/src/executors/public_1559_executor.rs b/src/executors/public_1559_executor.rs index 6b8b11a..8ca14de 100644 --- a/src/executors/public_1559_executor.rs +++ b/src/executors/public_1559_executor.rs @@ -6,7 +6,7 @@ use artemis_core::types::Executor; use async_trait::async_trait; use ethers::{providers::Middleware, types::U256}; -use crate::strategies::types::SubmitTxToMempoolWithAdvancedProfitCalculation; +use crate::strategies::types::SubmitTxToMempoolWithExecutionMetadata; /// An executor that sends transactions to the public mempool. pub struct Public1559Executor { @@ -24,7 +24,7 @@ impl Public1559Executor { } #[async_trait] -impl Executor for Public1559Executor +impl Executor for Public1559Executor where M: Middleware, M::Error: 'static, @@ -32,7 +32,7 @@ where N::Error: 'static, { /// Send a transaction to the mempool. - async fn execute(&self, mut action: SubmitTxToMempoolWithAdvancedProfitCalculation) -> Result<()> { + async fn execute(&self, mut action: SubmitTxToMempoolWithExecutionMetadata) -> Result<()> { let gas_usage_result = self .client .estimate_gas(&action.execution.tx, None) @@ -51,7 +51,7 @@ where 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) + bid_priority_fee = action.metadata.calculate_priority_fee(gas_bid_info.bid_percentage) } else { bid_priority_fee = Some(U256::from(50)); } diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index fe4768e..22e88ca 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -6,7 +6,7 @@ use crate::{collectors::{ block_collector::NewBlock, uniswapx_order_collector::UniswapXOrder, uniswapx_route_collector::{OrderBatchData, OrderData, PriorityOrderData, RoutedOrder}, -}, strategies::types::SubmitTxToMempoolWithAdvancedProfitCalculation}; +}, strategies::types::SubmitTxToMempoolWithExecutionMetadata}; use alloy_primitives::Uint; use anyhow::Result; use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; @@ -35,14 +35,14 @@ const REACTOR_ADDRESS: &str = "0x000000001Ec5656dcdB24D90DFa42742738De729"; pub const WETH_ADDRESS: &str = "0x4200000000000000000000000000000000000006"; #[derive(Debug, Clone)] -pub struct ProfitCalculation { +pub struct ExecutionMetadata { // amount of quote token we can get quote: U256, // amount of quote token needed to fill the order amount_out_required: U256, } -impl ProfitCalculation { +impl ExecutionMetadata { pub fn new(quote: U256, amount_out_required: U256) -> Self { Self { quote, @@ -177,7 +177,7 @@ impl UniswapXPriorityFill { .. } = &event.request; - if let Some(profit) = self.get_profit_calculation(&event) { + if let Some(profit) = self.get_execution_metadata(&event) { info!( "Sending trade: num trades: {} routed quote: {}, batch needs: {}", orders.len(), @@ -186,7 +186,7 @@ impl UniswapXPriorityFill { ); let signed_orders = self.get_signed_orders(orders.clone()).ok()?; - return Some(Action::SubmitPublicTx(SubmitTxToMempoolWithAdvancedProfitCalculation { + return Some(Action::SubmitPublicTx(SubmitTxToMempoolWithExecutionMetadata { execution: SubmitTxToMempool { tx: self .build_fill(self.client.clone(), &self.executor_address, signed_orders, event) @@ -194,10 +194,11 @@ impl UniswapXPriorityFill { .ok()?, gas_bid_info: Some(GasBidInfo { bid_percentage: self.bid_percentage, + // this field is not used for priority orders total_profit: U256::from(0), }), }, - profit_calculation: profit, + metadata: profit, })); } @@ -298,12 +299,12 @@ impl UniswapXPriorityFill { Ok(()) } - /// We still calculate profit in terms of ETH for priority fee orders + /// The profit of a priority order is calculated a bit differently /// Rationale: - /// - 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_calculation(&self, RoutedOrder { request, route }: &RoutedOrder) -> Option { + /// - we will always bid the base fee + /// - since we have to provide 1 MP (1/1000th of a bp) for every wei of priority fee + /// - we return the data needed to calculate the maximum MPS of improvement we can offer from our quote and the order specs + fn get_execution_metadata(&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()?; @@ -312,7 +313,7 @@ impl UniswapXPriorityFill { } return Some({ - ProfitCalculation { + ExecutionMetadata { quote, amount_out_required, } @@ -320,10 +321,11 @@ impl UniswapXPriorityFill { } fn update_order_state(&mut self, order: PriorityOrder, signature: String, order_hash: String) { - let resolved = order.resolve( self.last_block_timestamp + BLOCK_TIME, Uint::from(0)); + let resolved = order.resolve( self.last_block_number, self.last_block_timestamp + BLOCK_TIME, Uint::from(0)); let order_status: OrderStatus = match resolved { OrderResolution::Expired => OrderStatus::Done, OrderResolution::Invalid => OrderStatus::Done, + OrderResolution::NotFillableYet(resolved_order) => OrderStatus::Open(resolved_order), // TODO: gracefully handle this, currently this will cause a revert if we try to fill too earlty OrderResolution::Resolved(resolved_order) => OrderStatus::Open(resolved_order), }; diff --git a/src/strategies/types.rs b/src/strategies/types.rs index 3c0cf19..8c2390b 100644 --- a/src/strategies/types.rs +++ b/src/strategies/types.rs @@ -5,7 +5,7 @@ use crate::collectors::{ use artemis_core::executors::mempool_executor::SubmitTxToMempool; use uniswapx_rs::order::ResolvedOrder; -use super::priority_strategy::ProfitCalculation; +use super::priority_strategy::ExecutionMetadata; /// Core Event enum for the current strategy. #[derive(Debug, Clone)] @@ -17,16 +17,16 @@ pub enum Event { } #[derive(Debug, Clone)] -pub struct SubmitTxToMempoolWithAdvancedProfitCalculation { +pub struct SubmitTxToMempoolWithExecutionMetadata { pub execution: SubmitTxToMempool, - pub profit_calculation: ProfitCalculation, + pub metadata: ExecutionMetadata, } /// Core Action enum for the current strategy. #[derive(Debug, Clone)] pub enum Action { SubmitTx(SubmitTxToMempool), - SubmitPublicTx(SubmitTxToMempoolWithAdvancedProfitCalculation), + SubmitPublicTx(SubmitTxToMempoolWithExecutionMetadata), } /// 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 c71b61c..4830a11 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -317,6 +317,7 @@ impl UniswapXUniswapFill { OrderResolution::Expired => OrderStatus::Done, OrderResolution::Invalid => OrderStatus::Done, OrderResolution::Resolved(resolved_order) => OrderStatus::Open(resolved_order), + _ => OrderStatus::Done }; match order_status { From 50b1498b2254c173ad3ef065ce826ecb68624179 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Mon, 19 Aug 2024 14:27:03 -0400 Subject: [PATCH 22/31] Add notFillableYet --- crates/uniswapx-rs/src/order.rs | 6 +++--- src/collectors/uniswapx_order_collector.rs | 2 +- src/executors/public_1559_executor.rs | 7 +++++-- src/strategies/priority_strategy.rs | 19 ++++++++++++++----- src/strategies/types.rs | 1 + src/strategies/uniswapx_strategy.rs | 2 ++ 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/crates/uniswapx-rs/src/order.rs b/crates/uniswapx-rs/src/order.rs index b1effcd..5d2e800 100644 --- a/crates/uniswapx-rs/src/order.rs +++ b/crates/uniswapx-rs/src/order.rs @@ -130,7 +130,7 @@ pub enum OrderResolution { Resolved(ResolvedOrder), Expired, Invalid, - NotFillableYet(ResolvedOrder) + NotFillableYet } impl V2DutchOrder { @@ -217,8 +217,8 @@ impl PriorityOrder { .map(|output| output.scale(priority_fee)) .collect(); - if self.cosignerData.auctionTargetBlock.lt(&Uint::from(block_number)) { - return OrderResolution::NotFillableYet(ResolvedOrder { input, outputs }); + if Uint::from(block_number).lt(&self.cosignerData.auctionTargetBlock.saturating_sub(Uint::from(1))) { + return OrderResolution::NotFillableYet; }; OrderResolution::Resolved(ResolvedOrder { input, outputs }) diff --git a/src/collectors/uniswapx_order_collector.rs b/src/collectors/uniswapx_order_collector.rs index 5db121a..7e1c0ae 100644 --- a/src/collectors/uniswapx_order_collector.rs +++ b/src/collectors/uniswapx_order_collector.rs @@ -8,7 +8,7 @@ use tokio::time::Duration; use tokio_stream::wrappers::IntervalStream; static UNISWAPX_API_URL: &str = "https://api.uniswap.org/v2"; -static POLL_INTERVAL_SECS: u64 = 5; +static POLL_INTERVAL_SECS: u64 = 1; #[derive(Debug, PartialEq, Eq)] pub enum OrderType { diff --git a/src/executors/public_1559_executor.rs b/src/executors/public_1559_executor.rs index 8ca14de..8a3635a 100644 --- a/src/executors/public_1559_executor.rs +++ b/src/executors/public_1559_executor.rs @@ -37,9 +37,11 @@ where .client .estimate_gas(&action.execution.tx, None) .await - .context("Error estimating gas usage: {}"); + .unwrap_or_else(|err| { + info!("Error estimating gas: {}", err); + U256::from(1_000_000) + }); info!("Gas Usage {:?}", gas_usage_result); - // let gas_usage = gas_usage_result?; let bid_priority_fee; let base_fee: U256 = self @@ -57,6 +59,7 @@ where } 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; + action.execution.tx.set_gas(U256::from(1_000_000)); info!("Executing tx {:?}", action.execution.tx); self.sender_client.send_transaction(action.execution.tx, None).await?; Ok(()) diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index 22e88ca..8ecc054 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -157,6 +157,12 @@ impl UniswapXPriorityFill { .ok()?; self.update_order_state(order, event.signature, event.order_hash); + // try to send immediately + self.batch_sender + .send(self.get_order_batches()) + .await + .ok()?; + None } @@ -224,10 +230,10 @@ impl UniswapXPriorityFill { self.update_open_orders(); self.prune_done_orders(); - self.batch_sender - .send(self.get_order_batches()) - .await - .ok()?; + // self.batch_sender + // .send(self.get_order_batches()) + // .await + // .ok()?; None } @@ -325,7 +331,7 @@ impl UniswapXPriorityFill { let order_status: OrderStatus = match resolved { OrderResolution::Expired => OrderStatus::Done, OrderResolution::Invalid => OrderStatus::Done, - OrderResolution::NotFillableYet(resolved_order) => OrderStatus::Open(resolved_order), // TODO: gracefully handle this, currently this will cause a revert if we try to fill too earlty + OrderResolution::NotFillableYet => OrderStatus::NotFillableYet, // TODO: gracefully handle this, currently this will cause a revert if we try to fill too earlty OrderResolution::Resolved(resolved_order) => OrderStatus::Open(resolved_order), }; @@ -333,6 +339,9 @@ impl UniswapXPriorityFill { OrderStatus::Done => { self.mark_as_done(&order_hash); } + OrderStatus::NotFillableYet => { + info!("Order not fillable yet, skipping: {}", order_hash); + } OrderStatus::Open(resolved_order) => { if self.done_orders.contains_key(&order_hash) { info!("Order already done, skipping: {}", order_hash); diff --git a/src/strategies/types.rs b/src/strategies/types.rs index 8c2390b..1f8f21c 100644 --- a/src/strategies/types.rs +++ b/src/strategies/types.rs @@ -45,5 +45,6 @@ pub struct TokenInTokenOut { #[derive(Debug, Clone)] pub enum OrderStatus { Open(ResolvedOrder), + NotFillableYet, Done, } diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 4830a11..cef6f76 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -342,6 +342,8 @@ impl UniswapXUniswapFill { }, ); } + // Noop + _ => {} } } From 546a2f8ffa9fae232a5211c8ed92b7967ae89579 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Mon, 19 Aug 2024 15:05:47 -0400 Subject: [PATCH 23/31] Set gas usage in txn --- src/executors/public_1559_executor.rs | 2 +- src/strategies/priority_strategy.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/executors/public_1559_executor.rs b/src/executors/public_1559_executor.rs index 8a3635a..2e16c7a 100644 --- a/src/executors/public_1559_executor.rs +++ b/src/executors/public_1559_executor.rs @@ -59,7 +59,7 @@ where } 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; - action.execution.tx.set_gas(U256::from(1_000_000)); + action.execution.tx.set_gas(gas_usage_result); info!("Executing tx {:?}", action.execution.tx); self.sender_client.send_transaction(action.execution.tx, None).await?; Ok(()) diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index 8ecc054..eb22c16 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -230,10 +230,10 @@ impl UniswapXPriorityFill { self.update_open_orders(); self.prune_done_orders(); - // self.batch_sender - // .send(self.get_order_batches()) - // .await - // .ok()?; + self.batch_sender + .send(self.get_order_batches()) + .await + .ok()?; None } From 47dc3599cb8fcf22e7770bf575f4f8cdcf8dc044 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 20 Aug 2024 11:50:46 -0400 Subject: [PATCH 24/31] feedback remove unwrap --- src/collectors/uniswapx_route_collector.rs | 2 +- src/executors/protect_executor.rs | 6 +++++- src/executors/public_1559_executor.rs | 10 ++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index 1aa615d..a908bcc 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -267,7 +267,7 @@ pub async fn route_order(params: RouteOrderParams) -> Result { .await?) } -// our routing provider requires that "ETH" be used instead of the zero address +// The Uniswap routing API requires that "ETH" be used instead of the zero address fn resolve_address(token: String) -> String { if token == "0x0000000000000000000000000000000000000000" { return "ETH".to_string(); diff --git a/src/executors/protect_executor.rs b/src/executors/protect_executor.rs index 691a9e0..14a3a6c 100644 --- a/src/executors/protect_executor.rs +++ b/src/executors/protect_executor.rs @@ -60,7 +60,11 @@ where } 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)); + let eip1559_tx = action.tx.as_eip1559_mut(); + if let Some(eip1559_tx) = eip1559_tx { + eip1559_tx.max_priority_fee_per_gas = Some(U256::zero()); + } + 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 index 2e16c7a..debfbbb 100644 --- a/src/executors/public_1559_executor.rs +++ b/src/executors/public_1559_executor.rs @@ -57,9 +57,15 @@ where } 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; + + let eip1559_tx = action.execution.tx.as_eip1559_mut(); + if let Some(eip1559_tx) = eip1559_tx { + eip1559_tx.max_fee_per_gas = Some(base_fee); + eip1559_tx.max_priority_fee_per_gas = bid_priority_fee; + } + action.execution.tx.set_gas(gas_usage_result); + info!("Executing tx {:?}", action.execution.tx); self.sender_client.send_transaction(action.execution.tx, None).await?; Ok(()) From 4090d56b678772527c29b0f350332f2af611f0db Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 20 Aug 2024 12:01:45 -0400 Subject: [PATCH 25/31] Fix orderData enum --- crates/uniswapx-rs/src/order.rs | 1 + src/collectors/uniswapx_route_collector.rs | 36 ++---------------- src/strategies/priority_strategy.rs | 42 ++++++++++++--------- src/strategies/uniswapx_strategy.rs | 43 +++++++++++++--------- 4 files changed, 54 insertions(+), 68 deletions(-) diff --git a/crates/uniswapx-rs/src/order.rs b/crates/uniswapx-rs/src/order.rs index 5d2e800..de248dc 100644 --- a/crates/uniswapx-rs/src/order.rs +++ b/crates/uniswapx-rs/src/order.rs @@ -85,6 +85,7 @@ sol! { } +#[derive(Debug, Clone)] pub enum Order { V2DutchOrder(V2DutchOrder), PriorityOrder(PriorityOrder), diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index a908bcc..23b76b0 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -4,7 +4,7 @@ use reqwest::header::ORIGIN; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::info; -use uniswapx_rs::order::{PriorityOrder, ResolvedOrder, V2DutchOrder}; +use uniswapx_rs::order::{Order, ResolvedOrder}; use artemis_core::types::{Collector, CollectorStream}; use async_trait::async_trait; @@ -17,43 +17,13 @@ const SLIPPAGE_TOLERANCE: &str = "0.5"; const DEADLINE: u64 = 1000; #[derive(Debug, Clone)] -pub struct V2DutchOrderData { - pub order: V2DutchOrder, +pub struct OrderData { + pub order: Order, pub hash: String, pub signature: String, pub resolved: ResolvedOrder, } -#[derive(Debug, Clone)] -pub struct PriorityOrderData { - pub order: PriorityOrder, - pub hash: String, - pub signature: String, - pub resolved: ResolvedOrder, -} - -#[derive(Debug, Clone)] -pub enum OrderData { - V2DutchOrderData(V2DutchOrderData), - PriorityOrderData(PriorityOrderData), -} - -impl OrderData { - pub fn signature(&self) -> String { - match self { - OrderData::V2DutchOrderData(data) => data.signature.clone(), - OrderData::PriorityOrderData(data) => data.signature.clone(), - } - } - - pub fn hash(&self) -> String { - match self { - OrderData::V2DutchOrderData(data) => data.hash.clone(), - OrderData::PriorityOrderData(data) => data.hash.clone(), - } - } -} - #[derive(Clone, Debug)] pub struct OrderBatchData { pub orders: Vec, diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index eb22c16..9919afd 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -5,7 +5,7 @@ use super::{ use crate::{collectors::{ block_collector::NewBlock, uniswapx_order_collector::UniswapXOrder, - uniswapx_route_collector::{OrderBatchData, OrderData, PriorityOrderData, RoutedOrder}, + uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder}, }, strategies::types::SubmitTxToMempoolWithExecutionMetadata}; use alloy_primitives::Uint; use anyhow::Result; @@ -24,7 +24,7 @@ use std::str::FromStr; use std::sync::Arc; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::{error, info}; -use uniswapx_rs::order::{OrderResolution, PriorityOrder}; +use uniswapx_rs::order::{Order, OrderResolution, PriorityOrder}; use super::types::{Action, Event}; @@ -82,7 +82,7 @@ pub struct UniswapXPriorityFill { last_block_number: u64, last_block_timestamp: u64, // map of open order hashes to order data - open_orders: HashMap, + open_orders: HashMap, // map of done order hashes to time at which we can safely prune them done_orders: HashMap, batch_sender: Sender>, @@ -171,7 +171,7 @@ impl UniswapXPriorityFill { .request .orders .iter() - .any(|o| self.done_orders.contains_key(&o.hash())) + .any(|o| self.done_orders.contains_key(&o.hash)) { return None; } @@ -242,11 +242,11 @@ impl UniswapXPriorityFill { fn get_signed_orders(&self, orders: Vec) -> Result> { let mut signed_orders: Vec = Vec::new(); for batch in orders.iter() { - match batch { - OrderData::PriorityOrderData(order) => { + match &batch.order { + Order::PriorityOrder(order) => { signed_orders.push(SignedOrder { - order: Bytes::from(order.order.encode_inner()), - sig: Bytes::from_str(&order.signature)?, + order: Bytes::from(order.encode_inner()), + sig: Bytes::from_str(&batch.signature)?, }); } _ => { @@ -271,7 +271,7 @@ impl UniswapXPriorityFill { .fold(Uint::from(0), |sum, output| sum.wrapping_add(output.amount)); order_batches.push(OrderBatchData { - orders: vec![OrderData::PriorityOrderData(order_data.clone())], + orders: vec![order_data.clone()], amount_in, amount_out_required: amount_out, token_in: order_data.resolved.input.token.clone(), @@ -352,8 +352,8 @@ impl UniswapXPriorityFill { } self.open_orders.insert( order_hash.clone(), - PriorityOrderData { - order, + OrderData { + order: Order::PriorityOrder(order), hash: order_hash, signature, resolved: resolved_order, @@ -378,13 +378,21 @@ impl UniswapXPriorityFill { fn update_open_orders(&mut self) { // TODO: this is nasty, plz cleanup let binding = self.open_orders.clone(); - let order_hashes: Vec<(&String, &PriorityOrderData)> = binding.iter().collect(); + let order_hashes: Vec<(&String, &OrderData)> = binding.iter().collect(); for (order_hash, order_data) in order_hashes { - self.update_order_state( - order_data.order.clone(), - order_data.signature.clone(), - order_hash.clone().to_string(), - ); + match &order_data.order { + Order::PriorityOrder(order) => { + self.update_order_state( + order.clone(), + order_data.signature.clone(), + order_hash.clone().to_string(), + ); + } + _ => { + error!("Invalid order type"); + } + } + } } diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index cef6f76..bce74d9 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -5,7 +5,7 @@ use super::{ use crate::collectors::{ block_collector::NewBlock, uniswapx_order_collector::UniswapXOrder, - uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder, V2DutchOrderData}, + uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder}, }; use alloy_primitives::Uint; use anyhow::Result; @@ -24,7 +24,7 @@ use std::sync::Arc; use std::{collections::HashMap, fmt::Debug}; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::{error, info}; -use uniswapx_rs::order::{OrderResolution, V2DutchOrder}; +use uniswapx_rs::order::{Order, OrderResolution, V2DutchOrder}; use super::types::{Action, Event}; @@ -44,7 +44,7 @@ pub struct UniswapXUniswapFill { last_block_number: u64, last_block_timestamp: u64, // map of open order hashes to order data - open_orders: HashMap, + open_orders: HashMap, // map of done order hashes to time at which we can safely prune them done_orders: HashMap, batch_sender: Sender>, @@ -128,7 +128,7 @@ impl UniswapXUniswapFill { .request .orders .iter() - .any(|o| self.done_orders.contains_key(&o.hash())) + .any(|o| self.done_orders.contains_key(&o.hash)) { return None; } @@ -195,11 +195,11 @@ impl UniswapXUniswapFill { fn get_signed_orders(&self, orders: Vec) -> Result> { let mut signed_orders: Vec = Vec::new(); for batch in orders.iter() { - match batch { - OrderData::V2DutchOrderData(order) => { + match &batch.order { + Order::V2DutchOrder(order) => { signed_orders.push(SignedOrder { - order: Bytes::from(order.order.encode_inner()), - sig: Bytes::from_str(&order.signature)?, + order: Bytes::from(order.encode_inner()), + sig: Bytes::from_str(&batch.signature)?, }); } _ => { @@ -232,7 +232,7 @@ impl UniswapXUniswapFill { order_batches.entry(token_in_token_out.clone()) { e.insert(OrderBatchData { - orders: vec![OrderData::V2DutchOrderData(order_data.clone())], + orders: vec![order_data.clone()], amount_in, amount_out_required: amount_out, token_in: order_data.resolved.input.token.clone(), @@ -242,7 +242,7 @@ impl UniswapXUniswapFill { let order_batch_data = order_batches.get_mut(&token_in_token_out).unwrap(); order_batch_data .orders - .push(OrderData::V2DutchOrderData(order_data.clone())); + .push(order_data.clone()); order_batch_data.amount_in = order_batch_data.amount_in.wrapping_add(amount_in); order_batch_data.amount_out_required = order_batch_data .amount_out_required @@ -291,13 +291,20 @@ impl UniswapXUniswapFill { fn update_open_orders(&mut self) { // TODO: this is nasty, plz cleanup let binding = self.open_orders.clone(); - let order_hashes: Vec<(&String, &V2DutchOrderData)> = binding.iter().collect(); + let order_hashes: Vec<(&String, &OrderData)> = binding.iter().collect(); for (order_hash, order_data) in order_hashes { - self.update_order_state( - order_data.order.clone(), - order_data.signature.clone(), - order_hash.clone().to_string(), - ); + match &order_data.order { + Order::V2DutchOrder(order) => { + self.update_order_state( + order.clone(), + order_data.signature.clone(), + order_hash.clone().to_string(), + ); + }, + _ => { + error!("Invalid order type"); + } + } } } @@ -334,8 +341,8 @@ impl UniswapXUniswapFill { } self.open_orders.insert( order_hash.clone(), - V2DutchOrderData { - order, + OrderData { + order: Order::V2DutchOrder(order), hash: order_hash, signature, resolved: resolved_order, From 9b2101c28adc68ffce632e0df9186b22f1090700 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 20 Aug 2024 12:23:12 -0400 Subject: [PATCH 26/31] dedupe collectors and strategies and take in order type --- src/collectors/uniswapx_order_collector.rs | 25 ++++++-- src/main.rs | 70 ++++++++++------------ 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/collectors/uniswapx_order_collector.rs b/src/collectors/uniswapx_order_collector.rs index 7e1c0ae..f182b01 100644 --- a/src/collectors/uniswapx_order_collector.rs +++ b/src/collectors/uniswapx_order_collector.rs @@ -6,11 +6,26 @@ use reqwest::Client; use serde::Deserialize; use tokio::time::Duration; use tokio_stream::wrappers::IntervalStream; +use std::fmt; static UNISWAPX_API_URL: &str = "https://api.uniswap.org/v2"; static POLL_INTERVAL_SECS: u64 = 1; -#[derive(Debug, PartialEq, Eq)] + +#[derive(Debug)] +pub enum OrderTypeError { + InvalidOrderType, +} + +impl fmt::Display for OrderTypeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Invalid order type") + } +} + +impl std::error::Error for OrderTypeError {} + +#[derive(Debug, PartialEq, Eq, Clone)] pub enum OrderType { Dutch, Priority, @@ -24,11 +39,11 @@ impl OrderType { } } - pub fn from_str(s: &str) -> Option { + pub fn from_str(s: &str) -> Result { match s { - "Dutch_V2" => Some(OrderType::Dutch), - "Priority" => Some(OrderType::Priority), - _ => None, + "Dutch_V2" => Ok(OrderType::Dutch), + "Priority" => Ok(OrderType::Priority), + _ => Err(OrderTypeError::InvalidOrderType), } } } diff --git a/src/main.rs b/src/main.rs index 6fee032..cba780c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,6 +51,10 @@ pub struct Args { #[arg(long)] pub executor_address: String, + /// Order type to use. + #[arg(long)] + pub order_type: String, + /// chain id #[arg(long)] pub chain_id: u64, @@ -75,6 +79,10 @@ async fn main() -> Result<()> { let provider = Provider::new(ws); let chain_id = args.chain_id; + let order_type = OrderType::from_str(&args.order_type).unwrap_or_else(|err| { + panic!("Error parsing order type: {}", err); + }); + let mevblocker_provider = Provider::::try_from(MEV_BLOCKER).expect("could not instantiate HTTP Provider"); @@ -101,19 +109,12 @@ async fn main() -> Result<()> { engine.add_collector(Box::new(block_collector)); let (batch_sender, batch_receiver) = channel(512); - let (priority_batch_sender, priority_batch_receiver) = channel(512); let (route_sender, route_receiver) = channel(512); - let (priority_route_sender, priority_route_receiver) = channel(512); - - let uniswapx_collector = Box::new(UniswapXOrderCollector::new(chain_id, OrderType::Dutch)); - let uniswapx_collector = - CollectorMap::new(uniswapx_collector, |e| Event::UniswapXOrder(Box::new(e))); - engine.add_collector(Box::new(uniswapx_collector)); - let priority_collector = Box::new(UniswapXOrderCollector::new(chain_id, OrderType::Priority)); - let priority_collector = - CollectorMap::new(priority_collector, |e| Event::PriorityOrder(Box::new(e))); - engine.add_collector(Box::new(priority_collector)); + let uniswapx_order_collector = Box::new(UniswapXOrderCollector::new(chain_id, order_type.clone())); + let uniswapx_order_collector = + CollectorMap::new(uniswapx_order_collector, |e| Event::UniswapXOrder(Box::new(e))); + engine.add_collector(Box::new(uniswapx_order_collector)); let uniswapx_route_collector = Box::new(UniswapXRouteCollector::new(chain_id, batch_receiver, route_sender, args.executor_address.clone())); @@ -122,37 +123,32 @@ async fn main() -> Result<()> { }); engine.add_collector(Box::new(uniswapx_route_collector)); - let priority_route_collector = Box::new(UniswapXRouteCollector::new( - chain_id, - priority_batch_receiver, - priority_route_sender, - args.executor_address.clone(), - )); - let priority_route_collector = CollectorMap::new(priority_route_collector, |e| { - Event::UniswapXRoute(Box::new(e)) - }); - engine.add_collector(Box::new(priority_route_collector)); - let config = Config { bid_percentage: args.bid_percentage, executor_address: args.executor_address, }; - let uniswapx_strategy = UniswapXUniswapFill::new( - Arc::new(provider.clone()), - config.clone(), - batch_sender, - route_receiver, - ); - engine.add_strategy(Box::new(uniswapx_strategy)); - - let priority_strategy = UniswapXPriorityFill::new( - Arc::new(provider.clone()), - config.clone(), - priority_batch_sender, - priority_route_receiver, - ); - engine.add_strategy(Box::new(priority_strategy)); + match order_type { + OrderType::Dutch => { + let uniswapx_strategy = UniswapXUniswapFill::new( + Arc::new(provider.clone()), + config.clone(), + batch_sender, + route_receiver, + ); + engine.add_strategy(Box::new(uniswapx_strategy)); + } + OrderType::Priority => { + let priority_strategy = UniswapXPriorityFill::new( + Arc::new(provider.clone()), + config.clone(), + batch_sender, + route_receiver, + ); + + engine.add_strategy(Box::new(priority_strategy)); + } + } let protect_executor = Box::new(ProtectExecutor::new( provider.clone(), From e0610b09a6ef2c0a035191da84aa2f319f95f464 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 20 Aug 2024 14:05:01 -0400 Subject: [PATCH 27/31] fix event link --- src/strategies/priority_strategy.rs | 3 +-- src/strategies/types.rs | 1 - src/strategies/uniswapx_strategy.rs | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index 9919afd..734cd93 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -124,10 +124,9 @@ impl Strategy for UniswapXPriorityFill Option { match event { - Event::PriorityOrder(order) => self.process_order_event(*order).await, + Event::UniswapXOrder(order) => self.process_order_event(*order).await, Event::NewBlock(block) => self.process_new_block_event(block).await, Event::UniswapXRoute(route) => self.process_new_route(*route).await, - _ => None, } } } diff --git a/src/strategies/types.rs b/src/strategies/types.rs index 1f8f21c..1eb9292 100644 --- a/src/strategies/types.rs +++ b/src/strategies/types.rs @@ -12,7 +12,6 @@ use super::priority_strategy::ExecutionMetadata; pub enum Event { NewBlock(NewBlock), UniswapXOrder(Box), - PriorityOrder(Box), UniswapXRoute(Box), } diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index bce74d9..1e8dbc2 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -89,7 +89,6 @@ impl Strategy for UniswapXUniswapFill Event::UniswapXOrder(order) => self.process_order_event(*order).await, Event::NewBlock(block) => self.process_new_block_event(block).await, Event::UniswapXRoute(route) => self.process_new_route(*route).await, - _ => None, } } } From 1757b1f19d4daf14e18f4f75350fac7a756df2ed Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Tue, 20 Aug 2024 16:18:33 -0400 Subject: [PATCH 28/31] sub 2 --- crates/uniswapx-rs/src/order.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uniswapx-rs/src/order.rs b/crates/uniswapx-rs/src/order.rs index de248dc..eca04e9 100644 --- a/crates/uniswapx-rs/src/order.rs +++ b/crates/uniswapx-rs/src/order.rs @@ -218,7 +218,7 @@ impl PriorityOrder { .map(|output| output.scale(priority_fee)) .collect(); - if Uint::from(block_number).lt(&self.cosignerData.auctionTargetBlock.saturating_sub(Uint::from(1))) { + if Uint::from(block_number).lt(&self.cosignerData.auctionTargetBlock.saturating_sub(Uint::from(2))) { return OrderResolution::NotFillableYet; }; From 14df76e3ca96f2092fe0956aa41e9cc3260ecb40 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Thu, 22 Aug 2024 12:10:39 -0400 Subject: [PATCH 29/31] Add contexts --- src/collectors/uniswapx_route_collector.rs | 28 ++++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index 23b76b0..6321434 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -1,5 +1,5 @@ use alloy_primitives::Uint; -use anyhow::Result; +use anyhow::{Context, Result}; use reqwest::header::ORIGIN; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::{Receiver, Sender}; @@ -33,7 +33,7 @@ pub struct OrderBatchData { pub token_out: String, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] #[allow(dead_code)] enum TradeType { #[serde(rename = "exactIn")] @@ -42,7 +42,7 @@ enum TradeType { ExactOut, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] #[serde(rename_all = "camelCase")] struct RoutingApiQuery { token_in_address: String, @@ -194,11 +194,16 @@ impl Collector for UniswapXRouteCollector { let routes: Vec<_> = tasks.collect().await; for (batch, route_result) in routes { - if let Ok(route) = route_result { - yield RoutedOrder { - request: batch.clone(), - route: route, - }; + match route_result { + Ok(route) => { + yield RoutedOrder { + request: batch.clone(), + route: route, + }; + } + Err(e) => { + info!("Failed to route order: {}", e); + } } } } @@ -232,9 +237,12 @@ pub async fn route_order(params: RouteOrderParams) -> Result { .header(ORIGIN, "https://app.uniswap.org") .header("x-request-source", "uniswap-web") .send() - .await? + .await + .context("Quote request failed with {}")? .json::() - .await?) + .await + .context("Failed to parse response: {}")? + ) } // The Uniswap routing API requires that "ETH" be used instead of the zero address From 772f3a19ec3c40830a6f162d47308e706a0a2711 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Fri, 23 Aug 2024 11:14:21 -0400 Subject: [PATCH 30/31] feedback --- crates/uniswapx-rs/src/order.rs | 7 ++- src/collectors/uniswapx_order_collector.rs | 35 ++++++----- src/collectors/uniswapx_route_collector.rs | 7 +-- src/executors/mod.rs | 2 +- src/executors/protect_executor.rs | 7 +-- src/executors/public_1559_executor.rs | 14 +++-- src/main.rs | 37 ++++++------ src/strategies/priority_strategy.rs | 68 +++++++++++++--------- src/strategies/shared.rs | 7 ++- src/strategies/types.rs | 2 +- src/strategies/uniswapx_strategy.rs | 15 +++-- 11 files changed, 116 insertions(+), 85 deletions(-) diff --git a/crates/uniswapx-rs/src/order.rs b/crates/uniswapx-rs/src/order.rs index eca04e9..9ec702e 100644 --- a/crates/uniswapx-rs/src/order.rs +++ b/crates/uniswapx-rs/src/order.rs @@ -84,6 +84,7 @@ sol! { } } +pub const MPS: u64 = 1e7 as u64; #[derive(Debug, Clone)] pub enum Order { @@ -153,7 +154,7 @@ impl V2DutchOrder { // resolve over the decay curve // TODO: apply cosigner logic - let input: ResolvedInput = ResolvedInput { + let input = ResolvedInput { token: self.baseInput.token.to_string(), amount: resolve_decay( timestamp, @@ -228,7 +229,7 @@ impl PriorityOrder { impl PriorityInput { pub fn scale(&self, priority_fee: Uint<256, 4>) -> ResolvedInput { - let amount = self.amount.wrapping_mul(Uint::from(1e7).wrapping_add(priority_fee.wrapping_mul(self.mpsPerPriorityFeeWei))).wrapping_div(Uint::from(1e7)); + let amount = self.amount.wrapping_mul(Uint::from(MPS).wrapping_add(priority_fee.wrapping_mul(self.mpsPerPriorityFeeWei))).wrapping_div(Uint::from(MPS)); ResolvedInput { token: self.token.to_string(), amount, @@ -238,7 +239,7 @@ impl PriorityInput { impl PriorityOutput { pub fn scale(&self, priority_fee: Uint<256, 4>) -> ResolvedOutput { - let amount = self.amount.wrapping_mul(Uint::from(1e7).saturating_sub(priority_fee.wrapping_mul(self.mpsPerPriorityFeeWei))).wrapping_div(Uint::from(1e7)); + let amount = self.amount.wrapping_mul(Uint::from(MPS).saturating_sub(priority_fee.wrapping_mul(self.mpsPerPriorityFeeWei))).wrapping_div(Uint::from(MPS)); ResolvedOutput { token: self.token.to_string(), amount, diff --git a/src/collectors/uniswapx_order_collector.rs b/src/collectors/uniswapx_order_collector.rs index f182b01..6828b3b 100644 --- a/src/collectors/uniswapx_order_collector.rs +++ b/src/collectors/uniswapx_order_collector.rs @@ -4,14 +4,15 @@ use async_trait::async_trait; use futures::{stream, StreamExt}; use reqwest::Client; use serde::Deserialize; +use std::fmt; +use std::str::FromStr; +use std::string::ToString; use tokio::time::Duration; use tokio_stream::wrappers::IntervalStream; -use std::fmt; static UNISWAPX_API_URL: &str = "https://api.uniswap.org/v2"; static POLL_INTERVAL_SECS: u64 = 1; - #[derive(Debug)] pub enum OrderTypeError { InvalidOrderType, @@ -27,30 +28,34 @@ impl std::error::Error for OrderTypeError {} #[derive(Debug, PartialEq, Eq, Clone)] pub enum OrderType { - Dutch, + DutchV2, Priority, } -impl OrderType { - pub fn as_str(&self) -> &'static str { - match self { - OrderType::Dutch => "Dutch_V2", - OrderType::Priority => "Priority", - } - } +impl FromStr for OrderType { + type Err = OrderTypeError; - pub fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { match s { - "Dutch_V2" => Ok(OrderType::Dutch), + "Dutch_V2" => Ok(OrderType::DutchV2), "Priority" => Ok(OrderType::Priority), _ => Err(OrderTypeError::InvalidOrderType), } } } +impl ToString for OrderType { + fn to_string(&self) -> String { + match self { + OrderType::DutchV2 => "Dutch_V2".to_string(), + OrderType::Priority => "Priority".to_string(), + } + } +} + impl Default for OrderType { fn default() -> Self { - OrderType::Dutch + OrderType::DutchV2 } } @@ -106,7 +111,7 @@ impl Collector for UniswapXOrderCollector { "{}/orders?orderStatus=open&chainId={}&orderType={}", self.base_url, self.chain_id, - self.order_type.as_str() + self.order_type.to_string() ); // stream that polls the UniswapX API every 5 seconds @@ -164,7 +169,7 @@ mod tests { client: reqwest::Client::new(), base_url: url.clone(), chain_id: 1, - order_type: super::OrderType::Dutch, + order_type: super::OrderType::DutchV2, }; (res, server, mock) diff --git a/src/collectors/uniswapx_route_collector.rs b/src/collectors/uniswapx_route_collector.rs index 6321434..2247fa3 100644 --- a/src/collectors/uniswapx_route_collector.rs +++ b/src/collectors/uniswapx_route_collector.rs @@ -152,14 +152,14 @@ impl UniswapXRouteCollector { chain_id: u64, route_request_receiver: Receiver>, route_sender: Sender, - executor_address: String + executor_address: String, ) -> Self { Self { client: Client::new(), chain_id, route_request_receiver: Mutex::new(route_request_receiver), route_sender, - executor_address + executor_address, } } } @@ -241,8 +241,7 @@ pub async fn route_order(params: RouteOrderParams) -> Result { .context("Quote request failed with {}")? .json::() .await - .context("Failed to parse response: {}")? - ) + .context("Failed to parse response: {}")?) } // The Uniswap routing API requires that "ETH" be used instead of the zero address diff --git a/src/executors/mod.rs b/src/executors/mod.rs index c17998e..17706c8 100644 --- a/src/executors/mod.rs +++ b/src/executors/mod.rs @@ -1,2 +1,2 @@ pub mod protect_executor; -pub mod public_1559_executor; \ No newline at end of file +pub mod public_1559_executor; diff --git a/src/executors/protect_executor.rs b/src/executors/protect_executor.rs index 14a3a6c..172c7a2 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, types::U256}; +use ethers::providers::Middleware; /// An executor that sends transactions to the mempool. pub struct ProtectExecutor { @@ -59,11 +59,6 @@ where .context("Error getting gas price: {}")?; } action.tx.set_gas_price(bid_gas_price); - // set max_priority_fee_per_gas to be 0 - let eip1559_tx = action.tx.as_eip1559_mut(); - if let Some(eip1559_tx) = eip1559_tx { - eip1559_tx.max_priority_fee_per_gas = Some(U256::zero()); - } info!("Executing tx {:?}", action.tx); self.sender_client.send_transaction(action.tx, None).await?; diff --git a/src/executors/public_1559_executor.rs b/src/executors/public_1559_executor.rs index debfbbb..ac39e67 100644 --- a/src/executors/public_1559_executor.rs +++ b/src/executors/public_1559_executor.rs @@ -49,11 +49,13 @@ where .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.metadata.calculate_priority_fee(gas_bid_info.bid_percentage) + bid_priority_fee = action + .metadata + .calculate_priority_fee(gas_bid_info.bid_percentage) } else { bid_priority_fee = Some(U256::from(50)); } @@ -62,12 +64,16 @@ where if let Some(eip1559_tx) = eip1559_tx { eip1559_tx.max_fee_per_gas = Some(base_fee); eip1559_tx.max_priority_fee_per_gas = bid_priority_fee; + } else { + return Err(anyhow::anyhow!("Transaction is not EIP1559")); } action.execution.tx.set_gas(gas_usage_result); - + info!("Executing tx {:?}", action.execution.tx); - self.sender_client.send_transaction(action.execution.tx, None).await?; + self.sender_client + .send_transaction(action.execution.tx, None) + .await?; Ok(()) } } diff --git a/src/main.rs b/src/main.rs index cba780c..ccf06b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,7 @@ use artemis_core::engine::Engine; use artemis_core::types::{CollectorMap, ExecutorMap}; use collectors::uniswapx_order_collector::OrderType; use collectors::{ - block_collector::BlockCollector, - uniswapx_order_collector::UniswapXOrderCollector, + block_collector::BlockCollector, uniswapx_order_collector::UniswapXOrderCollector, uniswapx_route_collector::UniswapXRouteCollector, }; use ethers::{ @@ -53,9 +52,9 @@ pub struct Args { /// Order type to use. #[arg(long)] - pub order_type: String, + pub order_type: OrderType, - /// chain id + /// chain id #[arg(long)] pub chain_id: u64, } @@ -79,10 +78,6 @@ async fn main() -> Result<()> { let provider = Provider::new(ws); let chain_id = args.chain_id; - let order_type = OrderType::from_str(&args.order_type).unwrap_or_else(|err| { - panic!("Error parsing order type: {}", err); - }); - let mevblocker_provider = Provider::::try_from(MEV_BLOCKER).expect("could not instantiate HTTP Provider"); @@ -111,13 +106,21 @@ async fn main() -> Result<()> { let (batch_sender, batch_receiver) = channel(512); let (route_sender, route_receiver) = channel(512); - let uniswapx_order_collector = Box::new(UniswapXOrderCollector::new(chain_id, order_type.clone())); - let uniswapx_order_collector = - CollectorMap::new(uniswapx_order_collector, |e| Event::UniswapXOrder(Box::new(e))); + let uniswapx_order_collector = Box::new(UniswapXOrderCollector::new( + chain_id, + args.order_type.clone(), + )); + let uniswapx_order_collector = CollectorMap::new(uniswapx_order_collector, |e| { + Event::UniswapXOrder(Box::new(e)) + }); engine.add_collector(Box::new(uniswapx_order_collector)); - let uniswapx_route_collector = - Box::new(UniswapXRouteCollector::new(chain_id, batch_receiver, route_sender, args.executor_address.clone())); + let uniswapx_route_collector = Box::new(UniswapXRouteCollector::new( + chain_id, + batch_receiver, + route_sender, + args.executor_address.clone(), + )); let uniswapx_route_collector = CollectorMap::new(uniswapx_route_collector, |e| { Event::UniswapXRoute(Box::new(e)) }); @@ -128,8 +131,8 @@ async fn main() -> Result<()> { executor_address: args.executor_address, }; - match order_type { - OrderType::Dutch => { + match &args.order_type { + OrderType::DutchV2 => { let uniswapx_strategy = UniswapXUniswapFill::new( Arc::new(provider.clone()), config.clone(), @@ -142,10 +145,10 @@ async fn main() -> Result<()> { let priority_strategy = UniswapXPriorityFill::new( Arc::new(provider.clone()), config.clone(), - batch_sender, + batch_sender, route_receiver, ); - + engine.add_strategy(Box::new(priority_strategy)); } } diff --git a/src/strategies/priority_strategy.rs b/src/strategies/priority_strategy.rs index 734cd93..97065b6 100644 --- a/src/strategies/priority_strategy.rs +++ b/src/strategies/priority_strategy.rs @@ -2,11 +2,14 @@ use super::{ shared::UniswapXStrategy, types::{Config, OrderStatus}, }; -use crate::{collectors::{ - block_collector::NewBlock, - uniswapx_order_collector::UniswapXOrder, - uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder}, -}, strategies::types::SubmitTxToMempoolWithExecutionMetadata}; +use crate::{ + collectors::{ + block_collector::NewBlock, + uniswapx_order_collector::UniswapXOrder, + uniswapx_route_collector::{OrderBatchData, OrderData, RoutedOrder}, + }, + strategies::types::SubmitTxToMempoolWithExecutionMetadata, +}; use alloy_primitives::Uint; use anyhow::Result; use artemis_core::executors::mempool_executor::{GasBidInfo, SubmitTxToMempool}; @@ -24,7 +27,7 @@ use std::str::FromStr; use std::sync::Arc; use tokio::sync::mpsc::{Receiver, Sender}; use tracing::{error, info}; -use uniswapx_rs::order::{Order, OrderResolution, PriorityOrder}; +use uniswapx_rs::order::{Order, OrderResolution, PriorityOrder, MPS}; use super::types::{Action, Event}; @@ -51,16 +54,14 @@ impl ExecutionMetadata { } 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) + .saturating_mul(U256::from(MPS)) .checked_div(self.amount_out_required)?; info!("mps_of_improvement: {}", mps_of_improvement); let priority_fee = mps_of_improvement @@ -191,20 +192,27 @@ impl UniswapXPriorityFill { ); let signed_orders = self.get_signed_orders(orders.clone()).ok()?; - return Some(Action::SubmitPublicTx(SubmitTxToMempoolWithExecutionMetadata { - 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, - // this field is not used for priority orders - total_profit: U256::from(0), - }), + return Some(Action::SubmitPublicTx( + SubmitTxToMempoolWithExecutionMetadata { + 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, + // this field is not used for priority orders + total_profit: U256::from(0), + }), + }, + metadata: profit, }, - metadata: profit, - })); + )); } None @@ -309,7 +317,10 @@ impl UniswapXPriorityFill { /// - we will always bid the base fee /// - since we have to provide 1 MP (1/1000th of a bp) for every wei of priority fee /// - we return the data needed to calculate the maximum MPS of improvement we can offer from our quote and the order specs - fn get_execution_metadata(&self, RoutedOrder { request, route }: &RoutedOrder) -> Option { + fn get_execution_metadata( + &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()?; @@ -322,11 +333,15 @@ impl UniswapXPriorityFill { quote, amount_out_required, } - }) + }); } fn update_order_state(&mut self, order: PriorityOrder, signature: String, order_hash: String) { - let resolved = order.resolve( self.last_block_number, self.last_block_timestamp + BLOCK_TIME, Uint::from(0)); + let resolved = order.resolve( + self.last_block_number, + self.last_block_timestamp + BLOCK_TIME, + Uint::from(0), + ); let order_status: OrderStatus = match resolved { OrderResolution::Expired => OrderStatus::Done, OrderResolution::Invalid => OrderStatus::Done, @@ -391,7 +406,6 @@ impl UniswapXPriorityFill { error!("Invalid order type"); } } - } } diff --git a/src/strategies/shared.rs b/src/strategies/shared.rs index 3d743ff..637945f 100644 --- a/src/strategies/shared.rs +++ b/src/strategies/shared.rs @@ -46,7 +46,12 @@ pub trait UniswapXStrategy { .await?; let reactor_approval = self - .get_tokens_to_approve(client.clone(), token_out, &executor_address, REACTOR_ADDRESS) + .get_tokens_to_approve( + client.clone(), + token_out, + &executor_address, + REACTOR_ADDRESS, + ) .await?; // Strip off function selector diff --git a/src/strategies/types.rs b/src/strategies/types.rs index 1eb9292..3469cd9 100644 --- a/src/strategies/types.rs +++ b/src/strategies/types.rs @@ -32,7 +32,7 @@ pub enum Action { #[derive(Debug, Clone)] pub struct Config { pub bid_percentage: u64, - pub executor_address: String + pub executor_address: String, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] diff --git a/src/strategies/uniswapx_strategy.rs b/src/strategies/uniswapx_strategy.rs index 1e8dbc2..7ee53c0 100644 --- a/src/strategies/uniswapx_strategy.rs +++ b/src/strategies/uniswapx_strategy.rs @@ -150,7 +150,12 @@ impl UniswapXUniswapFill { let signed_orders = self.get_signed_orders(orders.clone()).ok()?; return Some(Action::SubmitTx(SubmitTxToMempool { tx: self - .build_fill(self.client.clone(), &self.executor_address, signed_orders, event) + .build_fill( + self.client.clone(), + &self.executor_address, + signed_orders, + event, + ) .await .ok()?, gas_bid_info: Some(GasBidInfo { @@ -239,9 +244,7 @@ impl UniswapXUniswapFill { }); } else { let order_batch_data = order_batches.get_mut(&token_in_token_out).unwrap(); - order_batch_data - .orders - .push(order_data.clone()); + order_batch_data.orders.push(order_data.clone()); order_batch_data.amount_in = order_batch_data.amount_in.wrapping_add(amount_in); order_batch_data.amount_out_required = order_batch_data .amount_out_required @@ -299,7 +302,7 @@ impl UniswapXUniswapFill { order_data.signature.clone(), order_hash.clone().to_string(), ); - }, + } _ => { error!("Invalid order type"); } @@ -323,7 +326,7 @@ impl UniswapXUniswapFill { OrderResolution::Expired => OrderStatus::Done, OrderResolution::Invalid => OrderStatus::Done, OrderResolution::Resolved(resolved_order) => OrderStatus::Open(resolved_order), - _ => OrderStatus::Done + _ => OrderStatus::Done, }; match order_status { From 1656de8f8ca05cb2c0889942c7328019978f67c8 Mon Sep 17 00:00:00 2001 From: Eric Zhong Date: Fri, 23 Aug 2024 11:16:18 -0400 Subject: [PATCH 31/31] remove shared impl resolve --- crates/uniswapx-rs/src/order.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/uniswapx-rs/src/order.rs b/crates/uniswapx-rs/src/order.rs index 9ec702e..e611479 100644 --- a/crates/uniswapx-rs/src/order.rs +++ b/crates/uniswapx-rs/src/order.rs @@ -93,13 +93,6 @@ pub enum Order { } impl Order { - pub fn resolve(&self, block_number: u64, timestamp: u64, priority_fee: Uint<256, 4>) -> OrderResolution { - match self { - Order::V2DutchOrder(order) => order.resolve(timestamp), - Order::PriorityOrder(order) => order.resolve(block_number, timestamp, priority_fee), - } - } - pub fn encode(&self) -> Vec { match self { Order::V2DutchOrder(order) => order.encode_inner(),