From 15bbd00a72c37e589152f1f4d6a00d4ee6adb6a6 Mon Sep 17 00:00:00 2001 From: Felix Leupold <1200333+fleupold@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:31:04 +0200 Subject: [PATCH 1/2] Initial implementation --- crates/model/src/quote.rs | 10 +++--- crates/orderbook/openapi.yml | 13 ++++++-- crates/orderbook/src/quoter.rs | 16 +++++++-- crates/orderbook/src/run.rs | 26 +++++++++++++-- crates/price-estimation/src/factory.rs | 46 ++++++++++++++++++-------- 5 files changed, 85 insertions(+), 26 deletions(-) diff --git a/crates/model/src/quote.rs b/crates/model/src/quote.rs index 361d9ed85b..2890ead7a6 100644 --- a/crates/model/src/quote.rs +++ b/crates/model/src/quote.rs @@ -27,10 +27,12 @@ use { pub enum PriceQuality { /// We pick the best quote of the fastest `n` price estimators. Fast, - #[default] - /// We pick the best quote of all price estimators. + /// We pick the best quote of all price estimators, ranked purely by the + /// promised price without verifying quotes by simulation. Optimal, - /// Quotes may by discarde when they failed to be verified by simulation. + #[default] + /// Quotes get verified by simulation whenever possible and verified + /// quotes are preferred over unverified ones, even at a worse price. Verified, } @@ -389,7 +391,7 @@ mod tests { "buyTokenBalance": "erc20", "signingScheme": "eip712", "timeout": null, - "priceQuality": "optimal", + "priceQuality": "verified", }) ); } diff --git a/crates/orderbook/openapi.yml b/crates/orderbook/openapi.yml index 3c636fed5b..025ed581b7 100644 --- a/crates/orderbook/openapi.yml +++ b/crates/orderbook/openapi.yml @@ -1004,9 +1004,16 @@ components: How good should the price estimate be? Fast: The price estimate is chosen among the fastest N price estimates. - Optimal: The price estimate is chosen among all price estimates. - Verified: The price estimate is chosen among all verified/simulated - price estimates. + Estimates do not get verified by + simulation. + Optimal: The price estimate is chosen among all price estimates, ranked + purely by the promised price. Estimates do not get verified by + simulation. + Verified: All price estimates get verified by simulation whenever + possible and verified estimates are preferred over unverified ones, + even when an unverified estimate promises a better price. The + response's `verified` flag indicates whether the returned estimate + was actually verified. **NOTE**: Orders are supposed to be created from `verified` or `optimal` price estimates. diff --git a/crates/orderbook/src/quoter.rs b/crates/orderbook/src/quoter.rs index b4f9c2d39a..264305c57b 100644 --- a/crates/orderbook/src/quoter.rs +++ b/crates/orderbook/src/quoter.rs @@ -64,6 +64,7 @@ impl AdjustedQuoteData { pub struct QuoteHandler { order_validator: Arc, optimal_quoter: Arc, + verified_quoter: Arc, fast_quoter: Arc, app_data: Arc, volume_fee: Option, @@ -93,6 +94,7 @@ impl QuoteHandler { Self { order_validator, optimal_quoter: quoter.clone(), + verified_quoter: quoter.clone(), fast_quoter: quoter, app_data, volume_fee, @@ -107,6 +109,11 @@ impl QuoteHandler { self } + pub fn with_verified_quoter(mut self, verified_quoter: Arc) -> Self { + self.verified_quoter = verified_quoter; + self + } + pub fn with_streaming_quoter(mut self, quoter: Arc) -> Self { self.streaming_quoter = Some(quoter); self @@ -126,9 +133,12 @@ impl QuoteHandler { let quote = match request.price_quality { PriceQuality::Optimal | PriceQuality::Verified => { - let competition = self.optimal_quoter.calculate_quote(params.clone()).await?; - let id = self - .optimal_quoter + let quoter = match request.price_quality { + PriceQuality::Verified => &self.verified_quoter, + _ => &self.optimal_quoter, + }; + let competition = quoter.calculate_quote(params.clone()).await?; + let id = quoter .store_quote(competition.clone()) .await .map_err(CalculateQuoteError::Other)?; diff --git a/crates/orderbook/src/run.rs b/crates/orderbook/src/run.rs index f16397511e..5999c27323 100644 --- a/crates/orderbook/src/run.rs +++ b/crates/orderbook/src/run.rs @@ -373,10 +373,29 @@ pub async fn run(config: Configuration) { ) }; - let optimal_quoter = Arc::new( + let verified_quoter = Arc::new( create_quoter(price_estimator.clone()).with_streaming_estimator(price_estimator.clone()), ); + let unverified_price_estimator = price_estimator_factory + .unverified_price_estimator( + &config + .order_quoting + .price_estimation_drivers + .iter() + .map( + |price_estimator_driver| configs::native_price_estimators::ExternalSolver { + name: price_estimator_driver.name.clone(), + url: price_estimator_driver.url.clone(), + }, + ) + .collect::>(), + native_price_estimator.clone(), + gas_price_estimator.clone(), + ) + .unwrap(); + let optimal_quoter = Arc::new(create_quoter(unverified_price_estimator)); + // Fast quoting is able to return early and if none of the produced quotes are // verifiable we are left with no quote at all. Since fast estimates don't // make any promises on correctness we can just skip quote verification for @@ -418,7 +437,7 @@ pub async fn run(config: Configuration) { config.eip1271_skip_creation_validation, deny_listed_tokens.clone(), hooks_contract, - optimal_quoter.clone(), + verified_quoter.clone(), balance_fetcher, signature_validator, validator_simulator, @@ -471,8 +490,9 @@ pub async fn run(config: Configuration) { *native_token.address(), token_info_fetcher.clone(), ) + .with_verified_quoter(verified_quoter.clone()) .with_fast_quoter(fast_quoter) - .with_streaming_quoter(optimal_quoter.clone()); + .with_streaming_quoter(verified_quoter.clone()); let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel(); let serve_api = serve_api( diff --git a/crates/price-estimation/src/factory.rs b/crates/price-estimation/src/factory.rs index 8e8706d501..e5eae71e59 100644 --- a/crates/price-estimation/src/factory.rs +++ b/crates/price-estimation/src/factory.rs @@ -37,8 +37,11 @@ use { #[derive(Clone)] struct EstimatorEntry { - optimal: Arc, - fast: Arc, + /// The estimator wrapped in trade verification (falls back to the + /// unverified estimator when no trade verifier is configured). + verified: Arc, + /// The raw estimator without trade verification. + unverified: Arc, native: Arc, } @@ -185,24 +188,24 @@ impl<'a> PriceEstimatorFactory<'a> { .as_ref() .and_then(|trade_verifier| estimator.verified(trade_verifier)); - let fast = instrument(estimator, name); - let optimal = match verified { + let unverified = instrument(estimator, name); + let verified = match verified { Some(verified) => instrument(verified, name), - None => fast.clone(), + None => unverified.clone(), }; // Eagerly create the native price estimator, even if we don't use it. // It just simplifies price estimator creation code and only costs a few // extra cycles during initialization. Also note that we intentionally - // don't share price estimators between optimal/fast and the native - // price estimator (this is because request sharing isn't benificial), - // nor do we configure the trade verifier (because external price - // precision is less critical). + // don't share price estimators between verified/unverified and the + // native price estimator (this is because request sharing isn't + // benificial), nor do we configure the trade verifier (because + // external price precision is less critical). let native = instrument(T::init(self, name, params)?, name); Ok(EstimatorEntry { - optimal, - fast, + verified, + unverified, native, }) } @@ -385,13 +388,30 @@ impl<'a> PriceEstimatorFactory<'a> { native: Arc, gas: Arc, ) -> Result>>> { - let estimators = self.get_estimators(solvers, |entry| &entry.optimal)?; + let estimators = self.get_estimators(solvers, |entry| &entry.verified)?; Ok(Arc::new( self.sanitized_competition(estimators, PriceRanking::BestBangForBuck { native, gas }) .with_verification(self.args.quote_verification), )) } + /// Like [`Self::price_estimator`] but without quote verification: all + /// estimators are queried and the best quote wins ranked purely by the + /// promised price, without simulating estimates or preferring verified + /// ones. + pub fn unverified_price_estimator( + &mut self, + solvers: &[ExternalSolver], + native: Arc, + gas: Arc, + ) -> Result>>> { + let estimators = self.get_estimators(solvers, |entry| &entry.unverified)?; + Ok(Arc::new(self.sanitized_competition( + estimators, + PriceRanking::BestBangForBuck { native, gas }, + ))) + } + pub fn fast_price_estimator( &mut self, solvers: &[ExternalSolver], @@ -399,7 +419,7 @@ impl<'a> PriceEstimatorFactory<'a> { native: Arc, gas: Arc, ) -> Result>>> { - let estimators = self.get_estimators(solvers, |entry| &entry.fast)?; + let estimators = self.get_estimators(solvers, |entry| &entry.unverified)?; Ok(Arc::new( self.sanitized_competition(estimators, PriceRanking::BestBangForBuck { native, gas }) .with_early_return(fast_price_estimation_results_required), From 1260bfc96ec44b3f6a46fe0f0d8e1dce216e2d17 Mon Sep 17 00:00:00 2001 From: Felix Leupold <1200333+fleupold@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:48:03 +0200 Subject: [PATCH 2/2] adjust e2e test --- crates/e2e/tests/e2e/quote_verification.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/e2e/tests/e2e/quote_verification.rs b/crates/e2e/tests/e2e/quote_verification.rs index dbfc6a00c7..db12741b22 100644 --- a/crates/e2e/tests/e2e/quote_verification.rs +++ b/crates/e2e/tests/e2e/quote_verification.rs @@ -9,7 +9,7 @@ use { contracts::ERC20, e2e::setup::*, ethrpc::{Web3, alloy::CallBuilderExt}, - model::quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount}, + model::quote::{OrderQuoteRequest, OrderQuoteSide, PriceQuality, SellAmount}, number::units::EthUnit, serde_json::json, }; @@ -115,6 +115,25 @@ async fn standard_verified_quote(web3: Web3) { .await .unwrap(); assert!(response.verified); + + // The same quote requested with `optimal` price quality skips + // verification entirely and reports the solver's unverified promise. + let response = services + .submit_quote(&OrderQuoteRequest { + from: trader.address(), + sell_token: *token.address(), + buy_token: *onchain.contracts().weth.address(), + side: OrderQuoteSide::Sell { + sell_amount: SellAmount::BeforeFee { + value: (1u64.eth()).try_into().unwrap(), + }, + }, + price_quality: PriceQuality::Optimal, + ..Default::default() + }) + .await + .unwrap(); + assert!(!response.verified); } /// Verified quotes work as for WETH trades without wrapping or approvals.