Skip to content

Commit c3a04a2

Browse files
committed
Fix quote verification for sell=buy
1 parent 9de851c commit c3a04a2

File tree

6 files changed

+123
-73
lines changed

6 files changed

+123
-73
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: 9 additions & 1 deletion
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,
@@ -459,6 +466,7 @@ mod tests {
459466
inner: Arc::new(wrapped_estimator),
460467
bad_token_detector: Arc::new(bad_token_detector),
461468
native_token,
469+
is_estimating_native_price: false,
462470
};
463471

464472
for (query, expectation) in queries {

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

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(

crates/solvers/src/domain/solver.rs

Lines changed: 46 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -170,21 +170,21 @@ impl Inner {
170170
.route(native_price_request, self.max_hops)
171171
.await
172172
{
173-
Ok(Some(route)) => {
173+
Some(route) if !route.is_empty() => {
174174
// how many units of buy_token are bought for one unit of sell_token
175175
// (buy_amount / sell_amount).
176176
let price = self.native_token_price_estimation_amount.to_f64_lossy()
177-
/ route.input().amount.to_f64_lossy();
177+
/ route
178+
.input()
179+
.expect("route is not empty")
180+
.amount
181+
.to_f64_lossy();
178182
let Some(price) = to_normalized_price(price) else {
179183
continue;
180184
};
181185

182186
auction::Price(eth::Ether(price))
183187
}
184-
Ok(None) => {
185-
tracing::info!("Solving for sell=buy, price is 1");
186-
auction::Price(eth::Ether(U256::one()))
187-
}
188188
_ => {
189189
// This is to allow quotes to be generated for tokens for which the sell
190190
// token price is not available, so we default to fee=0
@@ -196,48 +196,40 @@ impl Inner {
196196

197197
let compute_solution = async |request: Request| -> Option<Solution> {
198198
let wrappers = request.wrappers.clone();
199-
let route = boundary_solver.route(request, self.max_hops).await.ok()?;
199+
let route = boundary_solver.route(request, self.max_hops).await?;
200200
let interactions;
201201
let gas;
202202
let (input, mut output);
203203

204-
match route {
205-
Some(route) => {
206-
interactions = route
207-
.segments
208-
.iter()
209-
.map(|segment| {
210-
solution::Interaction::Liquidity(Box::new(
211-
solution::LiquidityInteraction {
212-
liquidity: segment.liquidity.clone(),
213-
input: segment.input,
214-
output: segment.output,
215-
// TODO does the baseline solver know about this
216-
// optimization?
217-
internalize: false,
218-
},
219-
))
220-
})
221-
.collect();
222-
gas = route.gas() + self.solution_gas_offset;
223-
input = route.input();
224-
output = route.output();
225-
}
226-
None => {
227-
interactions = Vec::default();
228-
gas = eth::Gas(U256::zero()) + self.solution_gas_offset;
229-
230-
(input, output) = match order.side {
231-
order::Side::Sell => (order.buy, order.sell),
232-
order::Side::Buy => (order.sell, order.buy),
233-
};
234-
let sell = order.sell;
235-
let buy = order.buy;
236-
tracing::info!(
237-
"Computing solution for sell=buy. input: {input:?} output: \
238-
{output:?}, sell: {sell:?}, buy: {buy:?}"
239-
);
240-
}
204+
if !route.is_empty() {
205+
interactions = route
206+
.segments
207+
.iter()
208+
.map(|segment| {
209+
solution::Interaction::Liquidity(Box::new(
210+
solution::LiquidityInteraction {
211+
liquidity: segment.liquidity.clone(),
212+
input: segment.input,
213+
output: segment.output,
214+
// TODO does the baseline solver know about this
215+
// optimization?
216+
internalize: false,
217+
},
218+
))
219+
})
220+
.collect();
221+
gas = route.gas() + self.solution_gas_offset;
222+
input = route.input().expect("route is not empty");
223+
output = route.output().expect("route is not empty");
224+
} else {
225+
interactions = Vec::default();
226+
gas = eth::Gas(U256::zero()) + self.solution_gas_offset;
227+
228+
(input, output) = match order.side {
229+
order::Side::Sell => (order.sell, order.buy),
230+
order::Side::Buy => (order.buy, order.sell),
231+
};
232+
output.amount = input.amount;
241233
}
242234

243235
// The baseline solver generates a path with swapping
@@ -386,22 +378,20 @@ pub struct Segment<'a> {
386378
}
387379

388380
impl<'a> Route<'a> {
389-
pub fn new(segments: Vec<Segment<'a>>) -> Option<Self> {
390-
if segments.is_empty() {
391-
return None;
392-
}
393-
Some(Self { segments })
381+
pub fn new(segments: Vec<Segment<'a>>) -> Self {
382+
Self { segments }
383+
}
384+
385+
fn input(&self) -> Option<eth::Asset> {
386+
self.segments.first().map(|segment| segment.input)
394387
}
395388

396-
fn input(&self) -> eth::Asset {
397-
self.segments[0].input
389+
fn output(&self) -> Option<eth::Asset> {
390+
self.segments.last().map(|segment| segment.output)
398391
}
399392

400-
fn output(&self) -> eth::Asset {
401-
self.segments
402-
.last()
403-
.expect("route has at least one segment by construction")
404-
.output
393+
fn is_empty(&self) -> bool {
394+
self.segments.is_empty()
405395
}
406396

407397
fn gas(&self) -> eth::Gas {

0 commit comments

Comments
 (0)