From cc93ca10ecdb1dd6b0e49fdb4147a284c4774ef5 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:48:09 +0200 Subject: [PATCH 01/15] driver: parallel unsupported order detection Make unsupported order detection read-only and execute it in parallel with sorting and data fetching. Apply filtering after update_orders to preserve existing ordering. --- crates/driver/src/domain/competition/mod.rs | 59 +++++----- .../domain/competition/risk_detector/mod.rs | 104 +++++++++--------- crates/driver/src/domain/quote.rs | 10 +- 3 files changed, 92 insertions(+), 81 deletions(-) diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index 864dbf39dc..8ded3a2d93 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -33,7 +33,7 @@ use { time::Instant, }, tokio::{sync::mpsc, task}, - tracing::{Instrument, instrument}, + tracing::{Instrument}, }; pub mod auction; @@ -333,17 +333,24 @@ impl Competition { let cow_amm_orders = tasks.cow_amm_orders.await; auction.orders.extend(cow_amm_orders.iter().cloned()); + // Clone orders so unsupported detection can run in parallel while + // preserving the existing auction ownership model. + let orders_for_filtering = auction.orders.clone(); + let unsupported_uids_future = self.risk_detector.unsupported_order_uids(&orders_for_filtering); + let sort_orders_future = Self::run_blocking_with_timer("sort_orders", move || { // Use spawn_blocking() because a lot of CPU bound computations are happening // and we don't want to block the runtime for too long. Self::sort_orders(auction, solver_address, order_sorting_strategies) }); - // We can sort the orders and fetch auction data in parallel - let (auction, balances, app_data) = - tokio::join!(sort_orders_future, tasks.balances, tasks.app_data); + // We can sort the orders, fetch auction data, and detect unsupported + // orders in parallel. + let (auction, balances, app_data, unsupported_uids) = tokio::join!( + sort_orders_future,tasks.balances,tasks.app_data,unsupported_uids_future + ); - let auction = Self::run_blocking_with_timer("update_orders", move || { + let mut auction = Self::run_blocking_with_timer("update_orders", move || { // Same as before with sort_orders, we use spawn_blocking() because a lot of CPU // bound computations are happening and we want to avoid blocking // the runtime. @@ -351,16 +358,28 @@ impl Competition { }) .await; - // We can run bad token filtering and liquidity fetching in parallel - let (liquidity, auction) = tokio::join!( - async { - match self.solver.liquidity() { - solver::Liquidity::Fetch => tasks.liquidity.await, - solver::Liquidity::Skip => Arc::new(Vec::new()), - } - }, - self.without_unsupported_orders(auction) - ); + let filter_auction = async move { + if !self.solver.config().flashloans_enabled { + auction.orders.retain(|o| o.app_data.flashloan().is_none()); + } + + if !unsupported_uids.is_empty() { + auction + .orders + .retain(|order| !unsupported_uids.contains(&order.uid)); + } + + auction + }; + + let fetch_liquidity = async { + match self.solver.liquidity() { + solver::Liquidity::Fetch => tasks.liquidity.await, + solver::Liquidity::Skip => Arc::new(Vec::new()), + } + }; + + let (auction, liquidity) = tokio::join!(filter_auction, fetch_liquidity); let elapsed = start.elapsed(); metrics::get() @@ -951,16 +970,6 @@ impl Competition { } Ok(()) } - - #[instrument(skip_all)] - async fn without_unsupported_orders(&self, mut auction: Auction) -> Auction { - if !self.solver.config().flashloans_enabled { - auction.orders.retain(|o| o.app_data.flashloan().is_none()); - } - self.risk_detector - .filter_unsupported_orders_in_auction(auction) - .await - } } const MAX_SOLUTIONS_TO_MERGE: usize = 10; diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index 357f0b6e04..953870e536 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -16,11 +16,12 @@ //! we were not able to predict issues with orders and pre-emptively //! filter them out of the auction. use { - crate::domain::competition::{Auction, order::Uid}, + crate::domain::competition::{order::Uid}, eth_domain_types as eth, futures::{StreamExt, stream::FuturesUnordered}, - std::{collections::HashMap, fmt, time::Instant}, + std::{collections::{HashMap, HashSet}, fmt, time::Instant}, }; +use crate::domain::competition::Order; pub mod bad_orders; pub mod bad_tokens; @@ -80,72 +81,73 @@ impl Detector { self } - /// Removes all unsupported orders from the auction. - pub async fn filter_unsupported_orders_in_auction(&self, mut auction: Auction) -> Auction { + pub async fn unsupported_order_uids(&self, orders: &[Order]) -> HashSet { let now = Instant::now(); - // reuse the original allocation - let supported_orders = std::mem::take(&mut auction.orders); let mut token_quality_checks = FuturesUnordered::new(); let mut removed_uids = Vec::new(); - let mut supported_orders: Vec<_> = supported_orders - .into_iter() - .filter(|order| { - self.metrics - .as_ref() - .map(|metrics| metrics.get_quality(&order.uid, now)) - .is_none_or(|q| q != Quality::Unsupported) - }) - .filter_map(|order| { - let sell = self.get_token_quality(order.sell.token, now); - let buy = self.get_token_quality(order.buy.token, now); - match (sell, buy) { - // both tokens supported => keep order - (Quality::Supported, Quality::Supported) => Some(order), - // at least 1 token unsupported => drop order - (Quality::Unsupported, _) | (_, Quality::Unsupported) => { - removed_uids.push(order.uid); - None - } - // sell token quality is unknown => keep order if token is supported - (Quality::Unknown, _) => { - let Some(detector) = &self.simulation_detector else { - // we can't determine quality => assume order is good - return Some(order); - }; - let check_tokens_fut = async move { - let quality = detector.determine_sell_token_quality(&order, now).await; - (order, quality) - }; - token_quality_checks.push(check_tokens_fut); - None - } - // buy token quality is unknown => keep order (because we can't - // determine quality and assume it's good) - (_, Quality::Unknown) => Some(order), + orders.iter().for_each(|order| { + if self + .metrics + .as_ref() + .map(|metrics| metrics.get_quality(&order.uid, now)) + .is_some_and(|q| q == Quality::Unsupported) + { + removed_uids.push(order.uid); + return; + } + + let sell = self.get_token_quality(order.sell.token, now); + let buy = self.get_token_quality(order.buy.token, now); + + match (sell, buy) { + // at least 1 token unsupported => drop order + (Quality::Unsupported, _) | (_, Quality::Unsupported) => { + removed_uids.push(order.uid); } - }) - .collect(); - while let Some((order, quality)) = token_quality_checks.next().await { - if quality == Quality::Supported { - supported_orders.push(order); - } else { - removed_uids.push(order.uid); + // sell token quality is unknown => keep order if token is supported + (Quality::Unknown, _) => { + let Some(detector) = &self.simulation_detector else { + // we can't determine quality => assume order is good + return; + }; + + let check_tokens_fut = async move { + let quality = detector.determine_sell_token_quality(order, now).await; + (order.uid, quality) + }; + token_quality_checks.push(check_tokens_fut); + } + + // both tokens supported => keep order + (Quality::Supported, Quality::Supported) => {} + + // buy token quality is unknown => keep order (because we can't + // determine quality and assume it's good) + (_, Quality::Unknown) => {} + } + }); + + while let Some((uid, quality)) = token_quality_checks.next().await { + if quality != Quality::Supported { + removed_uids.push(uid); } } - auction.orders = supported_orders; if !removed_uids.is_empty() { - tracing::debug!(orders = ?removed_uids, "ignored orders with unsupported tokens"); + tracing::debug!( + orders = ?removed_uids, + "ignored orders with unsupported tokens" + ); } if let Some(detector) = &self.simulation_detector { detector.evict_outdated_entries(); } - auction + removed_uids.into_iter().collect() } /// Updates the tokens quality metric for successful operation. diff --git a/crates/driver/src/domain/quote.rs b/crates/driver/src/domain/quote.rs index 4acc4946ba..736de64aec 100644 --- a/crates/driver/src/domain/quote.rs +++ b/crates/driver/src/domain/quote.rs @@ -141,13 +141,13 @@ impl Order { } solver::Liquidity::Skip => Default::default(), }; - - let auction = self + let mut auction = self .fake_auction(eth, tokens, solver.quote_using_limit_orders()) .await?; - let auction = risk_detector - .filter_unsupported_orders_in_auction(auction) - .await; + let unsupported_uids = risk_detector.unsupported_order_uids(&auction.orders).await; + if !unsupported_uids.is_empty() { + auction.orders.retain(|order| !unsupported_uids.contains(&order.uid)); + } if auction.orders.is_empty() { return Err(QuotingFailed::UnsupportedToken.into()); } From 889f437f958b3bae2acd18ee1d00351cf31ffdf3 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Wed, 22 Apr 2026 09:19:40 +0200 Subject: [PATCH 02/15] driver(tests): unsupported_order_uids test cases --- .../domain/competition/risk_detector/mod.rs | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index 953870e536..3eea71e0cb 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -184,3 +184,135 @@ impl fmt::Debug for Detector { .finish() } } + +#[cfg(test)] +mod tests { + use std::time::Duration; + use eth_domain_types::TokenAmount; + use super::*; + use crate::{infra::solver, + util, + domain::competition::{Order, + order::{signature, + BuyTokenBalance, + Kind, + Partial, + SellTokenBalance, + Side, + Signature, + Uid} + } + }; + + // Helper to create a mock order purely for test + fn order( + uid: Uid, + signer: eth::Address, + sell_token: eth::TokenAddress, + buy_token: eth::TokenAddress, + valid_to: u32, + ) -> Order { + Order { + uid, + receiver: Some(signer), + created: util::Timestamp(0), + valid_to: util::Timestamp(valid_to), + buy: eth::Asset { + token: buy_token, + amount: TokenAmount::from(1), + }, + sell: eth::Asset { + token: sell_token, + amount: TokenAmount::from(1), + }, + side: Side::Sell, + kind: Kind::Limit, + app_data: Default::default(), + partial: Partial::No, + pre_interactions: vec![], + post_interactions: vec![], + sell_token_balance: SellTokenBalance::Erc20, + buy_token_balance: BuyTokenBalance::Erc20, + signature: Signature { + scheme: signature::Scheme::PreSign, + data: Default::default(), + signer, + }, + protocol_fees: Default::default(), + quote: Default::default(), + } + } + + // Helper to create a mock UID purely for test + fn uid(n: u8, signer: eth::Address, valid_to: u32) -> Uid { + let order_hash = eth::B256::from([n; 32]); + Uid::from_parts(order_hash, signer, valid_to) + } + + #[tokio::test] + async fn unsupported_order_uids_empty_returns_empty() { + let detector = Detector::new(Default::default()); + let removed = detector.unsupported_order_uids(&[]).await; + assert!(removed.is_empty()); + } + + #[tokio::test] + async fn all_supported_orders_are_kept() { + let signer = eth::Address::from_slice(&[1; 20]); + let sell_token = eth::Address::from_slice(&[2; 20]).into(); + let buy_token = eth::Address::from_slice(&[3; 20]).into(); + + let detector = Detector::new(Default::default()); + + let removed = detector + .unsupported_order_uids(&[order(uid(1, signer, u32::MAX), signer, sell_token, buy_token, u32::MAX)]) + .await; + + assert!(removed.is_empty()); + } + + #[tokio::test] + async fn unsupported_order_uids_returns_only_unsupported_orders() { + fn addr(n: u8) -> eth::Address { + eth::Address::from_slice(&[n; 20]) + } + + let valid_to = u32::MAX; + + let orders = vec![ + order(uid(1, addr(6), valid_to), addr(6), addr(1).into(), addr(2).into(), valid_to), // metrics bad + order(uid(2, addr(7), valid_to), addr(7), addr(3).into(), addr(2).into(), valid_to), // token bad + order(uid(3, addr(8), valid_to), addr(8), addr(1).into(), addr(2).into(), valid_to), // token supported + order(uid(4, addr(9), valid_to), addr(9), addr(4).into(), addr(2).into(), valid_to), // unknown sell + order(uid(5, addr(10), valid_to), addr(10), addr(1).into(), addr(5).into(), valid_to), // unknown buy + ]; + + let metrics_uid = orders[0].uid; + let token_uid = orders[1].uid; + + let metrics_detector = bad_orders::metrics::Detector::new( + 0.5, + 2, + false, + Duration::from_secs(60), + Duration::from_secs(60), + Duration::from_secs(60), + solver::Name("test_solver".into()), + ); + + let mut detector_config = HashMap::new(); + detector_config.insert(addr(3).into(), Quality::Unsupported); + + let mut detector = Detector::new(detector_config); + detector.with_metrics_detector(metrics_detector); + + // Simulate repeated metrics failure for order with metrics_uid + detector.encoding_failed(&[metrics_uid]); + detector.encoding_failed(&[metrics_uid]); + detector.encoding_failed(&[metrics_uid]); + + let removed = detector.unsupported_order_uids(&orders).await; + + assert_eq!(removed, HashSet::from([metrics_uid, token_uid])); + } +} \ No newline at end of file From ae060d26f99de50ffbc7cc0927fe1b47a3f4c06c Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Wed, 22 Apr 2026 09:41:03 +0200 Subject: [PATCH 03/15] lint: run cargo fmt --- crates/driver/src/domain/competition/mod.rs | 11 +- .../domain/competition/risk_detector/mod.rs | 101 +++++++++++++----- crates/driver/src/domain/quote.rs | 4 +- 3 files changed, 84 insertions(+), 32 deletions(-) diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index 8ded3a2d93..cc23cdedcf 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -33,7 +33,7 @@ use { time::Instant, }, tokio::{sync::mpsc, task}, - tracing::{Instrument}, + tracing::Instrument, }; pub mod auction; @@ -336,7 +336,9 @@ impl Competition { // Clone orders so unsupported detection can run in parallel while // preserving the existing auction ownership model. let orders_for_filtering = auction.orders.clone(); - let unsupported_uids_future = self.risk_detector.unsupported_order_uids(&orders_for_filtering); + let unsupported_uids_future = self + .risk_detector + .unsupported_order_uids(&orders_for_filtering); let sort_orders_future = Self::run_blocking_with_timer("sort_orders", move || { // Use spawn_blocking() because a lot of CPU bound computations are happening @@ -347,7 +349,10 @@ impl Competition { // We can sort the orders, fetch auction data, and detect unsupported // orders in parallel. let (auction, balances, app_data, unsupported_uids) = tokio::join!( - sort_orders_future,tasks.balances,tasks.app_data,unsupported_uids_future + sort_orders_future, + tasks.balances, + tasks.app_data, + unsupported_uids_future ); let mut auction = Self::run_blocking_with_timer("update_orders", move || { diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index 3eea71e0cb..b8fb867b8a 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -16,12 +16,15 @@ //! we were not able to predict issues with orders and pre-emptively //! filter them out of the auction. use { - crate::domain::competition::{order::Uid}, + crate::domain::competition::{Order, order::Uid}, eth_domain_types as eth, futures::{StreamExt, stream::FuturesUnordered}, - std::{collections::{HashMap, HashSet}, fmt, time::Instant}, + std::{ + collections::{HashMap, HashSet}, + fmt, + time::Instant, + }, }; -use crate::domain::competition::Order; pub mod bad_orders; pub mod bad_tokens; @@ -138,9 +141,9 @@ impl Detector { if !removed_uids.is_empty() { tracing::debug!( - orders = ?removed_uids, - "ignored orders with unsupported tokens" - ); + orders = ?removed_uids, + "ignored orders with unsupported tokens" + ); } if let Some(detector) = &self.simulation_detector { @@ -187,21 +190,27 @@ impl fmt::Debug for Detector { #[cfg(test)] mod tests { - use std::time::Duration; - use eth_domain_types::TokenAmount; - use super::*; - use crate::{infra::solver, - util, - domain::competition::{Order, - order::{signature, - BuyTokenBalance, - Kind, - Partial, - SellTokenBalance, - Side, - Signature, - Uid} - } + use { + super::*, + crate::{ + domain::competition::{ + Order, + order::{ + BuyTokenBalance, + Kind, + Partial, + SellTokenBalance, + Side, + Signature, + Uid, + signature, + }, + }, + infra::solver, + util, + }, + eth_domain_types::TokenAmount, + std::time::Duration, }; // Helper to create a mock order purely for test @@ -265,7 +274,13 @@ mod tests { let detector = Detector::new(Default::default()); let removed = detector - .unsupported_order_uids(&[order(uid(1, signer, u32::MAX), signer, sell_token, buy_token, u32::MAX)]) + .unsupported_order_uids(&[order( + uid(1, signer, u32::MAX), + signer, + sell_token, + buy_token, + u32::MAX, + )]) .await; assert!(removed.is_empty()); @@ -280,11 +295,41 @@ mod tests { let valid_to = u32::MAX; let orders = vec![ - order(uid(1, addr(6), valid_to), addr(6), addr(1).into(), addr(2).into(), valid_to), // metrics bad - order(uid(2, addr(7), valid_to), addr(7), addr(3).into(), addr(2).into(), valid_to), // token bad - order(uid(3, addr(8), valid_to), addr(8), addr(1).into(), addr(2).into(), valid_to), // token supported - order(uid(4, addr(9), valid_to), addr(9), addr(4).into(), addr(2).into(), valid_to), // unknown sell - order(uid(5, addr(10), valid_to), addr(10), addr(1).into(), addr(5).into(), valid_to), // unknown buy + order( + uid(1, addr(6), valid_to), + addr(6), + addr(1).into(), + addr(2).into(), + valid_to, + ), // metrics bad + order( + uid(2, addr(7), valid_to), + addr(7), + addr(3).into(), + addr(2).into(), + valid_to, + ), // token bad + order( + uid(3, addr(8), valid_to), + addr(8), + addr(1).into(), + addr(2).into(), + valid_to, + ), // token supported + order( + uid(4, addr(9), valid_to), + addr(9), + addr(4).into(), + addr(2).into(), + valid_to, + ), // unknown sell + order( + uid(5, addr(10), valid_to), + addr(10), + addr(1).into(), + addr(5).into(), + valid_to, + ), // unknown buy ]; let metrics_uid = orders[0].uid; @@ -315,4 +360,4 @@ mod tests { assert_eq!(removed, HashSet::from([metrics_uid, token_uid])); } -} \ No newline at end of file +} diff --git a/crates/driver/src/domain/quote.rs b/crates/driver/src/domain/quote.rs index 736de64aec..89a9c5eb69 100644 --- a/crates/driver/src/domain/quote.rs +++ b/crates/driver/src/domain/quote.rs @@ -146,7 +146,9 @@ impl Order { .await?; let unsupported_uids = risk_detector.unsupported_order_uids(&auction.orders).await; if !unsupported_uids.is_empty() { - auction.orders.retain(|order| !unsupported_uids.contains(&order.uid)); + auction + .orders + .retain(|order| !unsupported_uids.contains(&order.uid)); } if auction.orders.is_empty() { return Err(QuotingFailed::UnsupportedToken.into()); From c5cf81d2d02ca9123e89c323d340b9d7ca9db97a Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:58:23 +0200 Subject: [PATCH 04/15] refactor --- crates/driver/src/domain/competition/mod.rs | 55 +++++++------------ .../domain/competition/risk_detector/mod.rs | 24 +++++++- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index cc23cdedcf..26027a3c01 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -333,27 +333,15 @@ impl Competition { let cow_amm_orders = tasks.cow_amm_orders.await; auction.orders.extend(cow_amm_orders.iter().cloned()); - // Clone orders so unsupported detection can run in parallel while - // preserving the existing auction ownership model. - let orders_for_filtering = auction.orders.clone(); - let unsupported_uids_future = self - .risk_detector - .unsupported_order_uids(&orders_for_filtering); - let sort_orders_future = Self::run_blocking_with_timer("sort_orders", move || { // Use spawn_blocking() because a lot of CPU bound computations are happening // and we don't want to block the runtime for too long. Self::sort_orders(auction, solver_address, order_sorting_strategies) }); - // We can sort the orders, fetch auction data, and detect unsupported - // orders in parallel. - let (auction, balances, app_data, unsupported_uids) = tokio::join!( - sort_orders_future, - tasks.balances, - tasks.app_data, - unsupported_uids_future - ); + // We can sort the orders, fetch auction data in parallel. + let (auction, balances, app_data) = + tokio::join!(sort_orders_future, tasks.balances, tasks.app_data); let mut auction = Self::run_blocking_with_timer("update_orders", move || { // Same as before with sort_orders, we use spawn_blocking() because a lot of CPU @@ -363,28 +351,23 @@ impl Competition { }) .await; - let filter_auction = async move { - if !self.solver.config().flashloans_enabled { - auction.orders.retain(|o| o.app_data.flashloan().is_none()); - } - - if !unsupported_uids.is_empty() { - auction - .orders - .retain(|order| !unsupported_uids.contains(&order.uid)); - } - - auction - }; - - let fetch_liquidity = async { - match self.solver.liquidity() { - solver::Liquidity::Fetch => tasks.liquidity.await, - solver::Liquidity::Skip => Arc::new(Vec::new()), + // We can run bad token filtering and liquidity fetching in parallel + let (_, liquidity) = tokio::join!( + async { + self.risk_detector + .filter_unsupported_orders_in_auction( + &mut auction, + self.solver.config().flashloans_enabled, + ) + .await + }, + async { + match self.solver.liquidity() { + solver::Liquidity::Fetch => tasks.liquidity.await, + solver::Liquidity::Skip => Arc::new(Vec::new()), + } } - }; - - let (auction, liquidity) = tokio::join!(filter_auction, fetch_liquidity); + ); let elapsed = start.elapsed(); metrics::get() diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index b8fb867b8a..16343d9a1b 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -15,8 +15,9 @@ //! In other words the bad order detection is a last fail safe in case //! we were not able to predict issues with orders and pre-emptively //! filter them out of the auction. + use { - crate::domain::competition::{Order, order::Uid}, + crate::domain::competition::{Auction, Order, order::Uid}, eth_domain_types as eth, futures::{StreamExt, stream::FuturesUnordered}, std::{ @@ -84,6 +85,27 @@ impl Detector { self } + /// Removes all unsupported orders from the auction. + pub async fn filter_unsupported_orders_in_auction( + &self, + auction: &mut Auction, + flashloans: bool, + ) { + // Filter out orders that require flashloans if flashloans is disabled. + if !flashloans { + auction.orders.retain(|o| o.app_data.flashloan().is_none()); + } + let unsupported_uids = self.unsupported_order_uids(auction.orders()).await; + // Filter out unsupported orders + if !unsupported_uids.is_empty() { + auction + .orders + .retain(|order| !unsupported_uids.contains(&order.uid)); + } + } + + /// Returns a set of orders uids for orders that are found to be + /// unsupported. pub async fn unsupported_order_uids(&self, orders: &[Order]) -> HashSet { let now = Instant::now(); From 8ae2e9b76646e3b76847722ebd21b1c4a7cc8a32 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Thu, 30 Apr 2026 00:30:10 +0200 Subject: [PATCH 05/15] refactor: implement trait to cover simulation branch in test --- crates/driver/src/domain/competition/mod.rs | 2 +- .../domain/competition/risk_detector/mod.rs | 90 ++++++++++++++++++- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index 26027a3c01..87371e32c4 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -339,7 +339,7 @@ impl Competition { Self::sort_orders(auction, solver_address, order_sorting_strategies) }); - // We can sort the orders, fetch auction data in parallel. + // We can sort the orders and fetch auction data in parallel. let (auction, balances, app_data) = tokio::join!(sort_orders_future, tasks.balances, tasks.app_data); diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index 16343d9a1b..2bdf12f933 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -23,6 +23,7 @@ use { std::{ collections::{HashMap, HashSet}, fmt, + ops::Deref, time::Instant, }, }; @@ -49,13 +50,35 @@ pub enum Quality { Unknown, } +#[async_trait::async_trait] +pub trait SellQualityDetector: Send + Sync { + async fn determine_sell_token_quality(&self, order: &Order, now: Instant) -> Quality; + fn get_quality(&self, token: ð::TokenAddress, now: Instant) -> Quality; + fn evict_outdated_entries(&self); +} + +#[async_trait::async_trait] +impl SellQualityDetector for bad_tokens::simulation::Detector { + async fn determine_sell_token_quality(&self, order: &Order, now: Instant) -> Quality { + self.determine_sell_token_quality(order, now).await + } + + fn get_quality(&self, token: ð::TokenAddress, now: Instant) -> Quality { + Deref::deref(self).get_quality(token, now) + } + + fn evict_outdated_entries(&self) { + Deref::deref(self).evict_outdated_entries() + } +} + #[derive(Default)] pub struct Detector { /// manually configured list of supported and unsupported tokens. Only /// tokens that get detected incorrectly by the automatic detectors get /// listed here and therefore have a higher precedence. hardcoded: HashMap, - simulation_detector: Option, + simulation_detector: Option>, metrics: Option, } @@ -73,9 +96,9 @@ impl Detector { /// methods. pub fn with_simulation_detector( &mut self, - detector: bad_tokens::simulation::Detector, + detector: impl SellQualityDetector + 'static, ) -> &mut Self { - self.simulation_detector = Some(detector); + self.simulation_detector = Some(Box::new(detector)); self } @@ -280,6 +303,38 @@ mod tests { Uid::from_parts(order_hash, signer, valid_to) } + struct TestSellQualityDetector { + sell_detector_unsupported_uid: Uid, + sell_detector_supported_uid: Uid, + } + + #[async_trait::async_trait] + impl SellQualityDetector for TestSellQualityDetector { + async fn determine_sell_token_quality(&self, order: &Order, _: Instant) -> Quality { + if order.uid == self.sell_detector_unsupported_uid { + Quality::Unsupported + } else if order.uid == self.sell_detector_supported_uid { + Quality::Supported + } else { + Quality::Supported + } + } + + fn get_quality(&self, _: ð::TokenAddress, _: Instant) -> Quality { + Quality::Unknown + } + + fn evict_outdated_entries(&self) {} + } + + // Helper to create a mock sell quality detector purely for test + fn sell_quality_detector(supported: Uid, unsupported: Uid) -> TestSellQualityDetector { + TestSellQualityDetector { + sell_detector_unsupported_uid: unsupported, + sell_detector_supported_uid: supported, + } + } + #[tokio::test] async fn unsupported_order_uids_empty_returns_empty() { let detector = Detector::new(Default::default()); @@ -352,10 +407,26 @@ mod tests { addr(5).into(), valid_to, ), // unknown buy + order( + uid(6, addr(11), valid_to), + addr(11), + addr(6).into(), + addr(2).into(), + valid_to, + ), // unknown sell unsupported + order( + uid(7, addr(12), valid_to), + addr(12), + addr(7).into(), + addr(2).into(), + valid_to, + ), // unknown sell supported ]; let metrics_uid = orders[0].uid; let token_uid = orders[1].uid; + let sell_detector_unsupported_uid = orders[5].uid; + let sell_detector_supported_uid = orders[6].uid; let metrics_detector = bad_orders::metrics::Detector::new( 0.5, @@ -378,8 +449,19 @@ mod tests { detector.encoding_failed(&[metrics_uid]); detector.encoding_failed(&[metrics_uid]); + detector.with_simulation_detector(sell_quality_detector( + sell_detector_supported_uid, + sell_detector_unsupported_uid, + )); + let removed = detector.unsupported_order_uids(&orders).await; - assert_eq!(removed, HashSet::from([metrics_uid, token_uid])); + assert_eq!( + removed, + HashSet::from([metrics_uid, token_uid, sell_detector_unsupported_uid]) + ); // all unsupported removed + assert!(!removed.contains(&orders[2].uid)); // supported token kept + assert!(!removed.contains(&orders[4].uid)); // unknown buy kept + assert!(!removed.contains(&sell_detector_supported_uid)); // supported unknown sell kept } } From 144212f06b82bc92aebb463e722115c15ea4aa81 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:27:14 +0200 Subject: [PATCH 06/15] fix: lint --- crates/driver/src/domain/competition/risk_detector/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index 2bdf12f933..db58f7a615 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -313,8 +313,6 @@ mod tests { async fn determine_sell_token_quality(&self, order: &Order, _: Instant) -> Quality { if order.uid == self.sell_detector_unsupported_uid { Quality::Unsupported - } else if order.uid == self.sell_detector_supported_uid { - Quality::Supported } else { Quality::Supported } From 8375c44ed68fbc2b2db8e49794c1deb2f285fd80 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:44:28 +0200 Subject: [PATCH 07/15] fix: lint --- crates/driver/src/domain/competition/risk_detector/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index db58f7a615..b650842040 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -305,6 +305,7 @@ mod tests { struct TestSellQualityDetector { sell_detector_unsupported_uid: Uid, + #[allow(dead_code)] sell_detector_supported_uid: Uid, } From 5ad8f6b63d55306064b65eb0d76c03bc9a11bdd2 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Fri, 1 May 2026 14:12:39 +0200 Subject: [PATCH 08/15] update: use tokio::spawn to schedule filtering and liquidity fetching as separate tasks --- crates/driver/src/domain/competition/mod.rs | 45 +++++++++++++-------- crates/driver/src/infra/api/error.rs | 3 ++ crates/driver/src/infra/observe/mod.rs | 1 + 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index 87371e32c4..fd1e8eebcb 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -351,23 +351,34 @@ impl Competition { }) .await; - // We can run bad token filtering and liquidity fetching in parallel - let (_, liquidity) = tokio::join!( - async { - self.risk_detector - .filter_unsupported_orders_in_auction( - &mut auction, - self.solver.config().flashloans_enabled, - ) - .await - }, - async { - match self.solver.liquidity() { - solver::Liquidity::Fetch => tasks.liquidity.await, - solver::Liquidity::Skip => Arc::new(Vec::new()), - } + // Run bad token filtering and liquidity fetching in separate tasks so they can + // make progress independently and may execute in parallel. + let risk_detector = self.risk_detector.clone(); + let flashloans_enabled = self.solver.config().flashloans_enabled; + let filter_auction = tokio::spawn(async move { + risk_detector + .filter_unsupported_orders_in_auction(&mut auction, flashloans_enabled) + .await; + auction + }); + + let fetch_liquidity = match self.solver.liquidity() { + solver::Liquidity::Fetch => { + let liquidity = tasks.liquidity; + tokio::spawn(liquidity) } - ); + solver::Liquidity::Skip => tokio::spawn(async { Arc::new(Vec::new()) }), + }; + + let (auction, liquidity) = tokio::join!(filter_auction, fetch_liquidity); + let auction = auction.map_err(|err| { + tracing::error!(?err, "unsupported order filtering task failed"); + Error::PreparationError + })?; + let liquidity = liquidity.map_err(|err| { + tracing::error!(?err, "liquidity fetching task failed"); + Error::PreparationError + })?; let elapsed = start.elapsed(); metrics::get() @@ -1074,4 +1085,6 @@ pub enum Error { NoValidOrdersFound, #[error("could not parse the request")] MalformedRequest, + #[error("Preparation task failed")] + PreparationError, } diff --git a/crates/driver/src/infra/api/error.rs b/crates/driver/src/infra/api/error.rs index 0b6529fa88..24bef5cb1a 100644 --- a/crates/driver/src/infra/api/error.rs +++ b/crates/driver/src/infra/api/error.rs @@ -29,6 +29,7 @@ enum Kind { TokenTemporarilySuspended, InsufficientLiquidity, CustomSolverError, + PreparationFailed, } #[derive(Debug, Serialize)] @@ -69,6 +70,7 @@ impl From for (axum::http::StatusCode, axum::Json) { Kind::TokenTemporarilySuspended => "Token is temporarily suspended from trading", Kind::InsufficientLiquidity => "Insufficient liquidity for the requested trade size", Kind::CustomSolverError => "Solver returned a custom error", + Kind::PreparationFailed => "Preparation failed", }; ( axum::http::StatusCode::BAD_REQUEST, @@ -146,6 +148,7 @@ impl From for (axum::http::StatusCode, axum::Json) { competition::Error::TooManyPendingSettlements => Kind::TooManyPendingSettlements, competition::Error::NoValidOrdersFound => Kind::NoValidOrders, competition::Error::MalformedRequest => Kind::MalformedRequest, + competition::Error::PreparationError => Kind::PreparationFailed, }; error.into() } diff --git a/crates/driver/src/infra/observe/mod.rs b/crates/driver/src/infra/observe/mod.rs index 6a69180f6c..503fa1c937 100644 --- a/crates/driver/src/infra/observe/mod.rs +++ b/crates/driver/src/infra/observe/mod.rs @@ -459,6 +459,7 @@ fn competition_error(err: &competition::Error) -> &'static str { competition::Error::TooManyPendingSettlements => "TooManyPendingSettlements", competition::Error::NoValidOrdersFound => "NoValidOrdersFound", competition::Error::MalformedRequest => "MalformedRequest", + competition::Error::PreparationError => "PreparationFailed", } } From b56b2b93d3732354ee71f8997cc939b45beb4b29 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Mon, 4 May 2026 23:13:00 +0200 Subject: [PATCH 09/15] review: address comments --- Cargo.lock | 1 + crates/driver/Cargo.toml | 1 + crates/driver/src/domain/competition/mod.rs | 54 +- .../risk_detector/bad_tokens/simulation.rs | 25 + .../domain/competition/risk_detector/mod.rs | 486 +++++++++++------- crates/driver/src/domain/quote.rs | 9 +- crates/driver/src/infra/api/error.rs | 12 +- crates/driver/src/infra/observe/mod.rs | 2 +- 8 files changed, 368 insertions(+), 222 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c23352acee..d1e73a045b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3939,6 +3939,7 @@ dependencies = [ "liquidity-sources", "maplit", "mimalloc", + "mockall", "model", "moka", "num", diff --git a/crates/driver/Cargo.toml b/crates/driver/Cargo.toml index 470395b1c2..af86ad8248 100644 --- a/crates/driver/Cargo.toml +++ b/crates/driver/Cargo.toml @@ -97,6 +97,7 @@ app-data = { workspace = true, features = ["test_helpers"] } contracts = { workspace = true } ethrpc = { workspace = true, features = ["test-util"] } maplit = { workspace = true } +mockall = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["process", "test-util"] } diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index fd1e8eebcb..196537968b 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -351,34 +351,32 @@ impl Competition { }) .await; - // Run bad token filtering and liquidity fetching in separate tasks so they can - // make progress independently and may execute in parallel. let risk_detector = self.risk_detector.clone(); let flashloans_enabled = self.solver.config().flashloans_enabled; - let filter_auction = tokio::spawn(async move { - risk_detector - .filter_unsupported_orders_in_auction(&mut auction, flashloans_enabled) - .await; - auction - }); - - let fetch_liquidity = match self.solver.liquidity() { - solver::Liquidity::Fetch => { - let liquidity = tasks.liquidity; - tokio::spawn(liquidity) - } - solver::Liquidity::Skip => tokio::spawn(async { Arc::new(Vec::new()) }), - }; - - let (auction, liquidity) = tokio::join!(filter_auction, fetch_liquidity); - let auction = auction.map_err(|err| { - tracing::error!(?err, "unsupported order filtering task failed"); - Error::PreparationError - })?; - let liquidity = liquidity.map_err(|err| { - tracing::error!(?err, "liquidity fetching task failed"); - Error::PreparationError - })?; + let liquidity_mode = self.solver.liquidity(); + + // We can run bad token filtering and liquidity fetching in parallel + let (auction, liquidity) = tokio::try_join!( + tokio::spawn( + async move { + risk_detector + .without_unsupported_orders(&mut auction.orders, flashloans_enabled) + .await; + auction + } + .in_current_span(), + ), + tokio::spawn( + async move { + match liquidity_mode { + solver::Liquidity::Fetch => tasks.liquidity.await, + solver::Liquidity::Skip => Arc::new(Vec::new()), + } + } + .in_current_span(), + ), + ) + .inspect_err(|err| tracing::error!(?err, "auction preparation task failed"))?; let elapsed = start.elapsed(); metrics::get() @@ -1085,6 +1083,6 @@ pub enum Error { NoValidOrdersFound, #[error("could not parse the request")] MalformedRequest, - #[error("Preparation task failed")] - PreparationError, + #[error("task join error: {0}")] + Join(#[from] task::JoinError), } diff --git a/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs b/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs index a6018e6035..91921f4f58 100644 --- a/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs +++ b/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs @@ -5,10 +5,12 @@ use { infra::{self, observe::metrics}, }, bad_tokens::{TokenQuality, trace_call::TraceCallDetectorRaw}, + eth_domain_types as eth, futures::FutureExt, model::interaction::InteractionData, request_sharing::BoxRequestSharing, std::{ + ops::Deref, sync::Arc, time::{Duration, Instant}, }, @@ -21,6 +23,29 @@ use { #[derive(Clone)] pub struct Detector(Arc); +#[cfg_attr(test, mockall::automock)] +#[async_trait::async_trait] +pub trait SellQualityDetector: Send + Sync { + async fn determine_sell_token_quality(&self, order: &Order, now: Instant) -> Quality; + fn get_quality(&self, token: ð::TokenAddress, now: Instant) -> Quality; + fn evict_outdated_entries(&self); +} + +#[async_trait::async_trait] +impl SellQualityDetector for Detector { + async fn determine_sell_token_quality(&self, order: &Order, now: Instant) -> Quality { + self.determine_sell_token_quality(order, now).await + } + + fn get_quality(&self, token: ð::TokenAddress, now: Instant) -> Quality { + Deref::deref(self).get_quality(token, now) + } + + fn evict_outdated_entries(&self) { + Deref::deref(self).evict_outdated_entries() + } +} + struct Inner { cache: Cache, detector: TraceCallDetectorRaw, diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index b650842040..309a30e141 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -17,15 +17,19 @@ //! filter them out of the auction. use { - crate::domain::competition::{Auction, Order, order::Uid}, + crate::domain::competition::{ + Order, + order::Uid, + risk_detector::bad_tokens::simulation::SellQualityDetector, + }, eth_domain_types as eth, futures::{StreamExt, stream::FuturesUnordered}, std::{ collections::{HashMap, HashSet}, fmt, - ops::Deref, time::Instant, }, + tracing::instrument, }; pub mod bad_orders; @@ -50,28 +54,6 @@ pub enum Quality { Unknown, } -#[async_trait::async_trait] -pub trait SellQualityDetector: Send + Sync { - async fn determine_sell_token_quality(&self, order: &Order, now: Instant) -> Quality; - fn get_quality(&self, token: ð::TokenAddress, now: Instant) -> Quality; - fn evict_outdated_entries(&self); -} - -#[async_trait::async_trait] -impl SellQualityDetector for bad_tokens::simulation::Detector { - async fn determine_sell_token_quality(&self, order: &Order, now: Instant) -> Quality { - self.determine_sell_token_quality(order, now).await - } - - fn get_quality(&self, token: ð::TokenAddress, now: Instant) -> Quality { - Deref::deref(self).get_quality(token, now) - } - - fn evict_outdated_entries(&self) { - Deref::deref(self).evict_outdated_entries() - } -} - #[derive(Default)] pub struct Detector { /// manually configured list of supported and unsupported tokens. Only @@ -108,22 +90,27 @@ impl Detector { self } - /// Removes all unsupported orders from the auction. - pub async fn filter_unsupported_orders_in_auction( + #[instrument(skip_all)] + /// Performs flashloan filtering and removes all unsupported orders from the + /// auction. + pub async fn without_unsupported_orders( &self, - auction: &mut Auction, - flashloans: bool, + orders: &mut Vec, + flashloans_enabled: bool, ) { - // Filter out orders that require flashloans if flashloans is disabled. - if !flashloans { - auction.orders.retain(|o| o.app_data.flashloan().is_none()); + if !flashloans_enabled { + orders.retain(|o| o.app_data.flashloan().is_none()); } - let unsupported_uids = self.unsupported_order_uids(auction.orders()).await; + self.filter_unsupported_orders_in_auction_orders(orders) + .await + } + + /// Removes all unsupported orders from the auction. + pub async fn filter_unsupported_orders_in_auction_orders(&self, orders: &mut Vec) { + let unsupported_uids = self.unsupported_order_uids(orders).await; // Filter out unsupported orders if !unsupported_uids.is_empty() { - auction - .orders - .retain(|order| !unsupported_uids.contains(&order.uid)); + orders.retain(|order| !unsupported_uids.contains(&order.uid)); } } @@ -131,56 +118,50 @@ impl Detector { /// unsupported. pub async fn unsupported_order_uids(&self, orders: &[Order]) -> HashSet { let now = Instant::now(); - let mut token_quality_checks = FuturesUnordered::new(); - let mut removed_uids = Vec::new(); - - orders.iter().for_each(|order| { - if self - .metrics - .as_ref() - .map(|metrics| metrics.get_quality(&order.uid, now)) - .is_some_and(|q| q == Quality::Unsupported) - { - removed_uids.push(order.uid); - return; - } - - let sell = self.get_token_quality(order.sell.token, now); - let buy = self.get_token_quality(order.buy.token, now); - match (sell, buy) { - // at least 1 token unsupported => drop order - (Quality::Unsupported, _) | (_, Quality::Unsupported) => { - removed_uids.push(order.uid); + let mut removed_uids: HashSet = orders + .iter() + .filter_map(|order| { + // Check if uid is unsupported in metrics + if matches!( + self.metrics + .as_ref() + .map(|m| m.get_quality(&order.uid, now)), + Some(Quality::Unsupported) + ) { + return Some(order.uid); } - - // sell token quality is unknown => keep order if token is supported - (Quality::Unknown, _) => { - let Some(detector) = &self.simulation_detector else { - // we can't determine quality => assume order is good - return; - }; - - let check_tokens_fut = async move { - let quality = detector.determine_sell_token_quality(order, now).await; - (order.uid, quality) - }; - token_quality_checks.push(check_tokens_fut); + let sell = self.get_token_quality(order.sell.token, now); + let buy = self.get_token_quality(order.buy.token, now); + match (sell, buy) { + // both tokens supported => keep order + (Quality::Supported, Quality::Supported) => None, + // at least 1 token unsupported => drop order + (Quality::Unsupported, _) | (_, Quality::Unsupported) => Some(order.uid), + // sell token quality is unknown => keep order if token is supported + (Quality::Unknown, _) => { + let Some(detector) = &self.simulation_detector else { + // we can't determine quality => assume order is good + return None; + }; + let check_tokens_fut = async move { + let quality = detector.determine_sell_token_quality(order, now).await; + (order.uid, quality) + }; + token_quality_checks.push(check_tokens_fut); + None + } + // buy token quality is unknown => keep order (because we can't + // determine quality and assume it's good) + (_, Quality::Unknown) => None, } - - // both tokens supported => keep order - (Quality::Supported, Quality::Supported) => {} - - // buy token quality is unknown => keep order (because we can't - // determine quality and assume it's good) - (_, Quality::Unknown) => {} - } - }); + }) + .collect(); while let Some((uid, quality)) = token_quality_checks.next().await { if quality != Quality::Supported { - removed_uids.push(uid); + removed_uids.insert(uid); } } @@ -195,7 +176,7 @@ impl Detector { detector.evict_outdated_entries(); } - removed_uids.into_iter().collect() + removed_uids } /// Updates the tokens quality metric for successful operation. @@ -248,16 +229,31 @@ mod tests { Side, Signature, Uid, + app_data::AppData, signature, }, + risk_detector::bad_tokens::simulation::MockSellQualityDetector, }, infra::solver, util, }, - eth_domain_types::TokenAmount, - std::time::Duration, + app_data::{ + AppDataHash, + Flashloan, + ProtocolAppData, + Root, + ValidatedAppData, + hash_full_app_data, + }, + eth_domain_types::{Asset, TokenAmount, U256}, + std::{sync::Arc, time::Duration}, }; + // Helper to create a mock eth:Address purely for test + fn addr(n: u8) -> eth::Address { + eth::Address::from_slice(&[n; 20]) + } + // Helper to create a mock order purely for test fn order( uid: Uid, @@ -265,23 +261,47 @@ mod tests { sell_token: eth::TokenAddress, buy_token: eth::TokenAddress, valid_to: u32, + flashloan: bool, ) -> Order { + let app_data = if flashloan { + let protocol = ProtocolAppData { + flashloan: Some(Flashloan { + liquidity_provider: addr(99), + receiver: signer, + token: sell_token.into(), + protocol_adapter: addr(98), + amount: U256::from(1), + }), + ..Default::default() + }; + let root = Root::new(Some(protocol.clone())); + let document = serde_json::to_string(&root).unwrap(); + let hash = AppDataHash(hash_full_app_data(document.as_bytes())); + AppData::Full(Arc::new(ValidatedAppData { + hash, + document, + protocol, + })) + } else { + Default::default() + }; + Order { uid, receiver: Some(signer), created: util::Timestamp(0), valid_to: util::Timestamp(valid_to), - buy: eth::Asset { + buy: Asset { token: buy_token, amount: TokenAmount::from(1), }, - sell: eth::Asset { + sell: Asset { token: sell_token, amount: TokenAmount::from(1), }, side: Side::Sell, kind: Kind::Limit, - app_data: Default::default(), + app_data, partial: Partial::No, pre_interactions: vec![], post_interactions: vec![], @@ -303,164 +323,264 @@ mod tests { Uid::from_parts(order_hash, signer, valid_to) } - struct TestSellQualityDetector { - sell_detector_unsupported_uid: Uid, - #[allow(dead_code)] - sell_detector_supported_uid: Uid, - } - - #[async_trait::async_trait] - impl SellQualityDetector for TestSellQualityDetector { - async fn determine_sell_token_quality(&self, order: &Order, _: Instant) -> Quality { - if order.uid == self.sell_detector_unsupported_uid { - Quality::Unsupported - } else { - Quality::Supported - } - } - - fn get_quality(&self, _: ð::TokenAddress, _: Instant) -> Quality { - Quality::Unknown - } - - fn evict_outdated_entries(&self) {} - } - // Helper to create a mock sell quality detector purely for test - fn sell_quality_detector(supported: Uid, unsupported: Uid) -> TestSellQualityDetector { - TestSellQualityDetector { - sell_detector_unsupported_uid: unsupported, - sell_detector_supported_uid: supported, - } + fn sell_quality_detector(unsupported_uid: Uid) -> MockSellQualityDetector { + let mut detector = MockSellQualityDetector::new(); + detector + .expect_determine_sell_token_quality() + .returning(move |order, _now| { + if order.uid == unsupported_uid { + Quality::Unsupported + } else { + Quality::Supported + } + }); + detector + .expect_get_quality() + .returning(|_, _| Quality::Unknown); + detector.expect_evict_outdated_entries().returning(|| ()); + detector } #[tokio::test] - async fn unsupported_order_uids_empty_returns_empty() { + async fn without_unsupported_orders_returns_empty() { + let mut orders = vec![]; let detector = Detector::new(Default::default()); - let removed = detector.unsupported_order_uids(&[]).await; - assert!(removed.is_empty()); + detector.without_unsupported_orders(&mut orders, true).await; + assert!(orders.is_empty()); } #[tokio::test] - async fn all_supported_orders_are_kept() { - let signer = eth::Address::from_slice(&[1; 20]); - let sell_token = eth::Address::from_slice(&[2; 20]).into(); - let buy_token = eth::Address::from_slice(&[3; 20]).into(); + async fn ithout_unsupported_orders_all_supported_orders_are_kept() { + let signer = addr(1); + let sell_token = addr(2).into(); + let buy_token = addr(3).into(); + let order_uid = uid(1, signer, u32::MAX); + let mut orders = vec![order( + order_uid, + signer, + sell_token, + buy_token, + u32::MAX, + false, + )]; let detector = Detector::new(Default::default()); - let removed = detector - .unsupported_order_uids(&[order( - uid(1, signer, u32::MAX), - signer, - sell_token, - buy_token, - u32::MAX, - )]) - .await; + detector.without_unsupported_orders(&mut orders, true).await; - assert!(removed.is_empty()); + assert_eq!(orders.len(), 1); + assert_eq!(orders[0].uid, order_uid); } #[tokio::test] - async fn unsupported_order_uids_returns_only_unsupported_orders() { - fn addr(n: u8) -> eth::Address { - eth::Address::from_slice(&[n; 20]) - } - + async fn without_unsupported_orders_filters_unsupported_orders_and_flashloans() { let valid_to = u32::MAX; - let orders = vec![ + let metrics_bad_uid = uid(1, addr(6), valid_to); + let token_bad_uid = uid(2, addr(7), valid_to); + let supported_uid = uid(3, addr(8), valid_to); + let unknown_sell_uid = uid(4, addr(9), valid_to); + let unknown_buy_uid = uid(5, addr(10), valid_to); + let sell_detector_unsupported_uid = uid(6, addr(11), valid_to); + let sell_detector_supported_uid = uid(7, addr(12), valid_to); + let flashloan_uid = uid(8, addr(13), valid_to); + + let metrics_detector = bad_orders::metrics::Detector::new( + 0.5, + 2, + false, + Duration::from_secs(60), + Duration::from_secs(60), + Duration::from_secs(60), + solver::Name("test_solver".into()), + ); + let metrics_quality_unsupported_address = addr(3).into(); + let mut detector_config = HashMap::new(); + detector_config.insert(metrics_quality_unsupported_address, Quality::Unsupported); + + let mut detector = Detector::new(detector_config); + detector.with_metrics_detector(metrics_detector); + + // Simulate repeated metrics failure for order with metrics_bad_uid + detector.encoding_failed(&[metrics_bad_uid]); + detector.encoding_failed(&[metrics_bad_uid]); + detector.encoding_failed(&[metrics_bad_uid]); + detector.with_simulation_detector(sell_quality_detector(sell_detector_unsupported_uid)); + + // Test with flashloans disabled + let mut orders_flashloans_disabled = vec![ order( - uid(1, addr(6), valid_to), + metrics_bad_uid, addr(6), addr(1).into(), addr(2).into(), valid_to, - ), // metrics bad + false, + ), order( - uid(2, addr(7), valid_to), + token_bad_uid, addr(7), - addr(3).into(), + metrics_quality_unsupported_address, addr(2).into(), valid_to, - ), // token bad + false, + ), order( - uid(3, addr(8), valid_to), + supported_uid, addr(8), addr(1).into(), addr(2).into(), valid_to, - ), // token supported + false, + ), order( - uid(4, addr(9), valid_to), + unknown_sell_uid, addr(9), addr(4).into(), addr(2).into(), valid_to, - ), // unknown sell + false, + ), order( - uid(5, addr(10), valid_to), + unknown_buy_uid, addr(10), addr(1).into(), addr(5).into(), valid_to, - ), // unknown buy + false, + ), order( - uid(6, addr(11), valid_to), + sell_detector_unsupported_uid, addr(11), addr(6).into(), addr(2).into(), valid_to, - ), // unknown sell unsupported + false, + ), order( - uid(7, addr(12), valid_to), + sell_detector_supported_uid, addr(12), addr(7).into(), addr(2).into(), valid_to, - ), // unknown sell supported + false, + ), + order( + flashloan_uid, + addr(13), + addr(1).into(), + addr(2).into(), + valid_to, + true, + ), ]; - let metrics_uid = orders[0].uid; - let token_uid = orders[1].uid; - let sell_detector_unsupported_uid = orders[5].uid; - let sell_detector_supported_uid = orders[6].uid; - - let metrics_detector = bad_orders::metrics::Detector::new( - 0.5, - 2, - false, - Duration::from_secs(60), - Duration::from_secs(60), - Duration::from_secs(60), - solver::Name("test_solver".into()), - ); + detector + .without_unsupported_orders(&mut orders_flashloans_disabled, false) + .await; - let mut detector_config = HashMap::new(); - detector_config.insert(addr(3).into(), Quality::Unsupported); + let remaining_uids = orders_flashloans_disabled + .iter() + .map(|order| order.uid) + .collect::>(); - let mut detector = Detector::new(detector_config); - detector.with_metrics_detector(metrics_detector); + assert_eq!( + remaining_uids, + vec![ + supported_uid, + unknown_sell_uid, + unknown_buy_uid, + sell_detector_supported_uid, + ] + ); - // Simulate repeated metrics failure for order with metrics_uid - detector.encoding_failed(&[metrics_uid]); - detector.encoding_failed(&[metrics_uid]); - detector.encoding_failed(&[metrics_uid]); + // Test with flashloans enabled + let mut orders_flashloans_enabled = vec![ + order( + metrics_bad_uid, + addr(6), + addr(1).into(), + addr(2).into(), + valid_to, + false, + ), + order( + token_bad_uid, + addr(7), + addr(3).into(), + addr(2).into(), + valid_to, + false, + ), + order( + supported_uid, + addr(8), + addr(1).into(), + addr(2).into(), + valid_to, + false, + ), + order( + unknown_sell_uid, + addr(9), + addr(4).into(), + addr(2).into(), + valid_to, + false, + ), + order( + unknown_buy_uid, + addr(10), + addr(1).into(), + addr(5).into(), + valid_to, + false, + ), + order( + sell_detector_unsupported_uid, + addr(11), + addr(6).into(), + addr(2).into(), + valid_to, + false, + ), + order( + sell_detector_supported_uid, + addr(12), + addr(7).into(), + addr(2).into(), + valid_to, + false, + ), + order( + flashloan_uid, + addr(13), + addr(1).into(), + addr(2).into(), + valid_to, + true, + ), + ]; - detector.with_simulation_detector(sell_quality_detector( - sell_detector_supported_uid, - sell_detector_unsupported_uid, - )); + detector + .without_unsupported_orders(&mut orders_flashloans_enabled, true) + .await; - let removed = detector.unsupported_order_uids(&orders).await; + let remaining_uids = orders_flashloans_enabled + .iter() + .map(|order| order.uid) + .collect::>(); assert_eq!( - removed, - HashSet::from([metrics_uid, token_uid, sell_detector_unsupported_uid]) - ); // all unsupported removed - assert!(!removed.contains(&orders[2].uid)); // supported token kept - assert!(!removed.contains(&orders[4].uid)); // unknown buy kept - assert!(!removed.contains(&sell_detector_supported_uid)); // supported unknown sell kept + remaining_uids, + vec![ + supported_uid, + unknown_sell_uid, + unknown_buy_uid, + sell_detector_supported_uid, + flashloan_uid, + ] + ); } } diff --git a/crates/driver/src/domain/quote.rs b/crates/driver/src/domain/quote.rs index 89a9c5eb69..df2452f0d4 100644 --- a/crates/driver/src/domain/quote.rs +++ b/crates/driver/src/domain/quote.rs @@ -144,12 +144,9 @@ impl Order { let mut auction = self .fake_auction(eth, tokens, solver.quote_using_limit_orders()) .await?; - let unsupported_uids = risk_detector.unsupported_order_uids(&auction.orders).await; - if !unsupported_uids.is_empty() { - auction - .orders - .retain(|order| !unsupported_uids.contains(&order.uid)); - } + risk_detector + .filter_unsupported_orders_in_auction_orders(&mut auction.orders) + .await; if auction.orders.is_empty() { return Err(QuotingFailed::UnsupportedToken.into()); } diff --git a/crates/driver/src/infra/api/error.rs b/crates/driver/src/infra/api/error.rs index 24bef5cb1a..fab31fb042 100644 --- a/crates/driver/src/infra/api/error.rs +++ b/crates/driver/src/infra/api/error.rs @@ -29,7 +29,7 @@ enum Kind { TokenTemporarilySuspended, InsufficientLiquidity, CustomSolverError, - PreparationFailed, + Join, } #[derive(Debug, Serialize)] @@ -70,10 +70,14 @@ impl From for (axum::http::StatusCode, axum::Json) { Kind::TokenTemporarilySuspended => "Token is temporarily suspended from trading", Kind::InsufficientLiquidity => "Insufficient liquidity for the requested trade size", Kind::CustomSolverError => "Solver returned a custom error", - Kind::PreparationFailed => "Preparation failed", + Kind::Join => "Internal task failure", + }; + let status = match value { + Kind::Join => axum::http::StatusCode::INTERNAL_SERVER_ERROR, + _ => axum::http::StatusCode::BAD_REQUEST, }; ( - axum::http::StatusCode::BAD_REQUEST, + status, axum::Json(Error { kind: value, description: description.into(), @@ -148,7 +152,7 @@ impl From for (axum::http::StatusCode, axum::Json) { competition::Error::TooManyPendingSettlements => Kind::TooManyPendingSettlements, competition::Error::NoValidOrdersFound => Kind::NoValidOrders, competition::Error::MalformedRequest => Kind::MalformedRequest, - competition::Error::PreparationError => Kind::PreparationFailed, + competition::Error::Join(_) => Kind::Join, }; error.into() } diff --git a/crates/driver/src/infra/observe/mod.rs b/crates/driver/src/infra/observe/mod.rs index 503fa1c937..7ff8851e3e 100644 --- a/crates/driver/src/infra/observe/mod.rs +++ b/crates/driver/src/infra/observe/mod.rs @@ -459,7 +459,7 @@ fn competition_error(err: &competition::Error) -> &'static str { competition::Error::TooManyPendingSettlements => "TooManyPendingSettlements", competition::Error::NoValidOrdersFound => "NoValidOrdersFound", competition::Error::MalformedRequest => "MalformedRequest", - competition::Error::PreparationError => "PreparationFailed", + competition::Error::Join(_) => "Join", } } From 6045c8b4409c7956e9c515a7d0a7416c9d5b0764 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Tue, 5 May 2026 12:15:49 +0200 Subject: [PATCH 10/15] refactor: simplify test --- .../domain/competition/risk_detector/mod.rs | 130 +++++------------- 1 file changed, 33 insertions(+), 97 deletions(-) diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index 309a30e141..ca33e0d283 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -351,7 +351,7 @@ mod tests { } #[tokio::test] - async fn ithout_unsupported_orders_all_supported_orders_are_kept() { + async fn without_unsupported_orders_all_supported_orders_are_kept() { let signer = addr(1); let sell_token = addr(2).into(); let buy_token = addr(3).into(); @@ -364,11 +364,8 @@ mod tests { u32::MAX, false, )]; - let detector = Detector::new(Default::default()); - detector.without_unsupported_orders(&mut orders, true).await; - assert_eq!(orders.len(), 1); assert_eq!(orders[0].uid, order_uid); } @@ -376,7 +373,6 @@ mod tests { #[tokio::test] async fn without_unsupported_orders_filters_unsupported_orders_and_flashloans() { let valid_to = u32::MAX; - let metrics_bad_uid = uid(1, addr(6), valid_to); let token_bad_uid = uid(2, addr(7), valid_to); let supported_uid = uid(3, addr(8), valid_to); @@ -385,32 +381,11 @@ mod tests { let sell_detector_unsupported_uid = uid(6, addr(11), valid_to); let sell_detector_supported_uid = uid(7, addr(12), valid_to); let flashloan_uid = uid(8, addr(13), valid_to); - - let metrics_detector = bad_orders::metrics::Detector::new( - 0.5, - 2, - false, - Duration::from_secs(60), - Duration::from_secs(60), - Duration::from_secs(60), - solver::Name("test_solver".into()), - ); let metrics_quality_unsupported_address = addr(3).into(); - let mut detector_config = HashMap::new(); - detector_config.insert(metrics_quality_unsupported_address, Quality::Unsupported); - - let mut detector = Detector::new(detector_config); - detector.with_metrics_detector(metrics_detector); - - // Simulate repeated metrics failure for order with metrics_bad_uid - detector.encoding_failed(&[metrics_bad_uid]); - detector.encoding_failed(&[metrics_bad_uid]); - detector.encoding_failed(&[metrics_bad_uid]); - detector.with_simulation_detector(sell_quality_detector(sell_detector_unsupported_uid)); - // Test with flashloans disabled - let mut orders_flashloans_disabled = vec![ + let orders = vec![ order( + // metrics bad -> discard metrics_bad_uid, addr(6), addr(1).into(), @@ -419,6 +394,7 @@ mod tests { false, ), order( + // token bad -> discard token_bad_uid, addr(7), metrics_quality_unsupported_address, @@ -427,6 +403,7 @@ mod tests { false, ), order( + // token good -> keep supported_uid, addr(8), addr(1).into(), @@ -435,6 +412,7 @@ mod tests { false, ), order( + // unknown sell -> keep unknown_sell_uid, addr(9), addr(4).into(), @@ -443,6 +421,7 @@ mod tests { false, ), order( + // unknown buy -> keep unknown_buy_uid, addr(10), addr(1).into(), @@ -451,6 +430,7 @@ mod tests { false, ), order( + // unsupported on quality check -> discard sell_detector_unsupported_uid, addr(11), addr(6).into(), @@ -459,6 +439,7 @@ mod tests { false, ), order( + // supported on quality check -> keep sell_detector_supported_uid, addr(12), addr(7).into(), @@ -467,6 +448,7 @@ mod tests { false, ), order( + // flashloan -> keep if flashloans is enabled, else discard flashloan_uid, addr(13), addr(1).into(), @@ -476,10 +458,32 @@ mod tests { ), ]; + let mut orders_flashloans_disabled = orders.clone(); + let mut orders_flashloans_enabled = orders.clone(); + + let metrics_detector = bad_orders::metrics::Detector::new( + 0.5, + 2, + false, + Duration::from_secs(60), + Duration::from_secs(60), + Duration::from_secs(60), + solver::Name("test_solver".into()), + ); + let mut detector_config = HashMap::new(); + detector_config.insert(metrics_quality_unsupported_address, Quality::Unsupported); + let mut detector = Detector::new(detector_config); + detector.with_metrics_detector(metrics_detector); + // Simulate repeated metrics failure for order with metrics_bad_uid + detector.encoding_failed(&[metrics_bad_uid]); + detector.encoding_failed(&[metrics_bad_uid]); + detector.encoding_failed(&[metrics_bad_uid]); + detector.with_simulation_detector(sell_quality_detector(sell_detector_unsupported_uid)); + + // Test with flashloans disabled detector .without_unsupported_orders(&mut orders_flashloans_disabled, false) .await; - let remaining_uids = orders_flashloans_disabled .iter() .map(|order| order.uid) @@ -496,77 +500,9 @@ mod tests { ); // Test with flashloans enabled - let mut orders_flashloans_enabled = vec![ - order( - metrics_bad_uid, - addr(6), - addr(1).into(), - addr(2).into(), - valid_to, - false, - ), - order( - token_bad_uid, - addr(7), - addr(3).into(), - addr(2).into(), - valid_to, - false, - ), - order( - supported_uid, - addr(8), - addr(1).into(), - addr(2).into(), - valid_to, - false, - ), - order( - unknown_sell_uid, - addr(9), - addr(4).into(), - addr(2).into(), - valid_to, - false, - ), - order( - unknown_buy_uid, - addr(10), - addr(1).into(), - addr(5).into(), - valid_to, - false, - ), - order( - sell_detector_unsupported_uid, - addr(11), - addr(6).into(), - addr(2).into(), - valid_to, - false, - ), - order( - sell_detector_supported_uid, - addr(12), - addr(7).into(), - addr(2).into(), - valid_to, - false, - ), - order( - flashloan_uid, - addr(13), - addr(1).into(), - addr(2).into(), - valid_to, - true, - ), - ]; - detector .without_unsupported_orders(&mut orders_flashloans_enabled, true) .await; - let remaining_uids = orders_flashloans_enabled .iter() .map(|order| order.uid) From ede7489ed2ebd0800b2b3ad62b90904f0fab3e49 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Tue, 5 May 2026 23:13:58 +0200 Subject: [PATCH 11/15] review: address additional review comments --- crates/driver/src/domain/competition/mod.rs | 17 +- .../risk_detector/bad_tokens/simulation.rs | 4 +- .../domain/competition/risk_detector/mod.rs | 490 ++++++++++-------- crates/driver/src/domain/quote.rs | 2 +- crates/driver/src/infra/api/error.rs | 8 +- crates/driver/src/infra/observe/mod.rs | 2 +- 6 files changed, 289 insertions(+), 234 deletions(-) diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index 196537968b..5714107c81 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -356,7 +356,7 @@ impl Competition { let liquidity_mode = self.solver.liquidity(); // We can run bad token filtering and liquidity fetching in parallel - let (auction, liquidity) = tokio::try_join!( + let (auction, liquidity) = tokio::join!( tokio::spawn( async move { risk_detector @@ -375,8 +375,15 @@ impl Competition { } .in_current_span(), ), - ) - .inspect_err(|err| tracing::error!(?err, "auction preparation task failed"))?; + ); + let auction = auction.map_err(|err| { + tracing::error!(?err, "auction preparation task failed"); + Error::InternalError(err.to_string()) + })?; + let liquidity = liquidity.map_err(|err| { + tracing::error!(?err, "liquidity fetch task failed"); + Error::InternalError(err.to_string()) + })?; let elapsed = start.elapsed(); metrics::get() @@ -1083,6 +1090,6 @@ pub enum Error { NoValidOrdersFound, #[error("could not parse the request")] MalformedRequest, - #[error("task join error: {0}")] - Join(#[from] task::JoinError), + #[error("internal error: {0}")] + InternalError(String), } diff --git a/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs b/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs index 91921f4f58..acd9410e01 100644 --- a/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs +++ b/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs @@ -25,14 +25,14 @@ pub struct Detector(Arc); #[cfg_attr(test, mockall::automock)] #[async_trait::async_trait] -pub trait SellQualityDetector: Send + Sync { +pub trait DetectorApi: Send + Sync { async fn determine_sell_token_quality(&self, order: &Order, now: Instant) -> Quality; fn get_quality(&self, token: ð::TokenAddress, now: Instant) -> Quality; fn evict_outdated_entries(&self); } #[async_trait::async_trait] -impl SellQualityDetector for Detector { +impl DetectorApi for Detector { async fn determine_sell_token_quality(&self, order: &Order, now: Instant) -> Quality { self.determine_sell_token_quality(order, now).await } diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index ca33e0d283..94563c6bda 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -20,7 +20,7 @@ use { crate::domain::competition::{ Order, order::Uid, - risk_detector::bad_tokens::simulation::SellQualityDetector, + risk_detector::bad_tokens::simulation::DetectorApi, }, eth_domain_types as eth, futures::{StreamExt, stream::FuturesUnordered}, @@ -60,7 +60,7 @@ pub struct Detector { /// tokens that get detected incorrectly by the automatic detectors get /// listed here and therefore have a higher precedence. hardcoded: HashMap, - simulation_detector: Option>, + simulation_detector: Option>, metrics: Option, } @@ -76,10 +76,7 @@ impl Detector { /// Enables detection of unsupported tokens via simulation based detection /// methods. - pub fn with_simulation_detector( - &mut self, - detector: impl SellQualityDetector + 'static, - ) -> &mut Self { + pub fn with_simulation_detector(&mut self, detector: impl DetectorApi + 'static) -> &mut Self { self.simulation_detector = Some(Box::new(detector)); self } @@ -90,93 +87,87 @@ impl Detector { self } - #[instrument(skip_all)] /// Performs flashloan filtering and removes all unsupported orders from the /// auction. + #[instrument(skip_all)] pub async fn without_unsupported_orders( &self, orders: &mut Vec, flashloans_enabled: bool, ) { - if !flashloans_enabled { - orders.retain(|o| o.app_data.flashloan().is_none()); - } - self.filter_unsupported_orders_in_auction_orders(orders) - .await - } - - /// Removes all unsupported orders from the auction. - pub async fn filter_unsupported_orders_in_auction_orders(&self, orders: &mut Vec) { - let unsupported_uids = self.unsupported_order_uids(orders).await; - // Filter out unsupported orders - if !unsupported_uids.is_empty() { - orders.retain(|order| !unsupported_uids.contains(&order.uid)); - } - } - - /// Returns a set of orders uids for orders that are found to be - /// unsupported. - pub async fn unsupported_order_uids(&self, orders: &[Order]) -> HashSet { let now = Instant::now(); let mut token_quality_checks = FuturesUnordered::new(); - let mut removed_uids: HashSet = orders - .iter() - .filter_map(|order| { - // Check if uid is unsupported in metrics - if matches!( - self.metrics - .as_ref() - .map(|m| m.get_quality(&order.uid, now)), - Some(Quality::Unsupported) - ) { - return Some(order.uid); + // List of orders that have been removed + let mut removed_uids: HashSet = HashSet::new(); + // List of orders that will be retained + let mut supported_orders = Vec::new(); + // Zero-copy allocation re-use + let original_orders = std::mem::take(orders); + for order in original_orders { + // Flashloans are disabled => drop order + if !flashloans_enabled && order.app_data.flashloan().is_some() { + removed_uids.insert(order.uid); + continue; + } + + // Metrics determined quality is unsupported => drop order + if self + .metrics + .as_ref() + .map(|metrics| metrics.get_quality(&order.uid, now)) + .is_some_and(|q| q == Quality::Unsupported) + { + removed_uids.insert(order.uid); + continue; + } + + let sell = self.get_token_quality(order.sell.token, now); + let buy = self.get_token_quality(order.buy.token, now); + match (sell, buy) { + // both tokens supported => keep order + (Quality::Supported, Quality::Supported) => supported_orders.push(order), + // at least 1 token unsupported => drop order + (Quality::Unsupported, _) | (_, Quality::Unsupported) => { + removed_uids.insert(order.uid); } - let sell = self.get_token_quality(order.sell.token, now); - let buy = self.get_token_quality(order.buy.token, now); - match (sell, buy) { - // both tokens supported => keep order - (Quality::Supported, Quality::Supported) => None, - // at least 1 token unsupported => drop order - (Quality::Unsupported, _) | (_, Quality::Unsupported) => Some(order.uid), - // sell token quality is unknown => keep order if token is supported - (Quality::Unknown, _) => { - let Some(detector) = &self.simulation_detector else { - // we can't determine quality => assume order is good - return None; - }; - let check_tokens_fut = async move { - let quality = detector.determine_sell_token_quality(order, now).await; - (order.uid, quality) - }; - token_quality_checks.push(check_tokens_fut); - None - } - // buy token quality is unknown => keep order (because we can't - // determine quality and assume it's good) - (_, Quality::Unknown) => None, + // sell token quality is unknown => keep order if token is supported, + // if simulation detector is unavailable assume it is good + (Quality::Unknown, _) => { + let Some(detector) = &self.simulation_detector else { + // We can't determine quality => assume order is good + supported_orders.push(order); + continue; + }; + // Add to quality checks to determine if supported or unsupported + token_quality_checks.push(async move { + let quality = detector.determine_sell_token_quality(&order, now).await; + (order, quality) + }); } - }) - .collect(); - - while let Some((uid, quality)) = token_quality_checks.next().await { - if quality != Quality::Supported { - removed_uids.insert(uid); + // buy token quality is unknown => keep order (because we can't + // determine quality and assume it's good) + (_, Quality::Unknown) => supported_orders.push(order), + } + } + // Wait for all quality checks to complete + while let Some((order, quality)) = token_quality_checks.next().await { + if quality == Quality::Supported { + supported_orders.push(order); + } else { + removed_uids.insert(order.uid); } } + // Replace the original orders in the auction with supported orders + *orders = supported_orders; if !removed_uids.is_empty() { - tracing::debug!( - orders = ?removed_uids, - "ignored orders with unsupported tokens" - ); + tracing::debug!(orders = ?removed_uids, "ignored orders with unsupported tokens"); } if let Some(detector) = &self.simulation_detector { detector.evict_outdated_entries(); } - - removed_uids } /// Updates the tokens quality metric for successful operation. @@ -232,7 +223,7 @@ mod tests { app_data::AppData, signature, }, - risk_detector::bad_tokens::simulation::MockSellQualityDetector, + risk_detector::bad_tokens::simulation::MockDetectorApi, }, infra::solver, util, @@ -246,7 +237,11 @@ mod tests { hash_full_app_data, }, eth_domain_types::{Asset, TokenAmount, U256}, - std::{sync::Arc, time::Duration}, + std::{ + collections::{HashMap, HashSet}, + sync::Arc, + time::Duration, + }, }; // Helper to create a mock eth:Address purely for test @@ -254,6 +249,12 @@ mod tests { eth::Address::from_slice(&[n; 20]) } + // Helper to create a mock UID purely for test + fn uid(n: u8, signer: eth::Address, valid_to: u32) -> Uid { + let order_hash = eth::B256::from([n; 32]); + Uid::from_parts(order_hash, signer, valid_to) + } + // Helper to create a mock order purely for test fn order( uid: Uid, @@ -317,19 +318,40 @@ mod tests { } } - // Helper to create a mock UID purely for test - fn uid(n: u8, signer: eth::Address, valid_to: u32) -> Uid { - let order_hash = eth::B256::from([n; 32]); - Uid::from_parts(order_hash, signer, valid_to) + // Helper to create a mock detector purely for test + fn detector( + hardcoded_tokens: HashMap, + metrics_bad_uids: HashSet, + ) -> Detector { + let metrics_detector = bad_orders::metrics::Detector::new( + 0.5, + 2, + false, + Duration::from_secs(60), + Duration::from_secs(60), + Duration::from_secs(60), + solver::Name("test_solver".into()), + ); + + let mut detector = Detector::new(hardcoded_tokens); + detector.with_metrics_detector(metrics_detector); + // Simulate + for uid in metrics_bad_uids { + detector.encoding_failed(&[uid]); + detector.encoding_failed(&[uid]); + detector.encoding_failed(&[uid]); + } + + detector } - // Helper to create a mock sell quality detector purely for test - fn sell_quality_detector(unsupported_uid: Uid) -> MockSellQualityDetector { - let mut detector = MockSellQualityDetector::new(); + // Helper to create a mock simulation detector purely for test + fn simulation_detector(unsupported_uids: HashSet) -> MockDetectorApi { + let mut detector = MockDetectorApi::new(); detector .expect_determine_sell_token_quality() - .returning(move |order, _now| { - if order.uid == unsupported_uid { + .returning(move |order, _| { + if unsupported_uids.contains(&order.uid) { Quality::Unsupported } else { Quality::Supported @@ -345,178 +367,204 @@ mod tests { #[tokio::test] async fn without_unsupported_orders_returns_empty() { let mut orders = vec![]; - let detector = Detector::new(Default::default()); + let detector = detector(HashMap::new(), HashSet::new()); detector.without_unsupported_orders(&mut orders, true).await; assert!(orders.is_empty()); } #[tokio::test] - async fn without_unsupported_orders_all_supported_orders_are_kept() { - let signer = addr(1); - let sell_token = addr(2).into(); - let buy_token = addr(3).into(); - let order_uid = uid(1, signer, u32::MAX); + async fn without_unsupported_orders_flashloan_disabled_returns_empty() { + let mut orders = vec![order( + uid(1, addr(1), u32::MAX), + addr(1), + addr(1).into(), + addr(2).into(), + u32::MAX, + true, + )]; + let detector = detector(HashMap::new(), HashSet::new()); + detector + .without_unsupported_orders(&mut orders, false) + .await; + assert!(orders.is_empty()); + } + + #[tokio::test] + async fn without_unsupported_orders_flashloan_enabled_kept() { + let id = uid(1, addr(1), u32::MAX); let mut orders = vec![order( - order_uid, - signer, - sell_token, - buy_token, + id, + addr(1), + addr(1).into(), + addr(2).into(), + u32::MAX, + true, + )]; + let detector = detector(HashMap::new(), HashSet::new()); + + // Note: This could be filtered out for other reasons later in reality + // but is contained to be true for this test + detector.without_unsupported_orders(&mut orders, true).await; + assert_eq!(orders[0].uid, id); + } + + #[tokio::test] + async fn without_unsupported_orders_metrics_bad_returns_empty() { + let unsupported_uid = uid(1, addr(1), u32::MAX); + let mut orders = vec![order( + unsupported_uid, + addr(1), + addr(1).into(), + addr(2).into(), u32::MAX, false, )]; - let detector = Detector::new(Default::default()); + let detector = detector(HashMap::new(), HashSet::from([unsupported_uid])); + detector.without_unsupported_orders(&mut orders, true).await; - assert_eq!(orders.len(), 1); - assert_eq!(orders[0].uid, order_uid); + assert!(orders.is_empty()); } #[tokio::test] - async fn without_unsupported_orders_filters_unsupported_orders_and_flashloans() { - let valid_to = u32::MAX; - let metrics_bad_uid = uid(1, addr(6), valid_to); - let token_bad_uid = uid(2, addr(7), valid_to); - let supported_uid = uid(3, addr(8), valid_to); - let unknown_sell_uid = uid(4, addr(9), valid_to); - let unknown_buy_uid = uid(5, addr(10), valid_to); - let sell_detector_unsupported_uid = uid(6, addr(11), valid_to); - let sell_detector_supported_uid = uid(7, addr(12), valid_to); - let flashloan_uid = uid(8, addr(13), valid_to); - let metrics_quality_unsupported_address = addr(3).into(); - - let orders = vec![ - order( - // metrics bad -> discard - metrics_bad_uid, - addr(6), - addr(1).into(), - addr(2).into(), - valid_to, - false, - ), - order( - // token bad -> discard - token_bad_uid, - addr(7), - metrics_quality_unsupported_address, - addr(2).into(), - valid_to, - false, - ), + async fn without_unsupported_orders_supported_kept() { + let uid = uid(1, addr(1), u32::MAX); + let sell_token = addr(1).into(); + let buy_token = addr(2).into(); + let mut orders = vec![order(uid, addr(1), sell_token, buy_token, u32::MAX, false)]; + let detector = detector( + HashMap::from([ + (sell_token, Quality::Supported), + (buy_token, Quality::Supported), + ]), + HashSet::new(), + ); + + detector.without_unsupported_orders(&mut orders, true).await; + assert_eq!(orders[0].uid, uid); + } + + #[tokio::test] + async fn without_unsupported_orders_buy_unsupported_empty() { + let uid = uid(1, addr(1), u32::MAX); + let sell_token = addr(1).into(); + let buy_token = addr(9).into(); + let mut orders = vec![order(uid, addr(1), sell_token, buy_token, u32::MAX, false)]; + let mut detector = detector( + HashMap::from([(buy_token, Quality::Unsupported)]), + HashSet::new(), + ); + detector.with_simulation_detector(simulation_detector(HashSet::new())); + + detector.without_unsupported_orders(&mut orders, true).await; + assert!(orders.is_empty()); + } + + #[tokio::test] + async fn without_unsupported_orders_mixed_orders_filters_correctly() { + let supported_uid = uid(1, addr(1), u32::MAX); + let bad_token_uid = uid(2, addr(2), u32::MAX); + let flashloan_uid = uid(3, addr(3), u32::MAX); + let metrics_bad_uid = uid(4, addr(4), u32::MAX); + let simulation_bad_uid = uid(5, addr(5), u32::MAX); + + let sell_token_supported = addr(1).into(); + let buy_token_supported = addr(2).into(); + let token_unsupported = addr(9).into(); + + // For tracking and reasoning about orders in this test: + // + // order( + // // description -> expected outcome + // uid, // if in metrics_bad_uids → discard + // // can also be used to verify order + // // inclusion/exclusion for the expected result. + // signer, // unused + // sell_token, // checked in hardcoded map in detector: + // // Supported → continue + // // Unsupported → discard + // // Unknown → may trigger simulation if detector is set + // buy_token, // checked in hardcoded map in detector: + // // Unsupported → discard + // valid_to, // unused + // flashloan, // if true and flashloans disabled → discard + // ) + // + // detector( + // HashMap::from([ + // (sell_token_supported, Quality::Supported), + // (buy_token_supported, Quality::Supported), + // (token_unsupported, Quality::Unsupported), + // ]), + // HashSet::from([metrics_bad_uid]), + // ) + // + // simulation_detector(HashSet::from([simulation_bad_uid])) + // Runs only when sell_token quality is Unknown and a simulation detector + // is set. If uid is in provided set → discard. + + let mut orders = vec![ order( - // token good -> keep + // token supported -> keep supported_uid, - addr(8), - addr(1).into(), - addr(2).into(), - valid_to, + addr(1), + sell_token_supported, + buy_token_supported, + u32::MAX, false, ), order( - // unknown sell -> keep - unknown_sell_uid, - addr(9), - addr(4).into(), - addr(2).into(), - valid_to, + // token unsupported -> discard + bad_token_uid, + addr(2), + token_unsupported, + buy_token_supported, + u32::MAX, false, ), order( - // unknown buy -> keep - unknown_buy_uid, - addr(10), - addr(1).into(), - addr(5).into(), - valid_to, - false, + // flashloan -> discard, disabled + flashloan_uid, + addr(3), + sell_token_supported, + buy_token_supported, + u32::MAX, + true, ), order( - // unsupported on quality check -> discard - sell_detector_unsupported_uid, - addr(11), - addr(6).into(), - addr(2).into(), - valid_to, + // metrics bad -> discard + metrics_bad_uid, + addr(4), + sell_token_supported, + buy_token_supported, + u32::MAX, false, ), order( - // supported on quality check -> keep - sell_detector_supported_uid, - addr(12), - addr(7).into(), - addr(2).into(), - valid_to, + // unsupported on quality check -> discard + simulation_bad_uid, + addr(5), + addr(5).into(), + buy_token_supported, + u32::MAX, false, ), - order( - // flashloan -> keep if flashloans is enabled, else discard - flashloan_uid, - addr(13), - addr(1).into(), - addr(2).into(), - valid_to, - true, - ), ]; - let mut orders_flashloans_disabled = orders.clone(); - let mut orders_flashloans_enabled = orders.clone(); - - let metrics_detector = bad_orders::metrics::Detector::new( - 0.5, - 2, - false, - Duration::from_secs(60), - Duration::from_secs(60), - Duration::from_secs(60), - solver::Name("test_solver".into()), + let mut detector = detector( + HashMap::from([ + (sell_token_supported, Quality::Supported), + (buy_token_supported, Quality::Supported), + (token_unsupported, Quality::Unsupported), + ]), + HashSet::from([metrics_bad_uid]), ); - let mut detector_config = HashMap::new(); - detector_config.insert(metrics_quality_unsupported_address, Quality::Unsupported); - let mut detector = Detector::new(detector_config); - detector.with_metrics_detector(metrics_detector); - // Simulate repeated metrics failure for order with metrics_bad_uid - detector.encoding_failed(&[metrics_bad_uid]); - detector.encoding_failed(&[metrics_bad_uid]); - detector.encoding_failed(&[metrics_bad_uid]); - detector.with_simulation_detector(sell_quality_detector(sell_detector_unsupported_uid)); + detector.with_simulation_detector(simulation_detector(HashSet::from([simulation_bad_uid]))); - // Test with flashloans disabled detector - .without_unsupported_orders(&mut orders_flashloans_disabled, false) + .without_unsupported_orders(&mut orders, false) .await; - let remaining_uids = orders_flashloans_disabled - .iter() - .map(|order| order.uid) - .collect::>(); - - assert_eq!( - remaining_uids, - vec![ - supported_uid, - unknown_sell_uid, - unknown_buy_uid, - sell_detector_supported_uid, - ] - ); - // Test with flashloans enabled - detector - .without_unsupported_orders(&mut orders_flashloans_enabled, true) - .await; - let remaining_uids = orders_flashloans_enabled - .iter() - .map(|order| order.uid) - .collect::>(); - - assert_eq!( - remaining_uids, - vec![ - supported_uid, - unknown_sell_uid, - unknown_buy_uid, - sell_detector_supported_uid, - flashloan_uid, - ] - ); + assert_eq!(orders.len(), 1); + assert_eq!(orders[0].uid, supported_uid); } } diff --git a/crates/driver/src/domain/quote.rs b/crates/driver/src/domain/quote.rs index df2452f0d4..0e937f11dc 100644 --- a/crates/driver/src/domain/quote.rs +++ b/crates/driver/src/domain/quote.rs @@ -145,7 +145,7 @@ impl Order { .fake_auction(eth, tokens, solver.quote_using_limit_orders()) .await?; risk_detector - .filter_unsupported_orders_in_auction_orders(&mut auction.orders) + .without_unsupported_orders(&mut auction.orders, solver.config().flashloans_enabled) .await; if auction.orders.is_empty() { return Err(QuotingFailed::UnsupportedToken.into()); diff --git a/crates/driver/src/infra/api/error.rs b/crates/driver/src/infra/api/error.rs index fab31fb042..26c8425a66 100644 --- a/crates/driver/src/infra/api/error.rs +++ b/crates/driver/src/infra/api/error.rs @@ -29,7 +29,7 @@ enum Kind { TokenTemporarilySuspended, InsufficientLiquidity, CustomSolverError, - Join, + InternalError, } #[derive(Debug, Serialize)] @@ -70,10 +70,10 @@ impl From for (axum::http::StatusCode, axum::Json) { Kind::TokenTemporarilySuspended => "Token is temporarily suspended from trading", Kind::InsufficientLiquidity => "Insufficient liquidity for the requested trade size", Kind::CustomSolverError => "Solver returned a custom error", - Kind::Join => "Internal task failure", + Kind::InternalError => "Internal error occurred", }; let status = match value { - Kind::Join => axum::http::StatusCode::INTERNAL_SERVER_ERROR, + Kind::InternalError => axum::http::StatusCode::INTERNAL_SERVER_ERROR, _ => axum::http::StatusCode::BAD_REQUEST, }; ( @@ -152,7 +152,7 @@ impl From for (axum::http::StatusCode, axum::Json) { competition::Error::TooManyPendingSettlements => Kind::TooManyPendingSettlements, competition::Error::NoValidOrdersFound => Kind::NoValidOrders, competition::Error::MalformedRequest => Kind::MalformedRequest, - competition::Error::Join(_) => Kind::Join, + competition::Error::InternalError(_) => Kind::InternalError, }; error.into() } diff --git a/crates/driver/src/infra/observe/mod.rs b/crates/driver/src/infra/observe/mod.rs index 7ff8851e3e..817f702366 100644 --- a/crates/driver/src/infra/observe/mod.rs +++ b/crates/driver/src/infra/observe/mod.rs @@ -459,7 +459,7 @@ fn competition_error(err: &competition::Error) -> &'static str { competition::Error::TooManyPendingSettlements => "TooManyPendingSettlements", competition::Error::NoValidOrdersFound => "NoValidOrdersFound", competition::Error::MalformedRequest => "MalformedRequest", - competition::Error::Join(_) => "Join", + competition::Error::InternalError(_) => "InternalError", } } From efc6d9047b7ea9001b0cb618fa700e33c03fc523 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Tue, 5 May 2026 23:40:06 +0200 Subject: [PATCH 12/15] fix: comment --- crates/driver/src/domain/competition/risk_detector/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index 94563c6bda..32b1949d33 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -335,7 +335,7 @@ mod tests { let mut detector = Detector::new(hardcoded_tokens); detector.with_metrics_detector(metrics_detector); - // Simulate + // Simulate multiple encoding failures to mark uid bad for uid in metrics_bad_uids { detector.encoding_failed(&[uid]); detector.encoding_failed(&[uid]); From e01df58d9900b9bee345f0d6ee043224dc002b03 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Tue, 5 May 2026 23:49:49 +0200 Subject: [PATCH 13/15] fix: comment --- crates/driver/src/domain/competition/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index 5714107c81..eefb8357a9 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -377,7 +377,7 @@ impl Competition { ), ); let auction = auction.map_err(|err| { - tracing::error!(?err, "auction preparation task failed"); + tracing::error!(?err, "order filtering task failed"); Error::InternalError(err.to_string()) })?; let liquidity = liquidity.map_err(|err| { From c06963ad66cb6f589937cc32a3bdb2387a403066 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Wed, 6 May 2026 22:59:20 +0200 Subject: [PATCH 14/15] refactor: separate concerns in without_unsupported_orders --- .../risk_detector/bad_tokens/simulation.rs | 2 +- .../domain/competition/risk_detector/mod.rs | 174 +++++++++++++----- 2 files changed, 128 insertions(+), 48 deletions(-) diff --git a/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs b/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs index acd9410e01..3ae4866779 100644 --- a/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs +++ b/crates/driver/src/domain/competition/risk_detector/bad_tokens/simulation.rs @@ -132,7 +132,7 @@ impl Detector { } } -impl std::ops::Deref for Detector { +impl Deref for Detector { type Target = Cache; fn deref(&self) -> &Self::Target { diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index 32b1949d33..a8613866e1 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -54,6 +54,12 @@ pub enum Quality { Unknown, } +enum OrderQuality { + Supported, + Unsupported, + ShouldRequireSellTokenSimulation, +} + #[derive(Default)] pub struct Detector { /// manually configured list of supported and unsupported tokens. Only @@ -87,8 +93,7 @@ impl Detector { self } - /// Performs flashloan filtering and removes all unsupported orders from the - /// auction. + /// Filters out unsupported and disallowed orders from the auction. #[instrument(skip_all)] pub async fn without_unsupported_orders( &self, @@ -96,60 +101,110 @@ impl Detector { flashloans_enabled: bool, ) { let now = Instant::now(); - let mut token_quality_checks = FuturesUnordered::new(); - // List of orders that have been removed let mut removed_uids: HashSet = HashSet::new(); - // List of orders that will be retained + // Choose filtering path depending on whether simulation detector is set + let supported_orders = match &self.simulation_detector { + Some(detector) => { + let supported_orders = self + .supported_orders_with_detector( + orders.drain(..), + flashloans_enabled, + now, + &mut removed_uids, + detector.as_ref(), + ) + .await; + detector.evict_outdated_entries(); + supported_orders + } + None => self.supported_orders_without_detector( + orders.drain(..), + flashloans_enabled, + now, + &mut removed_uids, + ), + }; + if !removed_uids.is_empty() { + tracing::debug!(orders = ?removed_uids, "ignored orders with unsupported tokens"); + } + // Replace the original orders in the auction with supported orders + *orders = supported_orders; + } + + /// Filters orders using only static checks + fn supported_orders_without_detector( + &self, + orders: impl Iterator, + flashloans_enabled: bool, + now: Instant, + removed_uids: &mut HashSet, + ) -> Vec { let mut supported_orders = Vec::new(); - // Zero-copy allocation re-use - let original_orders = std::mem::take(orders); - for order in original_orders { + for order in orders { // Flashloans are disabled => drop order - if !flashloans_enabled && order.app_data.flashloan().is_some() { + if Self::is_disabled_flashloan_order(&order, flashloans_enabled) { removed_uids.insert(order.uid); continue; } + // Determine whether to keep or drop order + match self.order_quality(&order, now) { + OrderQuality::Supported | OrderQuality::ShouldRequireSellTokenSimulation => { + supported_orders.push(order); + } + OrderQuality::Unsupported => { + removed_uids.insert(order.uid); + } + } + } + supported_orders + } + + /// Filters orders and uses simulation for unknown sell-token quality. + async fn supported_orders_with_detector( + &self, + orders: impl Iterator, + flashloans_enabled: bool, + now: Instant, + removed_uids: &mut HashSet, + detector: &dyn DetectorApi, + ) -> Vec { + let mut supported_orders = Vec::new(); + let mut orders_requiring_simulation = Vec::new(); - // Metrics determined quality is unsupported => drop order - if self - .metrics - .as_ref() - .map(|metrics| metrics.get_quality(&order.uid, now)) - .is_some_and(|q| q == Quality::Unsupported) - { + for order in orders { + // Flashloans are disabled => drop order + if Self::is_disabled_flashloan_order(&order, flashloans_enabled) { removed_uids.insert(order.uid); continue; } - - let sell = self.get_token_quality(order.sell.token, now); - let buy = self.get_token_quality(order.buy.token, now); - match (sell, buy) { - // both tokens supported => keep order - (Quality::Supported, Quality::Supported) => supported_orders.push(order), - // at least 1 token unsupported => drop order - (Quality::Unsupported, _) | (_, Quality::Unsupported) => { + // Determine whether to keep, simulate or drop order + match self.order_quality(&order, now) { + OrderQuality::Supported => { + supported_orders.push(order); + } + OrderQuality::Unsupported => { removed_uids.insert(order.uid); } - // sell token quality is unknown => keep order if token is supported, - // if simulation detector is unavailable assume it is good - (Quality::Unknown, _) => { - let Some(detector) = &self.simulation_detector else { - // We can't determine quality => assume order is good - supported_orders.push(order); - continue; - }; - // Add to quality checks to determine if supported or unsupported - token_quality_checks.push(async move { - let quality = detector.determine_sell_token_quality(&order, now).await; - (order, quality) - }); + OrderQuality::ShouldRequireSellTokenSimulation => { + orders_requiring_simulation.push(order); } - // buy token quality is unknown => keep order (because we can't - // determine quality and assume it's good) - (_, Quality::Unknown) => supported_orders.push(order), } } + + // If no orders require simulation, return early + if orders_requiring_simulation.is_empty() { + return supported_orders; + } + + let mut token_quality_checks = FuturesUnordered::new(); + for order in orders_requiring_simulation { + // Add to quality checks to determine if supported or unsupported + token_quality_checks.push(async move { + let quality = detector.determine_sell_token_quality(&order, now).await; + (order, quality) + }); + } // Wait for all quality checks to complete while let Some((order, quality)) = token_quality_checks.next().await { if quality == Quality::Supported { @@ -158,18 +213,41 @@ impl Detector { removed_uids.insert(order.uid); } } - // Replace the original orders in the auction with supported orders - *orders = supported_orders; + supported_orders + } - if !removed_uids.is_empty() { - tracing::debug!(orders = ?removed_uids, "ignored orders with unsupported tokens"); + /// Classifies an order using metrics and static checks. + fn order_quality(&self, order: &Order, now: Instant) -> OrderQuality { + // Metrics determined quality is unsupported => drop order + if self + .metrics + .as_ref() + .map(|metrics| metrics.get_quality(&order.uid, now)) + .is_some_and(|q| q == Quality::Unsupported) + { + return OrderQuality::Unsupported; } - - if let Some(detector) = &self.simulation_detector { - detector.evict_outdated_entries(); + let sell = self.get_token_quality(order.sell.token, now); + let buy = self.get_token_quality(order.buy.token, now); + match (sell, buy) { + // both tokens supported => keep order + (Quality::Supported, Quality::Supported) => OrderQuality::Supported, + // at least 1 token unsupported => drop order + (Quality::Unsupported, _) | (_, Quality::Unsupported) => OrderQuality::Unsupported, + // sell token quality is unknown => should require simulation detector, + // assume it is good if simulation detector is unavailable + (Quality::Unknown, _) => OrderQuality::ShouldRequireSellTokenSimulation, + // buy token quality is unknown => keep order (because we can't + // determine quality and assume it's good) + (_, Quality::Unknown) => OrderQuality::Supported, } } + /// Returns true if flashloans are disabled and the order uses one. + fn is_disabled_flashloan_order(order: &Order, flashloans_enabled: bool) -> bool { + !flashloans_enabled && order.app_data.flashloan().is_some() + } + /// Updates the tokens quality metric for successful operation. pub fn encoding_succeeded(&self, orders: &[Uid]) { if let Some(metrics) = &self.metrics { @@ -184,6 +262,8 @@ impl Detector { } } + /// Returns the quality of a token using hardcoded configuration or the + /// simulation detector. fn get_token_quality(&self, token: eth::TokenAddress, now: Instant) -> Quality { match self.hardcoded.get(&token) { None | Some(Quality::Unknown) => (), From 60e766f79ad2cf796221511f0271292bc4cabc72 Mon Sep 17 00:00:00 2001 From: metalurgical <97008724+metalurgical@users.noreply.github.com> Date: Thu, 7 May 2026 12:31:03 +0200 Subject: [PATCH 15/15] update --- .../domain/competition/risk_detector/mod.rs | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/crates/driver/src/domain/competition/risk_detector/mod.rs b/crates/driver/src/domain/competition/risk_detector/mod.rs index a8613866e1..da68d5f52e 100644 --- a/crates/driver/src/domain/competition/risk_detector/mod.rs +++ b/crates/driver/src/domain/competition/risk_detector/mod.rs @@ -6,13 +6,13 @@ //! was simply built with a buggy compiler which makes it incompatible //! with the settlement contract (see ). //! -//! Additionally there are some heuristics to detect when an +//! Additionally, there are some heuristics to detect when an //! order itself is somehow broken or causes issues and slipped through //! other detection mechanisms. One big error case is orders adjusting //! debt postions in lending protocols. While pre-checks might correctly //! detect that the EIP 1271 signature is valid the transfer of the token //! would fail because the user's debt position is not collateralized enough. -//! In other words the bad order detection is a last fail safe in case +//! In other words the bad order detection is a last fail-safe in case //! we were not able to predict issues with orders and pre-emptively //! filter them out of the auction. @@ -55,9 +55,12 @@ pub enum Quality { } enum OrderQuality { - Supported, - Unsupported, - ShouldRequireSellTokenSimulation, + /// Order should be kept + Good, + /// Order should not be kept + Bad, + /// Order needs to be evaluated further + Indeterminate, } #[derive(Default)] @@ -149,10 +152,10 @@ impl Detector { } // Determine whether to keep or drop order match self.order_quality(&order, now) { - OrderQuality::Supported | OrderQuality::ShouldRequireSellTokenSimulation => { + OrderQuality::Good | OrderQuality::Indeterminate => { supported_orders.push(order); } - OrderQuality::Unsupported => { + OrderQuality::Bad => { removed_uids.insert(order.uid); } } @@ -170,7 +173,7 @@ impl Detector { detector: &dyn DetectorApi, ) -> Vec { let mut supported_orders = Vec::new(); - let mut orders_requiring_simulation = Vec::new(); + let mut token_quality_checks = FuturesUnordered::new(); for order in orders { // Flashloans are disabled => drop order @@ -180,31 +183,25 @@ impl Detector { } // Determine whether to keep, simulate or drop order match self.order_quality(&order, now) { - OrderQuality::Supported => { + OrderQuality::Good => { supported_orders.push(order); } - OrderQuality::Unsupported => { + OrderQuality::Bad => { removed_uids.insert(order.uid); } - OrderQuality::ShouldRequireSellTokenSimulation => { - orders_requiring_simulation.push(order); + OrderQuality::Indeterminate => { + // Add to quality checks to determine if supported or unsupported + token_quality_checks.push(async move { + let quality = detector.determine_sell_token_quality(&order, now).await; + (order, quality) + }); } } } - // If no orders require simulation, return early - if orders_requiring_simulation.is_empty() { + if token_quality_checks.is_empty() { return supported_orders; } - - let mut token_quality_checks = FuturesUnordered::new(); - for order in orders_requiring_simulation { - // Add to quality checks to determine if supported or unsupported - token_quality_checks.push(async move { - let quality = detector.determine_sell_token_quality(&order, now).await; - (order, quality) - }); - } // Wait for all quality checks to complete while let Some((order, quality)) = token_quality_checks.next().await { if quality == Quality::Supported { @@ -225,21 +222,21 @@ impl Detector { .map(|metrics| metrics.get_quality(&order.uid, now)) .is_some_and(|q| q == Quality::Unsupported) { - return OrderQuality::Unsupported; + return OrderQuality::Bad; } let sell = self.get_token_quality(order.sell.token, now); let buy = self.get_token_quality(order.buy.token, now); match (sell, buy) { // both tokens supported => keep order - (Quality::Supported, Quality::Supported) => OrderQuality::Supported, + (Quality::Supported, Quality::Supported) => OrderQuality::Good, // at least 1 token unsupported => drop order - (Quality::Unsupported, _) | (_, Quality::Unsupported) => OrderQuality::Unsupported, + (Quality::Unsupported, _) | (_, Quality::Unsupported) => OrderQuality::Bad, // sell token quality is unknown => should require simulation detector, // assume it is good if simulation detector is unavailable - (Quality::Unknown, _) => OrderQuality::ShouldRequireSellTokenSimulation, + (Quality::Unknown, _) => OrderQuality::Indeterminate, // buy token quality is unknown => keep order (because we can't // determine quality and assume it's good) - (_, Quality::Unknown) => OrderQuality::Supported, + (_, Quality::Unknown) => OrderQuality::Good, } }