Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion crates/e2e/tests/e2e/quote_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions crates/model/src/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -389,7 +391,7 @@ mod tests {
"buyTokenBalance": "erc20",
"signingScheme": "eip712",
"timeout": null,
"priceQuality": "optimal",
"priceQuality": "verified",
})
);
}
Expand Down
13 changes: 10 additions & 3 deletions crates/orderbook/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +1007 to +1016

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
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.
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.
Expand Down
16 changes: 13 additions & 3 deletions crates/orderbook/src/quoter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ impl AdjustedQuoteData {
pub struct QuoteHandler {
order_validator: Arc<dyn OrderValidating>,
optimal_quoter: Arc<dyn OrderQuoting>,
verified_quoter: Arc<dyn OrderQuoting>,
fast_quoter: Arc<dyn OrderQuoting>,
app_data: Arc<app_data::Registry>,
volume_fee: Option<VolumeFeeConfig>,
Expand Down Expand Up @@ -93,6 +94,7 @@ impl QuoteHandler {
Self {
order_validator,
optimal_quoter: quoter.clone(),
verified_quoter: quoter.clone(),
fast_quoter: quoter,
app_data,
volume_fee,
Expand All @@ -107,6 +109,11 @@ impl QuoteHandler {
self
}

pub fn with_verified_quoter(mut self, verified_quoter: Arc<dyn OrderQuoting>) -> Self {
self.verified_quoter = verified_quoter;
self
}

pub fn with_streaming_quoter(mut self, quoter: Arc<dyn StreamingQuoting>) -> Self {
self.streaming_quoter = Some(quoter);
self
Expand All @@ -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)?;
Expand Down
26 changes: 23 additions & 3 deletions crates/orderbook/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>(),
native_price_estimator.clone(),
gas_price_estimator.clone(),
)
.unwrap();
Comment on lines +380 to +396

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This price_estimation_drivers.iter().map(...).collect() block is now repeated three times verbatim (here plus the price_estimator and fast_price_estimator calls above). Consider extracting it into a small local binding once, e.g.:

let price_estimation_solvers: Vec<_> = config
    .order_quoting
    .price_estimation_drivers
    .iter()
    .map(|d| configs::native_price_estimators::ExternalSolver {
        name: d.name.clone(),
        url: d.url.clone(),
    })
    .collect();

and pass &price_estimation_solvers to all three. Reduces the risk of the three copies drifting.

Fix this →

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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
46 changes: 33 additions & 13 deletions crates/price-estimation/src/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ use {

#[derive(Clone)]
struct EstimatorEntry {
optimal: Arc<dyn PriceEstimating>,
fast: Arc<dyn PriceEstimating>,
/// The estimator wrapped in trade verification (falls back to the
/// unverified estimator when no trade verifier is configured).
verified: Arc<dyn PriceEstimating>,
/// The raw estimator without trade verification.
unverified: Arc<dyn PriceEstimating>,
native: Arc<dyn PriceEstimating>,
}

Expand Down Expand Up @@ -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,
})
}
Expand Down Expand Up @@ -385,21 +388,38 @@ impl<'a> PriceEstimatorFactory<'a> {
native: Arc<dyn NativePriceEstimating>,
gas: Arc<dyn GasPriceEstimating>,
) -> Result<Arc<CompetitionEstimator<Arc<dyn PriceEstimating>>>> {
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<dyn NativePriceEstimating>,
gas: Arc<dyn GasPriceEstimating>,
) -> Result<Arc<CompetitionEstimator<Arc<dyn PriceEstimating>>>> {
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],
fast_price_estimation_results_required: NonZeroUsize,
native: Arc<dyn NativePriceEstimating>,
gas: Arc<dyn GasPriceEstimating>,
) -> Result<Arc<CompetitionEstimator<Arc<dyn PriceEstimating>>>> {
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),
Expand Down
Loading