Skip to content

Commit 6fe22d1

Browse files
committed
Fix quote verification for sell=buy
1 parent 7e9c82c commit 6fe22d1

File tree

6 files changed

+232
-77
lines changed

6 files changed

+232
-77
lines changed

crates/e2e/tests/e2e/submission.rs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ use {
99
futures::{Stream, StreamExt},
1010
model::{
1111
order::{OrderCreation, OrderKind},
12+
quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount},
1213
signature::EcdsaSigningScheme,
1314
},
15+
number::nonzero::U256 as NonZeroU256,
1416
secp256k1::SecretKey,
1517
shared::ethrpc::Web3,
1618
std::time::Duration,
@@ -280,19 +282,53 @@ async fn test_execute_same_sell_and_buy_token(web3: Web3) {
280282
.await
281283
.expect("Must be able to disable automine");
282284

285+
tracing::info!("Quoting");
286+
let quote_sell_amount = to_wei(1);
287+
let quote_request = OrderQuoteRequest {
288+
from: trader.address(),
289+
sell_token: *token.address(),
290+
buy_token: *token.address(),
291+
side: OrderQuoteSide::Sell {
292+
sell_amount: SellAmount::BeforeFee {
293+
value: NonZeroU256::try_from(quote_sell_amount).unwrap(),
294+
},
295+
},
296+
..Default::default()
297+
};
298+
let quote_response = services.submit_quote(&quote_request).await.unwrap();
299+
tracing::info!(?quote_response);
300+
assert!(quote_response.id.is_some());
301+
assert!(quote_response.verified);
302+
assert!(quote_response.quote.buy_amount.into_legacy() < quote_sell_amount);
303+
304+
let quote_metadata =
305+
crate::database::quote_metadata(services.db(), quote_response.id.unwrap()).await;
306+
assert!(quote_metadata.is_some());
307+
tracing::debug!(?quote_metadata);
308+
283309
tracing::info!("Placing order");
284310
let initial_balance = token.balanceOf(trader.address()).call().await.unwrap();
285311
assert_eq!(initial_balance, eth(10));
286312

287-
let order_sell_amount = to_wei(2);
288-
let order_buy_amount = to_wei(1);
313+
// let quote_sell_amount = to_wei(2);
314+
// let order_buy_amount = to_wei(1);
315+
// let order = OrderCreation {
316+
// sell_token: token.address().into_legacy(),
317+
// sell_amount: order_sell_amount,
318+
// buy_token: token.address().into_legacy(),
319+
// buy_amount: order_buy_amount,
320+
// valid_to: model::time::now_in_epoch_seconds() + 300,
321+
// kind: OrderKind::Sell,
322+
// ..Default::default()
323+
// }
289324
let order = OrderCreation {
325+
kind: OrderKind::Sell,
290326
sell_token: token.address().into_legacy(),
291-
sell_amount: order_sell_amount,
292327
buy_token: token.address().into_legacy(),
293-
buy_amount: order_buy_amount,
328+
quote_id: quote_response.id,
329+
sell_amount: quote_sell_amount,
330+
buy_amount: quote_response.quote.buy_amount.into_legacy(),
294331
valid_to: model::time::now_in_epoch_seconds() + 300,
295-
kind: OrderKind::Sell,
296332
..Default::default()
297333
}
298334
.sign(
@@ -314,7 +350,7 @@ async fn test_execute_same_sell_and_buy_token(web3: Web3) {
314350
onchain.mint_block().await;
315351

316352
// Wait for settlement tx to appear in txpool
317-
wait_for_condition(Duration::from_secs(2), || async {
353+
wait_for_condition(TIMEOUT, || async {
318354
get_pending_tx(solver.account().address(), &web3)
319355
.await
320356
.is_some()

crates/shared/src/price_estimation/factory.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ impl<'a> PriceEstimatorFactory<'a> {
194194
driver.name.clone(),
195195
Arc::new(InstrumentedPriceEstimator::new(
196196
NativePriceEstimator::new(
197-
Arc::new(self.sanitized(estimator)),
197+
Arc::new(self.sanitized_native_price(estimator)),
198198
self.network.native_token,
199199
native_token_price_estimation_amount,
200200
),
@@ -303,6 +303,21 @@ impl<'a> PriceEstimatorFactory<'a> {
303303
estimator,
304304
self.network.native_token,
305305
self.components.bad_token_detector.clone(),
306+
false, // not estimating native price
307+
)
308+
}
309+
310+
/// Creates a SanitizedPRiceEstimator that is used for native price
311+
/// estimations
312+
fn sanitized_native_price(
313+
&self,
314+
estimator: Arc<dyn PriceEstimating>,
315+
) -> SanitizedPriceEstimator {
316+
SanitizedPriceEstimator::new(
317+
estimator,
318+
self.network.native_token,
319+
self.components.bad_token_detector.clone(),
320+
true, // estimating native price
306321
)
307322
}
308323

crates/shared/src/price_estimation/sanitized.rs

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,22 @@ pub struct SanitizedPriceEstimator {
2424
inner: Arc<dyn PriceEstimating>,
2525
bad_token_detector: Arc<dyn BadTokenDetecting>,
2626
native_token: Address,
27+
/// Enables the short-circuiting logic in case of sell and buy is the same
28+
is_estimating_native_price: bool,
2729
}
2830

2931
impl SanitizedPriceEstimator {
3032
pub fn new(
3133
inner: Arc<dyn PriceEstimating>,
3234
native_token: Address,
3335
bad_token_detector: Arc<dyn BadTokenDetecting>,
36+
is_estimating_native_price: bool,
3437
) -> Self {
3538
Self {
3639
inner,
3740
native_token,
3841
bad_token_detector,
42+
is_estimating_native_price,
3943
}
4044
}
4145

@@ -64,7 +68,10 @@ impl PriceEstimating for SanitizedPriceEstimator {
6468
self.handle_bad_tokens(&query).await?;
6569

6670
// buy_token == sell_token => 1 to 1 conversion
67-
if query.buy_token == query.sell_token {
71+
// Only in case of native price estimation.
72+
// For regular price estimation, the sell and buy tokens can
73+
// be the same and should be priced as usual
74+
if self.is_estimating_native_price && query.buy_token == query.sell_token {
6875
let estimation = Estimate {
6976
out_amount: query.in_amount.get().into_alloy(),
7077
gas: 0,
@@ -454,11 +461,12 @@ mod tests {
454461
}
455462
.boxed()
456463
});
457-
464+
let bad_token_detector = Arc::new(bad_token_detector);
458465
let sanitized_estimator = SanitizedPriceEstimator {
459466
inner: Arc::new(wrapped_estimator),
460-
bad_token_detector: Arc::new(bad_token_detector),
467+
bad_token_detector: bad_token_detector.clone(),
461468
native_token,
469+
is_estimating_native_price: true,
462470
};
463471

464472
for (query, expectation) in queries {
@@ -473,5 +481,110 @@ mod tests {
473481
}
474482
}
475483
}
484+
485+
let queries = [
486+
// Can be estimated by `sanitized_estimator` because `buy_token` and `sell_token` are
487+
// identical.
488+
(
489+
Query {
490+
verification: Default::default(),
491+
sell_token: Address::with_last_byte(1),
492+
buy_token: Address::with_last_byte(1),
493+
in_amount: NonZeroU256::try_from(1).unwrap(),
494+
kind: OrderKind::Sell,
495+
block_dependent: false,
496+
timeout: HEALTHY_PRICE_ESTIMATION_TIME,
497+
},
498+
Ok(Estimate {
499+
out_amount: AlloyU256::ONE,
500+
gas: 100,
501+
solver: Default::default(),
502+
verified: true,
503+
execution: Default::default(),
504+
}),
505+
),
506+
(
507+
Query {
508+
verification: Default::default(),
509+
sell_token: native_token,
510+
buy_token: native_token,
511+
in_amount: NonZeroU256::try_from(1).unwrap(),
512+
kind: OrderKind::Sell,
513+
block_dependent: false,
514+
timeout: HEALTHY_PRICE_ESTIMATION_TIME,
515+
},
516+
Ok(Estimate {
517+
out_amount: AlloyU256::ONE,
518+
gas: 100,
519+
solver: Default::default(),
520+
verified: true,
521+
execution: Default::default(),
522+
}),
523+
),
524+
];
525+
526+
// SanitizedPriceEstimator will simply forward the Query in the sell=buy case
527+
// if it is not calculating native price
528+
let first_forwarded_query = queries[0].0.clone();
529+
530+
// SanitizedPriceEstimator will simply forward the Query if sell=buy of native
531+
// token case if it is not calculating the native price
532+
let second_forwarded_query = queries[1].0.clone();
533+
534+
let mut wrapped_estimator = MockPriceEstimating::new();
535+
wrapped_estimator
536+
.expect_estimate()
537+
.times(1)
538+
.withf(move |query| **query == first_forwarded_query)
539+
.returning(|_| {
540+
async {
541+
Ok(Estimate {
542+
out_amount: AlloyU256::ONE,
543+
gas: 100,
544+
solver: Default::default(),
545+
verified: true,
546+
execution: Default::default(),
547+
})
548+
}
549+
.boxed()
550+
});
551+
wrapped_estimator
552+
.expect_estimate()
553+
.times(1)
554+
.withf(move |query| **query == second_forwarded_query)
555+
.returning(|_| {
556+
async {
557+
Ok(Estimate {
558+
out_amount: AlloyU256::ONE,
559+
gas: 100,
560+
solver: Default::default(),
561+
verified: true,
562+
execution: Default::default(),
563+
})
564+
}
565+
.boxed()
566+
});
567+
568+
let sanitized_estimator_non_native = SanitizedPriceEstimator {
569+
inner: Arc::new(wrapped_estimator),
570+
bad_token_detector,
571+
native_token,
572+
is_estimating_native_price: false,
573+
};
574+
575+
for (query, expectation) in queries {
576+
let result = sanitized_estimator_non_native
577+
.estimate(Arc::new(query))
578+
.await;
579+
match result {
580+
Ok(estimate) => assert_eq!(estimate, expectation.unwrap()),
581+
Err(err) => {
582+
// we only compare the error variant; everything else would be a PITA
583+
let reported_error = std::mem::discriminant(&err);
584+
let expected_error = std::mem::discriminant(&expectation.unwrap_err());
585+
assert_eq!(reported_error, expected_error);
586+
}
587+
}
588+
}
476589
}
477590
}

crates/shared/src/price_estimation/trade_verifier/mod.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use {
1919
::alloy::sol_types::SolCall,
2020
alloy::primitives::{Address, address},
2121
anyhow::{Context, Result, anyhow},
22-
bigdecimal::BigDecimal,
22+
bigdecimal::{BigDecimal, Zero},
2323
contracts::alloy::{
2424
GPv2Settlement,
2525
WETH9,
@@ -766,8 +766,11 @@ fn add_balance_queries(
766766
alloy::primitives::U256::ZERO,
767767
query_balance_call.into(),
768768
);
769-
// query balance query at the end of pre-interactions
770-
settlement.interactions[0].push(interaction.clone());
769+
770+
// query the balance as the first interaction, the sell token has already
771+
// been transferred into the settlement contract since the calculation is
772+
// based on the out amount
773+
settlement.interactions[1].insert(0, interaction.clone());
771774
// query balance right after we payed out all `buy_token`
772775
settlement.interactions[2].insert(0, interaction);
773776
settlement
@@ -864,7 +867,7 @@ fn ensure_quote_accuracy(
864867
.get(&query.buy_token)
865868
.context("summary buy token is missing")?;
866869

867-
if *sell_token_lost >= sell_token_lost_limit || *buy_token_lost >= buy_token_lost_limit {
870+
if (!sell_token_lost.is_zero() && *sell_token_lost >= sell_token_lost_limit) || (!buy_token_lost.is_zero() && *buy_token_lost >= buy_token_lost_limit) {
868871
return Err(Error::TooInaccurate);
869872
}
870873

crates/solvers/src/boundary/baseline.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ impl<'a> Solver<'a> {
4646
&self,
4747
request: solver::Request,
4848
max_hops: usize,
49-
) -> Result<Option<solver::Route<'a>>, RouteComputationError> {
49+
) -> Option<solver::Route<'a>> {
5050
if request.sell.token == request.buy.token {
5151
tracing::info!("Sell=Buy, returning empty route");
52-
return Ok(None);
52+
return Some(solver::Route::new(vec![]));
5353
}
5454
let candidates = self.base_tokens.path_candidates_with_hops(
5555
request.sell.token.0.into_alloy(),
@@ -87,8 +87,7 @@ impl<'a> Solver<'a> {
8787
.await
8888
.into_iter()
8989
.flatten()
90-
.min_by_key(|(_, sell)| sell.value)
91-
.ok_or(RouteComputationError)?
90+
.min_by_key(|(_, sell)| sell.value)?
9291
}
9392
order::Side::Sell => {
9493
let futures = candidates.iter().map(|path| async {
@@ -119,12 +118,11 @@ impl<'a> Solver<'a> {
119118
.await
120119
.into_iter()
121120
.flatten()
122-
.max_by_key(|(_, buy)| buy.value)
123-
.ok_or(RouteComputationError)?
121+
.max_by_key(|(_, buy)| buy.value)?
124122
}
125123
};
126124

127-
solver::Route::new(segments).map(Ok).transpose()
125+
Some(solver::Route::new(segments))
128126
}
129127

130128
async fn traverse_path(

0 commit comments

Comments
 (0)