From 1c6826cd6e39d508f2db40fbb9e1c6b67a059c41 Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Sat, 25 Jul 2026 19:38:33 +0000 Subject: [PATCH 1/9] assemble orders in parallel --- crates/autopilot/src/run.rs | 1 - crates/autopilot/src/solvable_orders.rs | 1189 +++++------------ crates/bad-tokens/src/list_based.rs | 6 +- crates/configs/src/autopilot/mod.rs | 9 - crates/order-validation/src/banned/cached.rs | 7 +- crates/order-validation/src/banned/mod.rs | 18 +- .../src/native_price_cache.rs | 95 +- crates/shared/src/order_validation.rs | 5 +- 8 files changed, 454 insertions(+), 876 deletions(-) diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs index 52d9f8502a..a4e0ac171b 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -482,7 +482,6 @@ pub async fn run(config: Configuration, shutdown_controller: ShutdownController) *eth.contracts().weth().address(), ), config.surplus_capturing_jit_order_owners, - config.native_price_timeout, *eth.contracts().settlement().address(), config.disable_order_balance_filter, ); diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index ad6e33573f..ae7718093a 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -5,7 +5,11 @@ use { infra::{self, banned}, }, account_balances::{BalanceFetching, Query}, - alloy::primitives::{Address, U256}, + alloy::primitives::{ + Address, + U256, + map::{AddressHashSet, FbBuildHasher}, + }, anyhow::{Context, Result}, bad_tokens::list_based::DenyListedTokens, database::order_events::{ @@ -22,22 +26,28 @@ use { }, }, futures::FutureExt, - itertools::Itertools, model::{ - order::{Order, OrderClass, OrderUid}, + order::{Order, OrderUid}, signature::Signature, time::now_in_epoch_seconds, }, price_estimation::{native::to_normalized_price, native_price_cache::NativePriceUpdater}, - prometheus::{Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec}, + prometheus::{ + Histogram, + HistogramVec, + IntCounter, + IntCounterVec, + IntGauge, + IntGaugeVec, + core::{AtomicU64, GenericGauge}, + }, shared::remaining_amounts, std::{ - collections::{BTreeMap, HashMap, HashSet}, + collections::{HashMap, HashSet}, future::Future, sync::Arc, time::{Duration, Instant}, }, - strum::VariantNames, tokio::sync::Mutex, tracing::instrument, }; @@ -69,9 +79,8 @@ pub struct Metrics { #[metric(labels("class"))] auction_candidate_orders: IntGaugeVec, - /// Auction solvable orders grouped by class. - #[metric(labels("class"))] - auction_solvable_orders: IntGaugeVec, + /// Auction solvable orders. + auction_solvable_orders: GenericGauge, /// Auction filtered orders grouped by class. #[metric(labels("reason"))] @@ -88,37 +97,26 @@ impl Metrics { #[instrument(skip_all)] fn track_filtered_orders(reason: OrderFilterReason, invalid_orders: &[OrderUid]) { - if invalid_orders.is_empty() { - return; - } - Metrics::get() .auction_filtered_orders .with_label_values(&[reason.as_str()]) .set(i64::try_from(invalid_orders.len()).unwrap_or(i64::MAX)); - tracing::debug!( - %reason, - count = invalid_orders.len(), - orders = ?invalid_orders, "filtered orders" - ); + if !invalid_orders.is_empty() { + // clone and print in background task? + tracing::debug!( + %reason, + count = invalid_orders.len(), + orders = ?invalid_orders, "filtered orders" + ); + } } #[instrument(skip_all)] - fn track_orders_in_final_auction(orders: &[&Order]) { + fn track_orders_in_final_auction(orders: &[domain::Order]) { let metrics = Metrics::get(); metrics.auction_creations.inc(); - - let remaining_counts = orders - .iter() - .counts_by(|order| order.metadata.class.as_ref()); - for class in OrderClass::VARIANTS { - let count = remaining_counts.get(class).copied().unwrap_or_default(); - metrics - .auction_solvable_orders - .with_label_values(&[class]) - .set(i64::try_from(count).unwrap_or(i64::MAX)); - } + metrics.auction_solvable_orders.set(orders.len() as u64); } } @@ -139,14 +137,11 @@ pub struct SolvableOrdersCache { weth: Address, protocol_fees: domain::ProtocolFees, surplus_capturing_jit_order_owners: Vec
, - native_price_timeout: Duration, settlement_contract: Address, disable_order_balance_filter: bool, wrapper_cache: app_data::WrapperCache, } -type Balances = HashMap; - struct Inner { auction: domain::RawAuctionData, solvable_orders: boundary::SolvableOrders, @@ -164,7 +159,6 @@ impl SolvableOrdersCache { weth: Address, protocol_fees: domain::ProtocolFees, surplus_capturing_jit_order_owners: Vec
, - native_price_timeout: Duration, settlement_contract: Address, disable_order_balance_filter: bool, ) -> Arc { @@ -179,7 +173,6 @@ impl SolvableOrdersCache { weth, protocol_fees, surplus_capturing_jit_order_owners, - native_price_timeout, settlement_contract, disable_order_balance_filter, wrapper_cache: app_data::WrapperCache::new(20_000), @@ -210,98 +203,88 @@ impl SolvableOrdersCache { let db_solvable_orders = self.get_solvable_orders().await?; tracing::trace!("fetched solvable orders from db"); - let orders: Vec<&Order> = db_solvable_orders - .orders - .values() - .map(|order| order.as_ref()) - .collect(); - - let mut invalid_order_uids = HashMap::new(); - let mut filtered_order_events: Vec<(OrderUid, OrderFilterReason)> = Vec::new(); - - let balance_filter_exempt_orders: HashSet<_> = orders - .iter() - .filter(|order| { - self.wrapper_cache.has_wrappers( - &order.data.app_data, - order.metadata.full_app_data.as_deref(), - ) - }) - .map(|order| order.metadata.uid) - .collect(); - - let (balances, orders, in_flight) = { - let queries = orders - .iter() - .map(|o| Query::from_order(o)) - .collect::>(); - tokio::join!( - self.fetch_balances(queries), - self.filter_invalid_orders(orders, &mut invalid_order_uids), - self.fetch_in_flight_orders(block), - ) - }; - - // Remove in-flight orders - already won a previous auction, being settled - // on-chain. - let (orders, removed) = filter_out_in_flight_orders(orders, &in_flight); - Metrics::track_filtered_orders(InFlight, &removed); - filtered_order_events.extend(removed.into_iter().map(|uid| (uid, InFlight))); - // It's possible that some orders got marked as invalid due to missing balance - // or so, but the order is perfectly fine if it's in-flight - invalid_order_uids.retain(|uid, _| !in_flight.contains(uid)); - - let orders = if self.disable_order_balance_filter { - orders - } else { - let (orders, removed) = orders_with_balance( - orders, - &balances, - self.settlement_contract, - &balance_filter_exempt_orders, - ); - Metrics::track_filtered_orders(InsufficientBalance, &removed); - invalid_order_uids.extend(removed.into_iter().map(|uid| (uid, InsufficientBalance))); - - let (orders, removed) = filter_dust_orders(orders, &balances); - Metrics::track_filtered_orders(DustOrder, &removed); - filtered_order_events.extend(removed.into_iter().map(|uid| (uid, DustOrder))); - - orders - }; - - // create auction - let (orders, removed, mut prices) = self - .timed_future( - "get_orders_with_native_prices", - get_orders_with_native_prices( - orders, - &self.native_price_estimator, - self.native_price_timeout, - ), - ) - .await; - tracing::trace!("fetched native prices for solvable orders"); + // Phase 1: single-pass sync pre-filter that also collects everything + // needed for the concurrent I/O in phase 2. + let mut prices = self.native_price_estimator.cached_prices(); // WETH's native price is 1 by definition — insert it directly to // support ETH wrap when required. prices .entry(self.weth) .or_insert_with(|| to_normalized_price(1.0).unwrap()); - Metrics::track_filtered_orders(MissingNativePrice, &removed); - filtered_order_events.extend(removed.into_iter().map(|uid| (uid, MissingNativePrice))); - Metrics::track_orders_in_final_auction(&orders); + let capacity_hint = db_solvable_orders.orders.len(); + // filtered orders + let mut unsupported_uids = Vec::new(); + let mut presig_uids = Vec::new(); + let mut missing_price_uids = Vec::new(); + + // data needed for filtering logic in next phase + let mut traders = AddressHashSet::default(); + let mut balance_queries = Vec::with_capacity(capacity_hint); + let mut traded_tokens = AddressHashSet::default(); + let mut balance_filter_exempt = HashSet::>::default(); + let mut survivors: Vec<&Order> = Vec::with_capacity(capacity_hint); + + for order in db_solvable_orders.orders.values() { + let order = order.as_ref(); + let uid = order.metadata.uid; + + // store tokens even for discarded orders to later inform the native price + // cache about ALL the tokens we need prices for + traded_tokens.insert(order.data.sell_token); + traded_tokens.insert(order.data.buy_token); + + if is_unsupported(order, &self.deny_listed_tokens) { + unsupported_uids.push(uid); + continue; + } + if is_presig_pending(order) { + presig_uids.push(uid); + continue; + } + if !prices.contains_key(&order.data.sell_token) + || !prices.contains_key(&order.data.buy_token) + { + missing_price_uids.push(uid); + continue; + } - if store_events { - self.store_events_by_reason(invalid_order_uids, OrderEventLabel::Invalid); - self.store_events_by_reason(filtered_order_events, OrderEventLabel::Filtered); + traders.insert(order.metadata.owner); + if let Some(receiver) = order.data.receiver { + traders.insert(receiver); + } + balance_queries.push(Query::from_order(order)); + if self.wrapper_cache.has_wrappers( + &order.data.app_data, + order.metadata.full_app_data.as_deref(), + ) { + balance_filter_exempt.insert(uid); + } + survivors.push(order); } + Metrics::track_filtered_orders(UnsupportedToken, &unsupported_uids); + Metrics::track_filtered_orders(InvalidSignature, &presig_uids); + Metrics::track_filtered_orders(MissingNativePrice, &missing_price_uids); + + // at this point we know all relevant tokens and tell the native price + // cache to have them ready for the next auction + self.native_price_estimator + .schedule_token_updates(traded_tokens); + + // Phase 2: concurrent I/O based on phase-1 outputs. + let (in_flight, banned_set, balances) = tokio::join!( + self.fetch_in_flight_orders(block), + self.timed_future("banned_user_filtering", self.banned_users.banned(traders)), + self.fetch_balances(balance_queries), + ); + + // Phase 3: final pass using data from phase-2 // Exclude any owner that already has an order in-flight (i.e. won a previous // auction and is being settled on-chain). A surplus-capturing JIT order created // on its behalf could conflict with the settling order, so we drop the owner // from this auction until the in-flight order clears. - let in_flight_owners: HashSet
= in_flight + let in_flight_owners: AddressHashSet = in_flight .iter() .map(|uid| domain::OrderUid(uid.0).owner()) .collect(); @@ -311,21 +294,96 @@ impl SolvableOrdersCache { .filter(|owner| !in_flight_owners.contains(*owner)) .copied() .collect(); + + // filtered orders + let mut in_flight_removed = Vec::new(); + let mut banned_removed = Vec::new(); + let mut balance_removed = Vec::new(); + let mut dust_removed = Vec::new(); + + let final_orders = survivors + .into_iter() + .filter_map(|order| { + let uid = order.metadata.uid; + if in_flight.contains(&uid) { + in_flight_removed.push(uid); + return None; + } + let is_banned = banned_set.contains(&order.metadata.owner) + || order.data.receiver.is_some_and(|r| banned_set.contains(&r)); + if is_banned { + banned_removed.push(uid); + return None; + } + if !self.disable_order_balance_filter { + let balance = *balances.get(&Query::from_order(order))?; + + if !passes_balance( + order, + balance, + self.settlement_contract, + &balance_filter_exempt, + ) { + balance_removed.push(uid); + return None; + } + if !passes_dust(order, balance) { + dust_removed.push(uid); + return None; + } + } + + let quote = db_solvable_orders + .quotes + .get(&order.metadata.uid.into()) + .map(|quote| quote.as_ref().clone()); + let final_order = + self.protocol_fees + .apply(order, quote, &surplus_capturing_jit_order_owners); + Some(final_order) + }) + .collect::>(); + + Metrics::track_filtered_orders(InFlight, &in_flight_removed); + Metrics::track_filtered_orders(BannedUser, &banned_removed); + Metrics::track_filtered_orders(InsufficientBalance, &balance_removed); + Metrics::track_filtered_orders(DustOrder, &dust_removed); + + Metrics::track_orders_in_final_auction(&final_orders); + + if store_events { + let mut invalid_order_uids: HashMap> = + HashMap::with_hasher(FbBuildHasher::default()); + invalid_order_uids.extend( + unsupported_uids + .into_iter() + .map(|uid| (uid, UnsupportedToken)), + ); + invalid_order_uids.extend(presig_uids.into_iter().map(|uid| (uid, InvalidSignature))); + invalid_order_uids.extend(banned_removed.into_iter().map(|uid| (uid, BannedUser))); + invalid_order_uids.extend( + balance_removed + .into_iter() + .map(|uid| (uid, InsufficientBalance)), + ); + + let mut filtered_order_events: Vec<(OrderUid, OrderFilterReason)> = Vec::new(); + filtered_order_events + .extend(in_flight_removed.iter().copied().map(|uid| (uid, InFlight))); + filtered_order_events.extend(dust_removed.into_iter().map(|uid| (uid, DustOrder))); + filtered_order_events.extend( + missing_price_uids + .into_iter() + .map(|uid| (uid, MissingNativePrice)), + ); + + self.store_events_by_reason(invalid_order_uids, OrderEventLabel::Invalid); + self.store_events_by_reason(filtered_order_events, OrderEventLabel::Filtered); + } + let auction = domain::RawAuctionData { block, - orders: tracing::info_span!("assemble_orders").in_scope(|| { - orders - .into_iter() - .map(|order| { - let quote = db_solvable_orders - .quotes - .get(&order.metadata.uid.into()) - .map(|quote| quote.as_ref().clone()); - self.protocol_fees - .apply(order, quote, &surplus_capturing_jit_order_owners) - }) - .collect() - }), + orders: final_orders, prices: prices .into_iter() .map(|(key, value)| Price::try_new(value.into()).map(|price| (key.into(), price))) @@ -345,7 +403,7 @@ impl SolvableOrdersCache { Ok(()) } - async fn fetch_in_flight_orders(&self, block: u64) -> HashSet { + async fn fetch_in_flight_orders(&self, block: u64) -> HashSet> { self.persistence .fetch_in_flight_orders(block) .await @@ -424,43 +482,6 @@ impl SolvableOrdersCache { Ok(orders) } - /// Executed orders filtering in parallel. - #[instrument(skip_all)] - async fn filter_invalid_orders<'a>( - &self, - mut orders: Vec<&'a Order>, - invalid_order_uids: &mut HashMap, - ) -> Vec<&'a Order> { - let presignature_pending_orders = find_presignature_pending_orders(&orders); - - let unsupported_token_orders = find_unsupported_tokens(&orders, &self.deny_listed_tokens); - let banned_user_orders = self - .timed_future( - "banned_user_filtering", - find_banned_user_orders(&orders, &self.banned_users), - ) - .await; - tracing::trace!("filtered invalid orders"); - - Metrics::track_filtered_orders(BannedUser, &banned_user_orders); - Metrics::track_filtered_orders(InvalidSignature, &presignature_pending_orders); - Metrics::track_filtered_orders(UnsupportedToken, &unsupported_token_orders); - invalid_order_uids.extend(banned_user_orders.into_iter().map(|uid| (uid, BannedUser))); - invalid_order_uids.extend( - presignature_pending_orders - .into_iter() - .map(|uid| (uid, InvalidSignature)), - ); - invalid_order_uids.extend( - unsupported_token_orders - .into_iter() - .map(|uid| (uid, UnsupportedToken)), - ); - - orders.retain(|order| !invalid_order_uids.contains_key(&order.metadata.uid)); - orders - } - pub fn track_auction_update(&self, result: &str) { Metrics::get() .auction_update @@ -497,230 +518,70 @@ impl SolvableOrdersCache { } } -/// Finds all orders whose owners or receivers are in the set of "banned" -/// users. -async fn find_banned_user_orders(orders: &[&Order], banned_users: &banned::Users) -> Vec { - let banned = banned_users - .banned( - orders - .iter() - .flat_map(|order| std::iter::once(order.metadata.owner).chain(order.data.receiver)), - ) - .await; - orders - .iter() - .filter_map(|order| { - std::iter::once(order.metadata.owner) - .chain(order.data.receiver) - .any(|addr| banned.contains(&addr)) - .then_some(order.metadata.uid) - }) - .collect() -} - -async fn get_native_prices( - tokens: HashSet
, - native_price_estimator: &NativePriceUpdater, - timeout: Duration, -) -> BTreeMap { - native_price_estimator - .update_tokens_and_fetch_prices(tokens, timeout) - .await - .into_iter() - .flat_map(|(token, result)| { - let price = to_normalized_price(result.ok()?)?; - Some((token, price)) - }) - .collect() +/// Returns true if either of the order's tokens is on the deny list. +fn is_unsupported(order: &Order, deny_listed_tokens: &DenyListedTokens) -> bool { + deny_listed_tokens.contains(&order.data.sell_token) + || deny_listed_tokens.contains(&order.data.buy_token) } -/// Finds orders with pending presignatures. EIP-1271 signature validation is -/// skipped entirely - the driver validates signatures before settlement. -fn find_presignature_pending_orders(orders: &[&Order]) -> Vec { - orders - .iter() - .filter(|order| { - matches!( - order.metadata.status, - model::order::OrderStatus::PresignaturePending - ) - }) - .map(|order| order.metadata.uid) - .collect() +/// Returns true if the order is waiting for a pre-signature. EIP-1271 orders +/// are validated by the driver before settlement, so we don't check them here. +fn is_presig_pending(order: &Order) -> bool { + matches!( + order.metadata.status, + model::order::OrderStatus::PresignaturePending + ) } -/// Removes orders that can't possibly be settled because there isn't enough -/// balance. -#[instrument(skip_all)] -fn orders_with_balance<'a>( - mut orders: Vec<&'a Order>, - balances: &Balances, +/// Returns true if the order has sufficient balance to be settled. EIP-1271 +/// orders and orders exempt via a wrapper interaction bypass the check. +fn passes_balance( + order: &Order, + balance: U256, settlement_contract: Address, - filter_bypass_orders: &HashSet, -) -> (Vec<&'a Order>, Vec) { - // Prefer newer orders over older ones. - orders.sort_by_key(|order| std::cmp::Reverse(order.metadata.creation_date)); - let mut filtered_orders = vec![]; - let keep = |order: &Order| { - // Skip balance check for all EIP-1271 orders (they can rely on pre-interactions - // to unlock funds) or orders with wrappers (wrappers produce the required - // balance at settlement time). - if matches!(order.signature, Signature::Eip1271(_)) - || filter_bypass_orders.contains(&order.metadata.uid) - { - return true; - } - - if order.data.receiver.as_ref() == Some(&settlement_contract) { - // TODO: replace with proper detection logic - // for now we assume that all orders with the settlement contract - // as the receiver are flashloan orders which unlock the necessary - // funds via a pre-interaction that can't succeed in our balance - // fetching simulation logic. - return true; - } + exempt: &HashSet>, +) -> bool { + // EIP-1271 orders can unlock funds via pre-interactions; wrapper orders + // produce the required balance at settlement time. + if matches!(order.signature, Signature::Eip1271(_)) || exempt.contains(&order.metadata.uid) { + return true; + } - let balance = match balances.get(&Query::from_order(order)) { - None => return false, - Some(balance) => *balance, - }; + // TODO: replace with proper detection logic. For now, orders with the + // settlement contract as the receiver are treated as flashloan orders whose + // funds are unlocked via a pre-interaction that our balance simulation + // can't reproduce. + if order.data.receiver.as_ref() == Some(&settlement_contract) { + return true; + } - if order.data.partially_fillable && balance >= U256::ONE { - return true; - } + if order.data.partially_fillable && balance >= U256::ONE { + return true; + } - let needed_balance = match order.data.sell_amount.checked_add(order.data.fee_amount) { - None => return false, - Some(balance) => balance, - }; - balance >= needed_balance + let Some(needed_balance) = order.data.sell_amount.checked_add(order.data.fee_amount) else { + return false; }; - - orders.retain(|order| { - if keep(order) { - true - } else { - filtered_orders.push(order.metadata.uid); - false - } - }); - (orders, filtered_orders) + balance >= needed_balance } -/// Filters out dust orders i.e. partially fillable orders that, when scaled -/// have a 0 buy or sell amount. -fn filter_dust_orders<'a>( - mut orders: Vec<&'a Order>, - balances: &Balances, -) -> (Vec<&'a Order>, Vec) { - let mut removed = vec![]; - let keep = |order: &Order| { - if !order.data.partially_fillable { - return true; - } - - let balance = if let Some(balance) = balances.get(&Query::from_order(order)) { - *balance - } else { - return false; - }; - - let Ok(remaining) = - remaining_amounts::Remaining::from_order_with_balance(&order.into(), balance) - else { - return false; - }; - - let (Ok(sell_amount), Ok(buy_amount)) = ( - remaining.remaining(order.data.sell_amount), - remaining.remaining(order.data.buy_amount), - ) else { - return false; - }; - - !sell_amount.is_zero() && !buy_amount.is_zero() +/// Returns true if the order is not a dust order — its remaining sell and +/// buy amounts (scaled by balance) are both non-zero. +fn passes_dust(order: &Order, balance: U256) -> bool { + let Ok(remaining) = + remaining_amounts::Remaining::from_order_with_balance(&order.into(), balance) + else { + return false; }; - orders.retain(|order| { - if keep(order) { - true - } else { - removed.push(order.metadata.uid); - false - } - }); - (orders, removed) -} - -#[instrument(skip_all)] -async fn get_orders_with_native_prices<'a>( - orders: Vec<&'a Order>, - native_price_estimator: &NativePriceUpdater, - timeout: Duration, -) -> ( - Vec<&'a Order>, - Vec, - BTreeMap, -) { - let traded_tokens = orders - .iter() - .flat_map(|order| [order.data.sell_token, order.data.buy_token]) - .collect::>(); - - let prices = get_native_prices(traded_tokens, native_price_estimator, timeout).await; - - // Filter orders so that we only return orders that have prices - let mut removed_market_orders = 0_i64; - let mut removed_orders = vec![]; - let mut orders = orders; - orders.retain(|order| { - let both_prices_present = prices.contains_key(&order.data.sell_token) - && prices.contains_key(&order.data.buy_token); - if both_prices_present { - true - } else { - removed_orders.push(order.metadata.uid); - removed_market_orders += i64::from(order.metadata.class == OrderClass::Market); - false - } - }); - - Metrics::get() - .auction_market_order_missing_price - .set(removed_market_orders); - - (orders, removed_orders, prices) -} - -fn find_unsupported_tokens( - orders: &[&Order], - deny_listed_tokens: &DenyListedTokens, -) -> Vec { - orders - .iter() - .filter_map(|order| { - [&order.data.buy_token, &order.data.sell_token] - .iter() - .any(|token| deny_listed_tokens.contains(token)) - .then_some(order.metadata.uid) - }) - .collect() -} + let (Ok(sell_amount), Ok(buy_amount)) = ( + remaining.remaining(order.data.sell_amount), + remaining.remaining(order.data.buy_amount), + ) else { + return false; + }; -fn filter_out_in_flight_orders<'a>( - mut orders: Vec<&'a Order>, - in_flight: &HashSet, -) -> (Vec<&'a Order>, Vec) { - let mut removed = vec![]; - orders.retain(|order| { - if in_flight.contains(&order.metadata.uid) { - removed.push(order.metadata.uid); - false - } else { - true - } - }); - (orders, removed) + !sell_amount.is_zero() && !buy_amount.is_zero() } #[cfg(test)] @@ -729,407 +590,60 @@ mod tests { super::*, alloy::primitives::{Address, B256}, bad_tokens::list_based::DenyListedTokens, - futures::FutureExt, - maplit::{btreemap, hashset}, model::order::{OrderBuilder, OrderData, OrderMetadata, OrderUid}, - price_estimation::{ - HEALTHY_PRICE_ESTIMATION_TIME, - PriceEstimationError, - native::MockNativePriceEstimating, - native_price_cache::{ - ApproximationToken, - Cache, - CachingNativePriceEstimator, - NativePriceUpdater, - }, - }, }; - #[tokio::test] - async fn get_orders_with_native_prices_with_timeout() { - let token1 = Address::repeat_byte(1); - let token2 = Address::repeat_byte(2); - let token3 = Address::repeat_byte(3); - - let orders = [ - Arc::new( - OrderBuilder::default() - .with_sell_token(token1) - .with_buy_token(token2) - .with_buy_amount(alloy::primitives::U256::ONE) - .with_sell_amount(alloy::primitives::U256::ONE) - .build(), - ), - Arc::new( - OrderBuilder::default() - .with_sell_token(token1) - .with_buy_token(token3) - .with_buy_amount(alloy::primitives::U256::ONE) - .with_sell_amount(alloy::primitives::U256::ONE) - .build(), - ), - ]; - - let mut native_price_estimator = MockNativePriceEstimating::new(); - native_price_estimator - .expect_estimate_native_price() - .withf(move |token, _| *token == token1) - .returning(|_, _| async { Ok(2.) }.boxed()); - native_price_estimator - .expect_estimate_native_price() - .times(1) - .withf(move |token, _| *token == token2) - .returning(|_, _| async { Err(PriceEstimationError::NoLiquidity) }.boxed()); - native_price_estimator - .expect_estimate_native_price() - .times(1) - .withf(move |token, _| *token == token3) - .returning(|_, _| async { Ok(0.25) }.boxed()); - - let cache = Cache::new(Duration::from_secs(10), Default::default()); - let caching_estimator = CachingNativePriceEstimator::new( - Box::new(native_price_estimator), - cache, - 3, - Default::default(), - HEALTHY_PRICE_ESTIMATION_TIME, - ); - let native_price_estimator = - NativePriceUpdater::new(caching_estimator, Duration::MAX, Default::default()); - - let orders_ref = orders.iter().map(|o| o.as_ref()).collect::>(); - let (filtered_orders, _removed, prices) = get_orders_with_native_prices( - orders_ref, - &native_price_estimator, - Duration::from_millis(100), - ) - .await; - assert_eq!(filtered_orders, [orders[1].as_ref()]); - assert_eq!( - prices, - btreemap! { - token1 => alloy::primitives::U256::from(2_000_000_000_000_000_000_u128), - token3 => alloy::primitives::U256::from(250_000_000_000_000_000_u128), - } - ); - } - - #[tokio::test] - async fn filters_orders_with_tokens_without_native_prices() { - let token1 = Address::repeat_byte(1); - let token2 = Address::repeat_byte(2); - let token3 = Address::repeat_byte(3); - let token4 = Address::repeat_byte(4); - - let orders = [ - Arc::new( - OrderBuilder::default() - .with_sell_token(token1) - .with_buy_token(token2) - .with_buy_amount(alloy::primitives::U256::ONE) - .with_sell_amount(alloy::primitives::U256::ONE) - .build(), - ), - Arc::new( - OrderBuilder::default() - .with_sell_token(token2) - .with_buy_token(token3) - .with_buy_amount(alloy::primitives::U256::ONE) - .with_sell_amount(alloy::primitives::U256::ONE) - .build(), - ), - Arc::new( - OrderBuilder::default() - .with_sell_token(token1) - .with_buy_token(token3) - .with_buy_amount(alloy::primitives::U256::ONE) - .with_sell_amount(alloy::primitives::U256::ONE) - .build(), - ), - Arc::new( - OrderBuilder::default() - .with_sell_token(token2) - .with_buy_token(token4) - .with_buy_amount(alloy::primitives::U256::ONE) - .with_sell_amount(alloy::primitives::U256::ONE) - .build(), - ), - ]; - - let mut native_price_estimator = MockNativePriceEstimating::new(); - native_price_estimator - .expect_estimate_native_price() - .withf(move |token, _| *token == token1) - .returning(|_, _| async { Ok(2.) }.boxed()); - native_price_estimator - .expect_estimate_native_price() - .times(1) - .withf(move |token, _| *token == token2) - .returning(|_, _| async { Err(PriceEstimationError::NoLiquidity) }.boxed()); - native_price_estimator - .expect_estimate_native_price() - .times(1) - .withf(move |token, _| *token == token3) - .returning(|_, _| async { Ok(0.25) }.boxed()); - native_price_estimator - .expect_estimate_native_price() - .times(1) - .withf(move |token, _| *token == token4) - .returning(|_, _| async { Ok(0.) }.boxed()); - - let cache = Cache::new(Duration::from_secs(10), Default::default()); - let caching_estimator = CachingNativePriceEstimator::new( - Box::new(native_price_estimator), - cache, - 1, - Default::default(), - HEALTHY_PRICE_ESTIMATION_TIME, - ); - let native_price_estimator = NativePriceUpdater::new( - caching_estimator, - Duration::from_millis(5), - Default::default(), - ); - - // We'll have no native prices in this call. But set_tokens_to_update - // will cause the background task to fetch them in the next cycle. - let orders_ref = orders.iter().map(|o| o.as_ref()).collect::>(); - let (alive_orders, _removed_orders, prices) = - get_orders_with_native_prices(orders_ref, &native_price_estimator, Duration::ZERO) - .await; - assert!(alive_orders.is_empty()); - assert!(prices.is_empty()); - - // Wait for native prices to get fetched by the background task. - tokio::time::sleep(tokio::time::Duration::from_millis(30)).await; - - // Now we have all the native prices we want. - let orders_ref = orders.iter().map(|o| o.as_ref()).collect::>(); - let (alive_orders, _removed_orders, prices) = - get_orders_with_native_prices(orders_ref, &native_price_estimator, Duration::ZERO) - .await; - - assert_eq!(alive_orders, [orders[2].as_ref()]); - assert_eq!( - prices, - btreemap! { - token1 => alloy::primitives::U256::from(2_000_000_000_000_000_000_u128), - token3 => alloy::primitives::U256::from(250_000_000_000_000_000_u128), - } - ); - } - - #[tokio::test] - async fn check_native_price_approximations() { - let token1 = Address::repeat_byte(1); - let token2 = Address::repeat_byte(2); - let token3 = Address::repeat_byte(3); - - let token_approx1 = Address::repeat_byte(4); - let token_approx2 = Address::repeat_byte(5); - - let orders = [ - Arc::new( - OrderBuilder::default() - .with_sell_token(token1) - .with_buy_token(token2) - .with_buy_amount(alloy::primitives::U256::ONE) - .with_sell_amount(alloy::primitives::U256::ONE) - .build(), - ), - Arc::new( - OrderBuilder::default() - .with_sell_token(token1) - .with_buy_token(token2) - .with_buy_amount(alloy::primitives::U256::ONE) - .with_sell_amount(alloy::primitives::U256::ONE) - .build(), - ), - Arc::new( - OrderBuilder::default() - .with_sell_token(token1) - .with_buy_token(token3) - .with_buy_amount(alloy::primitives::U256::ONE) - .with_sell_amount(alloy::primitives::U256::ONE) - .build(), - ), - ]; - - let mut native_price_estimator = MockNativePriceEstimating::new(); - native_price_estimator - .expect_estimate_native_price() - .times(1) - .withf(move |token, _| *token == token3) - .returning(|_, _| async { Ok(3.) }.boxed()); - native_price_estimator - .expect_estimate_native_price() - .times(1) - .withf(move |token, _| *token == token_approx1) - .returning(|_, _| async { Ok(40.) }.boxed()); - native_price_estimator - .expect_estimate_native_price() - .times(1) - .withf(move |token, _| *token == token_approx2) - .returning(|_, _| async { Ok(50.) }.boxed()); - - let cache = Cache::new(Duration::from_secs(10), Default::default()); - let caching_estimator = CachingNativePriceEstimator::new( - Box::new(native_price_estimator), - cache, - 3, - // Set to use native price approximations for the following tokens - HashMap::from([ - (token1, ApproximationToken::same_decimals(token_approx1)), - (token2, ApproximationToken::same_decimals(token_approx2)), - ]), - HEALTHY_PRICE_ESTIMATION_TIME, - ); - let native_price_estimator = - NativePriceUpdater::new(caching_estimator, Duration::MAX, Default::default()); - - let orders_ref = orders.iter().map(|o| o.as_ref()).collect::>(); - let (alive_orders, _removed_orders, prices) = get_orders_with_native_prices( - orders_ref, - &native_price_estimator, - Duration::from_secs(10), - ) - .await; - assert!( - alive_orders - .iter() - .copied() - .eq(orders.iter().map(Arc::as_ref)) - ); - assert_eq!( - prices, - btreemap! { - token1 => alloy::primitives::U256::from(40_000_000_000_000_000_000_u128), - token2 => alloy::primitives::U256::from(50_000_000_000_000_000_000_u128), - token3 => alloy::primitives::U256::from(3_000_000_000_000_000_000_u128), - } - ); - } - - #[tokio::test] - async fn filters_banned_users() { - let banned_users = hashset!(Address::from([0xba; 20]), Address::from([0xbb; 20])); - let orders = [ - Address::repeat_byte(1), - Address::repeat_byte(1), - Address::repeat_byte(0xba), - Address::repeat_byte(2), - Address::repeat_byte(0xba), - Address::repeat_byte(0xbb), - Address::repeat_byte(3), - ] - .into_iter() - .enumerate() - .map(|(i, owner)| { - Arc::new(Order { - metadata: OrderMetadata { - owner, - uid: OrderUid([i as u8; 56]), - ..Default::default() - }, - data: OrderData { - buy_amount: alloy::primitives::U256::ONE, - sell_amount: alloy::primitives::U256::ONE, - ..Default::default() - }, - ..Default::default() - }) - }) - .collect::>(); - - let orders_ref = orders.iter().map(|o| o.as_ref()).collect::>(); - let banned_user_orders = find_banned_user_orders( - &orders_ref, - &order_validation::banned::Users::from_set(banned_users), - ) - .await; - assert_eq!( - banned_user_orders, - [OrderUid([2; 56]), OrderUid([4; 56]), OrderUid([5; 56])], - ); - } - #[test] - fn finds_presignature_pending_orders() { - let presign_uid = OrderUid::from_parts(B256::repeat_byte(1), Address::repeat_byte(11), 1); - let orders = [ - // PresignaturePending order - should be found - Arc::new(Order { - metadata: OrderMetadata { - uid: presign_uid, - status: model::order::OrderStatus::PresignaturePending, - ..Default::default() - }, - ..Default::default() - }), - // EIP-1271 order - not PresignaturePending - Arc::new(Order { - metadata: OrderMetadata { - uid: OrderUid::from_parts(B256::repeat_byte(2), Address::repeat_byte(22), 2), - ..Default::default() - }, - signature: Signature::Eip1271(vec![2, 2]), - ..Default::default() - }), - // Regular order - not PresignaturePending - Arc::new(Order { - metadata: OrderMetadata { - uid: OrderUid::from_parts(B256::repeat_byte(3), Address::repeat_byte(33), 3), - ..Default::default() - }, + fn is_presig_pending_only_matches_presig() { + let presig = Order { + metadata: OrderMetadata { + status: model::order::OrderStatus::PresignaturePending, ..Default::default() - }), - ]; + }, + ..Default::default() + }; + let eip1271 = Order { + signature: Signature::Eip1271(vec![2, 2]), + ..Default::default() + }; + let regular = Order::default(); - let orders_ref = orders.iter().map(|o| o.as_ref()).collect::>(); - let pending_orders = find_presignature_pending_orders(&orders_ref); - assert_eq!(pending_orders, vec![presign_uid]); + assert!(is_presig_pending(&presig)); + assert!(!is_presig_pending(&eip1271)); + assert!(!is_presig_pending(®ular)); } #[test] - fn filter_unsupported_tokens_() { + fn is_unsupported_matches_either_side() { let token0 = Address::with_last_byte(0); let token1 = Address::with_last_byte(1); let token2 = Address::with_last_byte(2); let deny_listed_tokens = DenyListedTokens::new(vec![token0]); - let orders = [ - Arc::new( - OrderBuilder::default() - .with_sell_token(token0) - .with_buy_token(token1) - .build(), - ), - Arc::new( - OrderBuilder::default() - .with_sell_token(token1) - .with_buy_token(token2) - .build(), - ), - Arc::new( - OrderBuilder::default() - .with_sell_token(token0) - .with_buy_token(token2) - .build(), - ), - ]; - let orders_ref = orders.iter().map(|o| o.as_ref()).collect::>(); - let unsupported_tokens_orders = find_unsupported_tokens(&orders_ref, &deny_listed_tokens); - assert_eq!( - unsupported_tokens_orders, - [orders[0].metadata.uid, orders[2].metadata.uid] - ); + + let sell_denied = OrderBuilder::default() + .with_sell_token(token0) + .with_buy_token(token1) + .build(); + let neither_denied = OrderBuilder::default() + .with_sell_token(token1) + .with_buy_token(token2) + .build(); + let buy_denied = OrderBuilder::default() + .with_sell_token(token1) + .with_buy_token(token0) + .build(); + + assert!(is_unsupported(&sell_denied, &deny_listed_tokens)); + assert!(!is_unsupported(&neither_denied, &deny_listed_tokens)); + assert!(is_unsupported(&buy_denied, &deny_listed_tokens)); } #[test] - fn orders_with_balance_() { + fn passes_balance_covers_all_cases() { let settlement_contract = Address::repeat_byte(1); let orders = [ // enough balance for sell and fee - Arc::new(Order { + Order { data: OrderData { sell_token: Address::with_last_byte(2), sell_amount: alloy::primitives::U256::ONE, @@ -1138,9 +652,9 @@ mod tests { ..Default::default() }, ..Default::default() - }), + }, // missing fee balance - Arc::new(Order { + Order { data: OrderData { sell_token: Address::with_last_byte(3), sell_amount: alloy::primitives::U256::ONE, @@ -1149,9 +663,9 @@ mod tests { ..Default::default() }, ..Default::default() - }), + }, // at least 1 partially fillable balance - Arc::new(Order { + Order { data: OrderData { sell_token: Address::with_last_byte(4), sell_amount: alloy::primitives::U256::from(2), @@ -1160,9 +674,9 @@ mod tests { ..Default::default() }, ..Default::default() - }), + }, // 0 partially fillable balance - Arc::new(Order { + Order { data: OrderData { sell_token: Address::with_last_byte(5), sell_amount: alloy::primitives::U256::from(2), @@ -1171,9 +685,9 @@ mod tests { ..Default::default() }, ..Default::default() - }), + }, // considered flashloan order because of special receiver - Arc::new(Order { + Order { data: OrderData { sell_token: Address::with_last_byte(6), sell_amount: alloy::primitives::U256::from(200), @@ -1183,36 +697,47 @@ mod tests { ..Default::default() }, ..Default::default() - }), + }, ]; - let balances = [ - (Query::from_order(&orders[0]), U256::from(2)), - (Query::from_order(&orders[1]), U256::from(1)), - (Query::from_order(&orders[2]), U256::from(1)), - (Query::from_order(&orders[3]), U256::from(0)), - (Query::from_order(&orders[4]), U256::from(0)), - ] - .into_iter() - .collect(); - let expected = &[0, 2, 4]; - - let no_bypass: HashSet = HashSet::new(); - let orders_ref = orders.iter().map(|o| o.as_ref()).collect::>(); - let (alive_orders, _removed_orders) = - orders_with_balance(orders_ref, &balances, settlement_contract, &no_bypass); - assert_eq!(alive_orders.len(), expected.len()); - for index in expected { - let found = alive_orders.iter().any(|o| o.data == orders[*index].data); - assert!(found, "{}", index); - } + let no_bypass = HashSet::with_hasher(FbBuildHasher::default()); + + assert!(passes_balance( + &orders[0], + U256::from(2), + settlement_contract, + &no_bypass + )); + assert!(!passes_balance( + &orders[1], + U256::ONE, + settlement_contract, + &no_bypass + )); + assert!(passes_balance( + &orders[2], + U256::ONE, + settlement_contract, + &no_bypass + )); + assert!(!passes_balance( + &orders[3], + U256::ZERO, + settlement_contract, + &no_bypass + )); + assert!(passes_balance( + &orders[4], + U256::ZERO, + settlement_contract, + &no_bypass + )); } #[test] - fn eip1271_and_wrapper_orders_skip_balance_filtering() { + fn passes_balance_bypasses_eip1271_and_wrappers() { let settlement_contract = Address::repeat_byte(1); - // EIP-1271 order (should skip balance check) - let eip1271_order = Arc::new(Order { + let eip1271_order = Order { data: OrderData { sell_token: Address::with_last_byte(7), sell_amount: alloy::primitives::U256::from(10), @@ -1226,12 +751,11 @@ mod tests { ..Default::default() }, ..Default::default() - }); + }; - // Order with wrappers in bypass set (should skip balance check) let wrapper_order_uid = OrderUid::from_parts(B256::repeat_byte(7), Address::repeat_byte(77), 7); - let wrapper_order = Arc::new(Order { + let wrapper_order = Order { data: OrderData { sell_token: Address::with_last_byte(8), sell_amount: alloy::primitives::U256::from(10), @@ -1244,10 +768,9 @@ mod tests { ..Default::default() }, ..Default::default() - }); + }; - // Regular ECDSA order without wrappers (should be filtered) - let regular_order = Arc::new(Order { + let regular_order = Order { data: OrderData { sell_token: Address::with_last_byte(9), sell_amount: alloy::primitives::U256::from(10), @@ -1260,38 +783,52 @@ mod tests { ..Default::default() }, ..Default::default() - }); + }; - let orders = [ - regular_order.clone(), - eip1271_order.clone(), - wrapper_order.clone(), - ]; - let balances: Balances = Default::default(); // No balances - - // EIP-1271 order and wrapper order should be retained, regular order filtered - let wrapper_set = HashSet::from([wrapper_order_uid]); - let orders_ref = orders.iter().map(|o| o.as_ref()).collect::>(); - let (alive_orders, _removed_orders) = - orders_with_balance(orders_ref, &balances, settlement_contract, &wrapper_set); - assert_eq!(alive_orders.len(), 2); - assert!( - alive_orders - .iter() - .any(|o| o.metadata.uid == eip1271_order.metadata.uid) - ); - assert!( - alive_orders - .iter() - .any(|o| o.metadata.uid == wrapper_order.metadata.uid) - ); + let wrapper_set: HashSet> = + [wrapper_order_uid].into_iter().collect(); + let empty_set = HashSet::with_hasher(FbBuildHasher::default()); - // Without wrapper set, only EIP-1271 order should be retained - let empty_set: HashSet = HashSet::new(); - let orders_ref = orders.iter().map(|o| o.as_ref()).collect::>(); - let (alive_orders, _removed_orders) = - orders_with_balance(orders_ref, &balances, settlement_contract, &empty_set); - assert_eq!(alive_orders.len(), 1); - assert_eq!(alive_orders[0].metadata.uid, eip1271_order.metadata.uid); + // EIP-1271 always bypasses regardless of the exempt set. + assert!(passes_balance( + &eip1271_order, + U256::ZERO, + settlement_contract, + &empty_set + )); + assert!(passes_balance( + &eip1271_order, + U256::ZERO, + settlement_contract, + &wrapper_set + )); + + // Wrapper order bypasses only when its uid is in the exempt set. + assert!(!passes_balance( + &wrapper_order, + U256::ZERO, + settlement_contract, + &empty_set + )); + assert!(passes_balance( + &wrapper_order, + U256::ZERO, + settlement_contract, + &wrapper_set + )); + + // Regular order without a matching balance entry always fails. + assert!(!passes_balance( + ®ular_order, + U256::ZERO, + settlement_contract, + &empty_set + )); + assert!(!passes_balance( + ®ular_order, + U256::ZERO, + settlement_contract, + &wrapper_set + )); } } diff --git a/crates/bad-tokens/src/list_based.rs b/crates/bad-tokens/src/list_based.rs index 297e33c42c..ecec3f94b7 100644 --- a/crates/bad-tokens/src/list_based.rs +++ b/crates/bad-tokens/src/list_based.rs @@ -1,6 +1,6 @@ use { - alloy_primitives::Address, - std::{collections::HashSet, sync::Arc}, + alloy_primitives::{Address, map::AddressHashSet}, + std::sync::Arc, }; /// Explicitly deny listed tokens. @@ -9,7 +9,7 @@ pub struct DenyListedTokens(Arc); #[derive(Default)] struct Inner { - deny_list: HashSet
, + deny_list: AddressHashSet, } impl DenyListedTokens { diff --git a/crates/configs/src/autopilot/mod.rs b/crates/configs/src/autopilot/mod.rs index 5cfcc3af78..f76c9837ed 100644 --- a/crates/configs/src/autopilot/mod.rs +++ b/crates/configs/src/autopilot/mod.rs @@ -121,11 +121,6 @@ pub struct Configuration { #[serde(default)] pub run_loop: RunLoopConfig, - /// Maximum timeout for fetching native prices in the run loop. - /// If 0, native prices are fetched from cache. - #[serde(with = "humantime_serde", default)] - pub native_price_timeout: Duration, - /// Whether to skip filtering out orders with insufficient balances. #[serde(default)] pub disable_order_balance_filter: bool, @@ -220,7 +215,6 @@ impl Configuration { run_loop: TestDefault::test_default(), disable_order_balance_filter: false, max_maintenance_timeout: default_max_maintenance_timeout(), - native_price_timeout: Duration::from_millis(500), unsupported_tokens: Default::default(), min_order_validity_period: default_min_order_validity_period(), max_auction_age: default_max_auction_age(), @@ -251,7 +245,6 @@ impl Configuration { run_loop: TestDefault::test_default(), disable_order_balance_filter: false, max_maintenance_timeout: default_max_maintenance_timeout(), - native_price_timeout: Duration::from_millis(500), unsupported_tokens: Default::default(), min_order_validity_period: default_min_order_validity_period(), max_auction_age: default_max_auction_age(), @@ -453,7 +446,6 @@ mod tests { ); assert_eq!(config.min_order_validity_period, Duration::from_secs(120)); assert_eq!(config.max_auction_age, Duration::from_secs(600)); - assert_eq!(config.native_price_timeout, Duration::from_secs(3)); } #[test] @@ -516,7 +508,6 @@ mod tests { assert!(config.surplus_capturing_jit_order_owners.is_empty()); assert_eq!(config.min_order_validity_period, Duration::from_secs(60)); assert_eq!(config.max_auction_age, Duration::from_secs(300)); - assert_eq!(config.native_price_timeout, Duration::ZERO); } #[test] diff --git a/crates/order-validation/src/banned/cached.rs b/crates/order-validation/src/banned/cached.rs index 2ac6e66dda..6a35a8d547 100644 --- a/crates/order-validation/src/banned/cached.rs +++ b/crates/order-validation/src/banned/cached.rs @@ -3,12 +3,11 @@ //! "banned by anyone?"; backends stay as pure fetchers. use { - alloy_primitives::Address, + alloy_primitives::{Address, map::AddressHashSet}, async_trait::async_trait, futures::{StreamExt, future::join_all, stream}, moka::sync::Cache, std::{ - collections::HashSet, sync::{Arc, Weak}, time::{Duration, Instant}, }, @@ -65,8 +64,8 @@ impl Cached { /// Returns the subset reported as banned by any backend. Misses fan out /// to backends concurrently. - pub(super) async fn check(&self, addresses: &HashSet
) -> HashSet
{ - let mut banned = HashSet::new(); + pub(super) async fn check(&self, addresses: &AddressHashSet) -> AddressHashSet { + let mut banned = AddressHashSet::default(); let mut need_lookup = Vec::new(); for address in addresses { match self.cache.get(address) { diff --git a/crates/order-validation/src/banned/mod.rs b/crates/order-validation/src/banned/mod.rs index b9754c5d0c..6be8046c53 100644 --- a/crates/order-validation/src/banned/mod.rs +++ b/crates/order-validation/src/banned/mod.rs @@ -12,14 +12,14 @@ use { hermod::Client as Hermod, onchain::Onchain, }, - alloy_primitives::Address, + alloy_primitives::{Address, map::AddressHashSet}, contracts::ChainalysisOracle, - std::{collections::HashSet, sync::Arc}, + std::sync::Arc, }; /// A list of banned users and optional registries that can be checked. pub struct Users { - list: HashSet
, + list: AddressHashSet, remote: Option>, } @@ -39,7 +39,7 @@ impl Users { backends.push(Box::new(Hermod::new(config))); } Self { - list: HashSet::from_iter(banned_users), + list: AddressHashSet::from_iter(banned_users), remote: Cached::new(backends, cache_max_size), } } @@ -47,21 +47,21 @@ impl Users { /// Creates a new `Users` instance that passes all addresses. pub fn none() -> Self { Self { - list: HashSet::new(), + list: AddressHashSet::default(), remote: None, } } /// Creates a new `Users` instance that passes all addresses except for the /// ones in `list`. - pub fn from_set(list: HashSet
) -> Self { + pub fn from_set(list: AddressHashSet) -> Self { Self { list, remote: None } } /// Returns the subset of `addresses` that are banned. Cache misses hit /// the configured remote sources. - pub async fn banned(&self, addresses: impl IntoIterator) -> HashSet
{ - let mut banned = HashSet::new(); + pub async fn banned(&self, addresses: impl IntoIterator) -> AddressHashSet { + let mut banned = AddressHashSet::default(); let need_lookup = addresses .into_iter() @@ -78,7 +78,7 @@ impl Users { } }) // Need to collect here to make sure filter gets executed and we insert addresses - .collect::>(); + .collect::(); if let Some(remote) = &self.remote { banned.extend(remote.check(&need_lookup).await); diff --git a/crates/price-estimation/src/native_price_cache.rs b/crates/price-estimation/src/native_price_cache.rs index 1db8fe8b21..daccccf79e 100644 --- a/crates/price-estimation/src/native_price_cache.rs +++ b/crates/price-estimation/src/native_price_cache.rs @@ -1,7 +1,16 @@ use { super::PriceEstimationError, - crate::native::{NativePriceEstimateResult, NativePriceEstimating, from_normalized_price}, - alloy::primitives::Address, + crate::native::{ + NativePriceEstimateResult, + NativePriceEstimating, + from_normalized_price, + to_normalized_price, + }, + alloy::primitives::{ + Address, + U256, + map::{AddressHashMap, FbBuildHasher}, + }, arc_swap::ArcSwap, bigdecimal::BigDecimal, futures::{FutureExt, StreamExt}, @@ -10,6 +19,7 @@ use { request_sharing::{BoxRequestSharing, RequestSharing}, std::{ collections::{HashMap, HashSet}, + hash::BuildHasher, sync::Arc, time::{Duration, Instant}, }, @@ -160,7 +170,7 @@ pub struct Cache(Arc); const MAX_CACHE_SIZE: u64 = 20_000; struct CacheInner { - data: moka::sync::Cache, + data: moka::sync::Cache>, max_age: Duration, } @@ -169,9 +179,10 @@ impl Cache { let mut rng = rand::rng(); let now = std::time::Instant::now(); - let data = moka::sync::Cache::builder() - .max_capacity(MAX_CACHE_SIZE) - .build(); + let data: moka::sync::Cache> = + moka::sync::Cache::builder() + .max_capacity(MAX_CACHE_SIZE) + .build_with_hasher(FbBuildHasher::<20>::default()); for (token, price) in initial_prices { if let Some(price) = from_normalized_price(price) { @@ -222,7 +233,7 @@ impl Cache { fn get_cached_price( token: Address, now: Instant, - cache: &moka::sync::Cache, + cache: &moka::sync::Cache>, max_age: &Duration, ) -> Option { let entry = cache.get(&token)?; @@ -230,11 +241,35 @@ impl Cache { (is_recent && entry.is_ready()).then_some(entry) } + /// Snapshot of all currently cached, non-expired, ready prices normalized + /// to `U256`. Skips expired entries, entries not yet considered ready, and + /// errors. + pub fn snapshot(&self) -> AddressHashMap { + let now = Instant::now(); + let max_age = self.0.max_age; + let mut out = AddressHashMap::with_capacity_and_hasher( + self.0.data.entry_count() as usize, + FbBuildHasher::default(), + ); + for (token, entry) in self.0.data.iter() { + let is_recent = now.saturating_duration_since(entry.updated_at) < max_age; + if !is_recent || !entry.is_ready() { + continue; + } + if let Ok(price) = entry.result + && let Some(u256) = to_normalized_price(price) + { + out.insert(*token, u256); + } + } + out + } + /// Only returns prices that are currently cached. fn get_cached_prices( &self, tokens: &[Address], - ) -> HashMap> { + ) -> HashMap, FbBuildHasher<20>> { let now = Instant::now(); let mut results = HashMap::default(); for token in tokens { @@ -286,7 +321,7 @@ struct CachingInner { /// safe (e.g. csUSDL => Dai). /// The normalization factor handles decimal differences between tokens. /// After startup this is a read only value. - approximation_tokens: HashMap, + approximation_tokens: AddressHashMap, quote_timeout: Duration, requests_in_flight: BoxRequestSharing, } @@ -303,7 +338,7 @@ impl CachingNativePriceEstimator { estimator, cache, concurrent_requests, - approximation_tokens, + approximation_tokens: approximation_tokens.into_iter().collect(), quote_timeout, requests_in_flight: RequestSharing::labelled("native_price".to_string()), }); @@ -384,7 +419,7 @@ impl CachingNativePriceEstimator { &self, tokens: &[Address], timeout: Duration, - ) -> HashMap { + ) -> AddressHashMap { let mut prices = self.0.cache.get_cached_prices(tokens); if timeout.is_zero() { return prices; @@ -450,7 +485,8 @@ impl NativePriceEstimating for CachingNativePriceEstimator { /// and caching prices. pub struct NativePriceUpdater { estimator: CachingNativePriceEstimator, - tokens_to_update: ArcSwap>, + /// tokens are guaranteed to be unique + tokens_to_update: ArcSwap>, } impl NativePriceUpdater { @@ -468,7 +504,7 @@ impl NativePriceUpdater { let updater = Arc::new(Self { estimator, - tokens_to_update: ArcSwap::new(Arc::new(HashSet::new())), + tokens_to_update: ArcSwap::new(Arc::new(Vec::new())), }); // Don't keep the updater alive just for the background task @@ -489,17 +525,34 @@ impl NativePriceUpdater { updater } + /// Sync snapshot of all currently cached prices normalized to `U256`. + /// Callers can use this to filter orders by native price without paying + /// for an async round-trip. + pub fn cached_prices(&self) -> AddressHashMap { + self.estimator.cache().snapshot() + } + + /// Replaces the full set of tokens that should be maintained by the + /// background task without triggering an immediate fetch. + pub fn schedule_token_updates(&self, tokens: HashSet) { + tracing::trace!(?tokens, "update tokens to maintain"); + self.tokens_to_update + .store(Arc::new(tokens.into_iter().collect())); + } + /// Replaces the full set of tokens that should be maintained by the - /// background task and fetches their current prices. - pub async fn update_tokens_and_fetch_prices( + /// background task and fetches their current prices. Generic over the + /// hasher so callers can pass address-optimised hash sets without a + /// rehash round-trip through the default hasher. + pub async fn update_tokens_and_fetch_prices( &self, - tokens: HashSet
, + tokens: HashSet, timeout: Duration, - ) -> HashMap { + ) -> AddressHashMap { tracing::trace!(?tokens, "update tokens to maintain"); - let token_list: Vec<_> = tokens.iter().copied().collect(); - self.tokens_to_update.store(Arc::new(tokens)); - self.estimator.fetch_prices(&token_list, timeout).await + let tokens: Arc> = Arc::new(tokens.into_iter().collect()); + self.tokens_to_update.store(tokens.clone()); + self.estimator.fetch_prices(&tokens, timeout).await } async fn single_update(&self, prefetch_time: Duration) { @@ -1038,7 +1091,7 @@ mod tests { // Tell the updater about these tokens updater .update_tokens_and_fetch_prices( - [token(0), token(1)].into_iter().collect(), + [token(0), token(1)].into_iter().collect::>(), Duration::ZERO, ) .await; diff --git a/crates/shared/src/order_validation.rs b/crates/shared/src/order_validation.rs index b1998093ee..2f9517cf73 100644 --- a/crates/shared/src/order_validation.rs +++ b/crates/shared/src/order_validation.rs @@ -1265,12 +1265,11 @@ mod tests { }, account_balances::MockBalanceFetching, alloy::{ - primitives::{Address, U160, U256, address, b256}, + primitives::{Address, U160, U256, address, b256, map::AddressHashSet}, providers::{Provider, ProviderBuilder, mock::Asserter}, signers::local::PrivateKeySigner, }, futures::FutureExt, - maplit::hashset, mockall::predicate::{always, eq}, model::{ quote::default_verification_gas_limit, @@ -1357,7 +1356,7 @@ mod tests { max_market: Duration::from_secs(100), max_limit: Duration::from_secs(200), }; - let banned_users = hashset![Address::from(U160::from(1))]; + let banned_users = AddressHashSet::from_iter([Address::from(U160::from(1))]); let legit_valid_to = time::now_in_epoch_seconds() + validity_configuration.min.as_secs() as u32 + 2; let mut limit_order_counter = MockLimitOrderCounting::new(); From f3532d4fcfb66c241a1de3be983c9d15a9b1ce1b Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Thu, 30 Jul 2026 10:11:41 +0000 Subject: [PATCH 2/9] report in separate function --- crates/autopilot/src/solvable_orders.rs | 160 ++++++++++++------------ 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index ae7718093a..7f892e239b 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -147,6 +147,72 @@ struct Inner { solvable_orders: boundary::SolvableOrders, } +/// Orders dropped during a single `update()` cycle, grouped by the reason they +/// were dropped. Owns the ability to both emit filtered-order metrics and +/// persist the events, keeping observability separate from the filtering logic. +#[derive(Default)] +struct FilteredOrders { + token_deny_listed: Vec, + presig_pending: Vec, + missing_price: Vec, + in_flight: Vec, + banned_user: Vec, + insufficient_balance: Vec, + dust: Vec, +} + +impl FilteredOrders { + /// Emits per-reason metrics and, when `store_events` is set, forwards each + /// reason's uids to persistence with the correct event label. Consumes + /// `self` so the uid vecs can be moved into the background storage task + /// without copying. + fn report(self, persistence: &infra::Persistence, store_events: bool) { + Metrics::track_filtered_orders(UnsupportedToken, &self.token_deny_listed); + Metrics::track_filtered_orders(InvalidSignature, &self.presig_pending); + Metrics::track_filtered_orders(MissingNativePrice, &self.missing_price); + Metrics::track_filtered_orders(InFlight, &self.in_flight); + Metrics::track_filtered_orders(BannedUser, &self.banned_user); + Metrics::track_filtered_orders(InsufficientBalance, &self.insufficient_balance); + Metrics::track_filtered_orders(DustOrder, &self.dust); + + if !store_events { + return; + } + + let store = |uids: Vec, label, reason| { + persistence.store_order_events_owned( + uids, + |uid| domain::OrderUid(uid.0), + label, + Some(reason), + ); + }; + store( + self.token_deny_listed, + OrderEventLabel::Invalid, + UnsupportedToken, + ); + store( + self.presig_pending, + OrderEventLabel::Invalid, + InvalidSignature, + ); + store(self.banned_user, OrderEventLabel::Invalid, BannedUser); + store( + self.insufficient_balance, + OrderEventLabel::Invalid, + InsufficientBalance, + ); + store(self.in_flight, OrderEventLabel::Filtered, InFlight); + store(self.dust, OrderEventLabel::Filtered, DustOrder); + store( + self.missing_price, + OrderEventLabel::Filtered, + MissingNativePrice, + ); + } +} + impl SolvableOrdersCache { #[expect(clippy::too_many_arguments)] pub fn new( @@ -213,10 +279,7 @@ impl SolvableOrdersCache { .or_insert_with(|| to_normalized_price(1.0).unwrap()); let capacity_hint = db_solvable_orders.orders.len(); - // filtered orders - let mut unsupported_uids = Vec::new(); - let mut presig_uids = Vec::new(); - let mut missing_price_uids = Vec::new(); + let mut filtered = FilteredOrders::default(); // data needed for filtering logic in next phase let mut traders = AddressHashSet::default(); @@ -234,18 +297,18 @@ impl SolvableOrdersCache { traded_tokens.insert(order.data.sell_token); traded_tokens.insert(order.data.buy_token); - if is_unsupported(order, &self.deny_listed_tokens) { - unsupported_uids.push(uid); + if token_deny_listed(order, &self.deny_listed_tokens) { + filtered.token_deny_listed.push(uid); continue; } if is_presig_pending(order) { - presig_uids.push(uid); + filtered.presig_pending.push(uid); continue; } if !prices.contains_key(&order.data.sell_token) || !prices.contains_key(&order.data.buy_token) { - missing_price_uids.push(uid); + filtered.missing_price.push(uid); continue; } @@ -263,10 +326,6 @@ impl SolvableOrdersCache { survivors.push(order); } - Metrics::track_filtered_orders(UnsupportedToken, &unsupported_uids); - Metrics::track_filtered_orders(InvalidSignature, &presig_uids); - Metrics::track_filtered_orders(MissingNativePrice, &missing_price_uids); - // at this point we know all relevant tokens and tell the native price // cache to have them ready for the next auction self.native_price_estimator @@ -295,24 +354,18 @@ impl SolvableOrdersCache { .copied() .collect(); - // filtered orders - let mut in_flight_removed = Vec::new(); - let mut banned_removed = Vec::new(); - let mut balance_removed = Vec::new(); - let mut dust_removed = Vec::new(); - let final_orders = survivors .into_iter() .filter_map(|order| { let uid = order.metadata.uid; if in_flight.contains(&uid) { - in_flight_removed.push(uid); + filtered.in_flight.push(uid); return None; } let is_banned = banned_set.contains(&order.metadata.owner) || order.data.receiver.is_some_and(|r| banned_set.contains(&r)); if is_banned { - banned_removed.push(uid); + filtered.banned_user.push(uid); return None; } if !self.disable_order_balance_filter { @@ -324,11 +377,11 @@ impl SolvableOrdersCache { self.settlement_contract, &balance_filter_exempt, ) { - balance_removed.push(uid); + filtered.insufficient_balance.push(uid); return None; } if !passes_dust(order, balance) { - dust_removed.push(uid); + filtered.dust.push(uid); return None; } } @@ -344,42 +397,8 @@ impl SolvableOrdersCache { }) .collect::>(); - Metrics::track_filtered_orders(InFlight, &in_flight_removed); - Metrics::track_filtered_orders(BannedUser, &banned_removed); - Metrics::track_filtered_orders(InsufficientBalance, &balance_removed); - Metrics::track_filtered_orders(DustOrder, &dust_removed); - Metrics::track_orders_in_final_auction(&final_orders); - - if store_events { - let mut invalid_order_uids: HashMap> = - HashMap::with_hasher(FbBuildHasher::default()); - invalid_order_uids.extend( - unsupported_uids - .into_iter() - .map(|uid| (uid, UnsupportedToken)), - ); - invalid_order_uids.extend(presig_uids.into_iter().map(|uid| (uid, InvalidSignature))); - invalid_order_uids.extend(banned_removed.into_iter().map(|uid| (uid, BannedUser))); - invalid_order_uids.extend( - balance_removed - .into_iter() - .map(|uid| (uid, InsufficientBalance)), - ); - - let mut filtered_order_events: Vec<(OrderUid, OrderFilterReason)> = Vec::new(); - filtered_order_events - .extend(in_flight_removed.iter().copied().map(|uid| (uid, InFlight))); - filtered_order_events.extend(dust_removed.into_iter().map(|uid| (uid, DustOrder))); - filtered_order_events.extend( - missing_price_uids - .into_iter() - .map(|uid| (uid, MissingNativePrice)), - ); - - self.store_events_by_reason(invalid_order_uids, OrderEventLabel::Invalid); - self.store_events_by_reason(filtered_order_events, OrderEventLabel::Filtered); - } + filtered.report(&self.persistence, store_events); let auction = domain::RawAuctionData { block, @@ -497,29 +516,10 @@ impl SolvableOrdersCache { .start_timer(); fut.await } - - fn store_events_by_reason( - &self, - orders: impl IntoIterator, - label: OrderEventLabel, - ) { - let mut by_reason: HashMap> = HashMap::new(); - for (uid, reason) in orders { - by_reason.entry(reason).or_default().push(uid); - } - for (reason, uids) in by_reason { - self.persistence.store_order_events_owned( - uids, - |uid| domain::OrderUid(uid.0), - label, - Some(reason), - ); - } - } } /// Returns true if either of the order's tokens is on the deny list. -fn is_unsupported(order: &Order, deny_listed_tokens: &DenyListedTokens) -> bool { +fn token_deny_listed(order: &Order, deny_listed_tokens: &DenyListedTokens) -> bool { deny_listed_tokens.contains(&order.data.sell_token) || deny_listed_tokens.contains(&order.data.buy_token) } @@ -633,9 +633,9 @@ mod tests { .with_buy_token(token0) .build(); - assert!(is_unsupported(&sell_denied, &deny_listed_tokens)); - assert!(!is_unsupported(&neither_denied, &deny_listed_tokens)); - assert!(is_unsupported(&buy_denied, &deny_listed_tokens)); + assert!(token_deny_listed(&sell_denied, &deny_listed_tokens)); + assert!(!token_deny_listed(&neither_denied, &deny_listed_tokens)); + assert!(token_deny_listed(&buy_denied, &deny_listed_tokens)); } #[test] From 934787239d17db5ebdba7e9a0a4d7756922791b3 Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Thu, 30 Jul 2026 10:30:49 +0000 Subject: [PATCH 3/9] drop settlement contract filter for flashloan orders --- crates/autopilot/src/run.rs | 1 - crates/autopilot/src/solvable_orders.rs | 112 +++--------------------- 2 files changed, 12 insertions(+), 101 deletions(-) diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs index a4e0ac171b..041a2cac6d 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -482,7 +482,6 @@ pub async fn run(config: Configuration, shutdown_controller: ShutdownController) *eth.contracts().weth().address(), ), config.surplus_capturing_jit_order_owners, - *eth.contracts().settlement().address(), config.disable_order_balance_filter, ); diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index 7f892e239b..f247cee3b6 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -137,7 +137,6 @@ pub struct SolvableOrdersCache { weth: Address, protocol_fees: domain::ProtocolFees, surplus_capturing_jit_order_owners: Vec
, - settlement_contract: Address, disable_order_balance_filter: bool, wrapper_cache: app_data::WrapperCache, } @@ -225,7 +224,6 @@ impl SolvableOrdersCache { weth: Address, protocol_fees: domain::ProtocolFees, surplus_capturing_jit_order_owners: Vec
, - settlement_contract: Address, disable_order_balance_filter: bool, ) -> Arc { Arc::new(Self { @@ -239,7 +237,6 @@ impl SolvableOrdersCache { weth, protocol_fees, surplus_capturing_jit_order_owners, - settlement_contract, disable_order_balance_filter, wrapper_cache: app_data::WrapperCache::new(20_000), }) @@ -371,12 +368,7 @@ impl SolvableOrdersCache { if !self.disable_order_balance_filter { let balance = *balances.get(&Query::from_order(order))?; - if !passes_balance( - order, - balance, - self.settlement_contract, - &balance_filter_exempt, - ) { + if !passes_balance(order, balance, &balance_filter_exempt) { filtered.insufficient_balance.push(uid); return None; } @@ -538,7 +530,6 @@ fn is_presig_pending(order: &Order) -> bool { fn passes_balance( order: &Order, balance: U256, - settlement_contract: Address, exempt: &HashSet>, ) -> bool { // EIP-1271 orders can unlock funds via pre-interactions; wrapper orders @@ -547,15 +538,7 @@ fn passes_balance( return true; } - // TODO: replace with proper detection logic. For now, orders with the - // settlement contract as the receiver are treated as flashloan orders whose - // funds are unlocked via a pre-interaction that our balance simulation - // can't reproduce. - if order.data.receiver.as_ref() == Some(&settlement_contract) { - return true; - } - - if order.data.partially_fillable && balance >= U256::ONE { + if order.data.partially_fillable && balance.is_zero() { return true; } @@ -640,7 +623,6 @@ mod tests { #[test] fn passes_balance_covers_all_cases() { - let settlement_contract = Address::repeat_byte(1); let orders = [ // enough balance for sell and fee Order { @@ -686,57 +668,17 @@ mod tests { }, ..Default::default() }, - // considered flashloan order because of special receiver - Order { - data: OrderData { - sell_token: Address::with_last_byte(6), - sell_amount: alloy::primitives::U256::from(200), - fee_amount: alloy::primitives::U256::ZERO, - partially_fillable: true, - receiver: Some(settlement_contract), - ..Default::default() - }, - ..Default::default() - }, ]; let no_bypass = HashSet::with_hasher(FbBuildHasher::default()); - assert!(passes_balance( - &orders[0], - U256::from(2), - settlement_contract, - &no_bypass - )); - assert!(!passes_balance( - &orders[1], - U256::ONE, - settlement_contract, - &no_bypass - )); - assert!(passes_balance( - &orders[2], - U256::ONE, - settlement_contract, - &no_bypass - )); - assert!(!passes_balance( - &orders[3], - U256::ZERO, - settlement_contract, - &no_bypass - )); - assert!(passes_balance( - &orders[4], - U256::ZERO, - settlement_contract, - &no_bypass - )); + assert!(passes_balance(&orders[0], U256::from(2), &no_bypass)); + assert!(!passes_balance(&orders[1], U256::ONE, &no_bypass)); + assert!(passes_balance(&orders[2], U256::ONE, &no_bypass)); + assert!(!passes_balance(&orders[3], U256::ZERO, &no_bypass)); } #[test] fn passes_balance_bypasses_eip1271_and_wrappers() { - let settlement_contract = Address::repeat_byte(1); - let eip1271_order = Order { data: OrderData { sell_token: Address::with_last_byte(7), @@ -790,45 +732,15 @@ mod tests { let empty_set = HashSet::with_hasher(FbBuildHasher::default()); // EIP-1271 always bypasses regardless of the exempt set. - assert!(passes_balance( - &eip1271_order, - U256::ZERO, - settlement_contract, - &empty_set - )); - assert!(passes_balance( - &eip1271_order, - U256::ZERO, - settlement_contract, - &wrapper_set - )); + assert!(passes_balance(&eip1271_order, U256::ZERO, &empty_set)); + assert!(passes_balance(&eip1271_order, U256::ZERO, &wrapper_set)); // Wrapper order bypasses only when its uid is in the exempt set. - assert!(!passes_balance( - &wrapper_order, - U256::ZERO, - settlement_contract, - &empty_set - )); - assert!(passes_balance( - &wrapper_order, - U256::ZERO, - settlement_contract, - &wrapper_set - )); + assert!(!passes_balance(&wrapper_order, U256::ZERO, &empty_set)); + assert!(passes_balance(&wrapper_order, U256::ZERO, &wrapper_set)); // Regular order without a matching balance entry always fails. - assert!(!passes_balance( - ®ular_order, - U256::ZERO, - settlement_contract, - &empty_set - )); - assert!(!passes_balance( - ®ular_order, - U256::ZERO, - settlement_contract, - &wrapper_set - )); + assert!(!passes_balance(®ular_order, U256::ZERO, &empty_set)); + assert!(!passes_balance(®ular_order, U256::ZERO, &wrapper_set)); } } From 6477d18f7d0cee3baa703664dfa4b22341ef79ab Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Thu, 30 Jul 2026 10:38:03 +0000 Subject: [PATCH 4/9] separate function --- crates/autopilot/src/solvable_orders.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index f247cee3b6..404019ab10 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -161,10 +161,7 @@ struct FilteredOrders { } impl FilteredOrders { - /// Emits per-reason metrics and, when `store_events` is set, forwards each - /// reason's uids to persistence with the correct event label. Consumes - /// `self` so the uid vecs can be moved into the background storage task - /// without copying. + /// Handles all the observability (metrics, logging, upload order events). fn report(self, persistence: &infra::Persistence, store_events: bool) { Metrics::track_filtered_orders(UnsupportedToken, &self.token_deny_listed); Metrics::track_filtered_orders(InvalidSignature, &self.presig_pending); @@ -174,11 +171,18 @@ impl FilteredOrders { Metrics::track_filtered_orders(InsufficientBalance, &self.insufficient_balance); Metrics::track_filtered_orders(DustOrder, &self.dust); - if !store_events { - return; + if store_events { + self.store_order_events(persistence) } + } + /// Uploads order debug events to the database in separate + /// background tasks. + fn store_order_events(self, persistence: &infra::Persistence) { let store = |uids: Vec, label, reason| { + if uids.is_empty() { + return; + } persistence.store_order_events_owned( uids, |uid| domain::OrderUid(uid.0), @@ -511,6 +515,7 @@ impl SolvableOrdersCache { } /// Returns true if either of the order's tokens is on the deny list. +#[inline(always)] fn token_deny_listed(order: &Order, deny_listed_tokens: &DenyListedTokens) -> bool { deny_listed_tokens.contains(&order.data.sell_token) || deny_listed_tokens.contains(&order.data.buy_token) @@ -518,6 +523,7 @@ fn token_deny_listed(order: &Order, deny_listed_tokens: &DenyListedTokens) -> bo /// Returns true if the order is waiting for a pre-signature. EIP-1271 orders /// are validated by the driver before settlement, so we don't check them here. +#[inline(always)] fn is_presig_pending(order: &Order) -> bool { matches!( order.metadata.status, @@ -527,6 +533,7 @@ fn is_presig_pending(order: &Order) -> bool { /// Returns true if the order has sufficient balance to be settled. EIP-1271 /// orders and orders exempt via a wrapper interaction bypass the check. +#[inline(always)] fn passes_balance( order: &Order, balance: U256, @@ -550,6 +557,7 @@ fn passes_balance( /// Returns true if the order is not a dust order — its remaining sell and /// buy amounts (scaled by balance) are both non-zero. +#[inline(always)] fn passes_dust(order: &Order, balance: U256) -> bool { let Ok(remaining) = remaining_amounts::Remaining::from_order_with_balance(&order.into(), balance) From 3357ad1c1308f0b4c9fbb20f0b0e75b4a53a0dad Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Thu, 30 Jul 2026 11:06:43 +0000 Subject: [PATCH 5/9] minor optimizations --- crates/autopilot/src/solvable_orders.rs | 61 +++++++++++++------------ 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index 404019ab10..8a30f539fc 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -267,9 +267,28 @@ impl SolvableOrdersCache { let _timer = observe::metrics::metrics() .on_auction_overhead_start("autopilot", "update_solvabe_orders"); - let db_solvable_orders = self.get_solvable_orders().await?; + let (db_solvable_orders, in_flight) = tokio::try_join!( + self.get_solvable_orders(), + self.fetch_in_flight_orders(block).map(Ok), + )?; tracing::trace!("fetched solvable orders from db"); + // Exclude any owner that already has an order in-flight (i.e. won a previous + // auction and is being settled on-chain). A surplus-capturing JIT order created + // on its behalf could conflict with the settling order, so we drop the owner + // from this auction until the in-flight order clears. + let surplus_capturing_jit_order_owners: Vec
= { + let in_flight_owners: AddressHashSet = in_flight + .iter() + .map(|uid| domain::OrderUid(uid.0).owner()) + .collect(); + self.surplus_capturing_jit_order_owners + .iter() + .filter(|owner| !in_flight_owners.contains(*owner)) + .copied() + .collect() + }; + // Phase 1: single-pass sync pre-filter that also collects everything // needed for the concurrent I/O in phase 2. let mut prices = self.native_price_estimator.cached_prices(); @@ -298,6 +317,10 @@ impl SolvableOrdersCache { traded_tokens.insert(order.data.sell_token); traded_tokens.insert(order.data.buy_token); + if in_flight.contains(&uid) { + filtered.in_flight.push(uid); + continue; + } if token_deny_listed(order, &self.deny_listed_tokens) { filtered.token_deny_listed.push(uid); continue; @@ -317,12 +340,14 @@ impl SolvableOrdersCache { if let Some(receiver) = order.data.receiver { traders.insert(receiver); } - balance_queries.push(Query::from_order(order)); - if self.wrapper_cache.has_wrappers( - &order.data.app_data, - order.metadata.full_app_data.as_deref(), - ) { - balance_filter_exempt.insert(uid); + if !self.disable_order_balance_filter { + balance_queries.push(Query::from_order(order)); + if self.wrapper_cache.has_wrappers( + &order.data.app_data, + order.metadata.full_app_data.as_deref(), + ) { + balance_filter_exempt.insert(uid); + } } survivors.push(order); } @@ -333,36 +358,16 @@ impl SolvableOrdersCache { .schedule_token_updates(traded_tokens); // Phase 2: concurrent I/O based on phase-1 outputs. - let (in_flight, banned_set, balances) = tokio::join!( - self.fetch_in_flight_orders(block), + let (banned_set, balances) = tokio::join!( self.timed_future("banned_user_filtering", self.banned_users.banned(traders)), self.fetch_balances(balance_queries), ); // Phase 3: final pass using data from phase-2 - // Exclude any owner that already has an order in-flight (i.e. won a previous - // auction and is being settled on-chain). A surplus-capturing JIT order created - // on its behalf could conflict with the settling order, so we drop the owner - // from this auction until the in-flight order clears. - let in_flight_owners: AddressHashSet = in_flight - .iter() - .map(|uid| domain::OrderUid(uid.0).owner()) - .collect(); - let surplus_capturing_jit_order_owners: Vec
= self - .surplus_capturing_jit_order_owners - .iter() - .filter(|owner| !in_flight_owners.contains(*owner)) - .copied() - .collect(); - let final_orders = survivors .into_iter() .filter_map(|order| { let uid = order.metadata.uid; - if in_flight.contains(&uid) { - filtered.in_flight.push(uid); - return None; - } let is_banned = banned_set.contains(&order.metadata.owner) || order.data.receiver.is_some_and(|r| banned_set.contains(&r)); if is_banned { From 8f73153c2b4963ffb81bd28797ce3e9a0f314998 Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Thu, 30 Jul 2026 12:58:46 +0000 Subject: [PATCH 6/9] don't use FbHasher in moka --- crates/price-estimation/src/native_price_cache.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/price-estimation/src/native_price_cache.rs b/crates/price-estimation/src/native_price_cache.rs index daccccf79e..f504afb10b 100644 --- a/crates/price-estimation/src/native_price_cache.rs +++ b/crates/price-estimation/src/native_price_cache.rs @@ -170,7 +170,7 @@ pub struct Cache(Arc); const MAX_CACHE_SIZE: u64 = 20_000; struct CacheInner { - data: moka::sync::Cache>, + data: moka::sync::Cache, max_age: Duration, } @@ -179,10 +179,9 @@ impl Cache { let mut rng = rand::rng(); let now = std::time::Instant::now(); - let data: moka::sync::Cache> = - moka::sync::Cache::builder() - .max_capacity(MAX_CACHE_SIZE) - .build_with_hasher(FbBuildHasher::<20>::default()); + let data: moka::sync::Cache = moka::sync::Cache::builder() + .max_capacity(MAX_CACHE_SIZE) + .build(); for (token, price) in initial_prices { if let Some(price) = from_normalized_price(price) { @@ -233,7 +232,7 @@ impl Cache { fn get_cached_price( token: Address, now: Instant, - cache: &moka::sync::Cache>, + cache: &moka::sync::Cache, max_age: &Duration, ) -> Option { let entry = cache.get(&token)?; @@ -269,7 +268,7 @@ impl Cache { fn get_cached_prices( &self, tokens: &[Address], - ) -> HashMap, FbBuildHasher<20>> { + ) -> AddressHashMap> { let now = Instant::now(); let mut results = HashMap::default(); for token in tokens { From a2569b4d6e5382de341ff8c69c9d5523f72fc9a1 Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Thu, 30 Jul 2026 13:08:47 +0000 Subject: [PATCH 7/9] fixup --- crates/autopilot/src/solvable_orders.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index 8a30f539fc..97df7bffca 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -85,9 +85,6 @@ pub struct Metrics { /// Auction filtered orders grouped by class. #[metric(labels("reason"))] auction_filtered_orders: IntGaugeVec, - - /// Auction filtered market orders due to missing native token price. - auction_market_order_missing_price: IntGauge, } impl Metrics { @@ -375,7 +372,10 @@ impl SolvableOrdersCache { return None; } if !self.disable_order_balance_filter { - let balance = *balances.get(&Query::from_order(order))?; + let Some(&balance) = balances.get(&Query::from_order(order)) else { + filtered.insufficient_balance.push(uid); + return None; + }; if !passes_balance(order, balance, &balance_filter_exempt) { filtered.insufficient_balance.push(uid); @@ -550,7 +550,7 @@ fn passes_balance( return true; } - if order.data.partially_fillable && balance.is_zero() { + if order.data.partially_fillable && !balance.is_zero() { return true; } From f375d92e4b7c6805b49c9b90dbe997738c5dbc1e Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Thu, 30 Jul 2026 13:11:17 +0000 Subject: [PATCH 8/9] fix unit test + clippy --- crates/autopilot/src/solvable_orders.rs | 1 - crates/configs/src/autopilot/mod.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index 97df7bffca..ac0d1a3ed6 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -37,7 +37,6 @@ use { HistogramVec, IntCounter, IntCounterVec, - IntGauge, IntGaugeVec, core::{AtomicU64, GenericGauge}, }, diff --git a/crates/configs/src/autopilot/mod.rs b/crates/configs/src/autopilot/mod.rs index f76c9837ed..53ff846c73 100644 --- a/crates/configs/src/autopilot/mod.rs +++ b/crates/configs/src/autopilot/mod.rs @@ -298,7 +298,6 @@ mod tests { surplus-capturing-jit-order-owners = ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"] min-order-validity-period = "2m" max-auction-age = "10m" - native-price-timeout = "3s" [[drivers]] name = "solver1" From a5458f8fc641caf1b031f4b42166b7a4b476888e Mon Sep 17 00:00:00 2001 From: MartinquaXD Date: Thu, 30 Jul 2026 14:24:26 +0000 Subject: [PATCH 9/9] make e2e test pass --- crates/configs/src/autopilot/native_price.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/configs/src/autopilot/native_price.rs b/crates/configs/src/autopilot/native_price.rs index 7ebf0f77bc..d2e049a601 100644 --- a/crates/configs/src/autopilot/native_price.rs +++ b/crates/configs/src/autopilot/native_price.rs @@ -5,7 +5,7 @@ use { }; const fn default_native_price_cache_refresh() -> Duration { - Duration::from_secs(1) + Duration::from_millis(100) } const fn default_native_price_prefetch_time() -> Duration {