Skip to content

Commit dc1d497

Browse files
feat: validate rfq quotes in request_binding_quote
1 parent 6239b46 commit dc1d497

File tree

4 files changed

+368
-20
lines changed

4 files changed

+368
-20
lines changed

src/rfq/protocols/bebop/client.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,8 @@ impl RFQClient for BebopClient {
376376

377377
match quote_response {
378378
BebopQuoteResponse::Success(quote) => {
379+
quote.validate(params)?;
380+
379381
let mut quote_attributes: HashMap<String, Bytes> = HashMap::new();
380382
quote_attributes.insert("calldata".into(), quote.tx.data);
381383
quote_attributes.insert(

src/rfq/protocols/bebop/models.rs

Lines changed: 231 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use alloy::primitives::Address;
22
use prost::Message;
33
use serde::{Deserialize, Serialize};
4-
use tycho_common::Bytes;
4+
use tycho_common::{models::protocol::GetAmountOutParams, Bytes};
5+
6+
use crate::rfq::errors::RFQError;
57

68
/// Protobuf message for Bebop pricing updates
79
#[derive(Clone, PartialEq, Message)]
@@ -212,6 +214,61 @@ pub struct BebopQuotePartial {
212214
pub partial_fill_offset: u64,
213215
}
214216

217+
impl BebopQuotePartial {
218+
pub fn validate(&self, params: &GetAmountOutParams) -> Result<(), RFQError> {
219+
match &self.to_sign {
220+
BebopOrderToSign::Single(single) => {
221+
if single.taker_token != params.token_in {
222+
return Err(RFQError::FatalError(format!(
223+
"Base token mismatch: expected {}, got {}",
224+
params.token_in, single.taker_token
225+
)));
226+
}
227+
if single.maker_token != params.token_out {
228+
return Err(RFQError::FatalError(format!(
229+
"Quote token mismatch: expected {}, got {}",
230+
params.token_out, single.maker_token
231+
)));
232+
}
233+
if single.taker_address != params.sender {
234+
return Err(RFQError::FatalError(format!(
235+
"Taker address mismatch: expected {}, got {}",
236+
params.sender, single.taker_address
237+
)));
238+
}
239+
if single.receiver != params.receiver {
240+
return Err(RFQError::FatalError(format!(
241+
"Receiver address mismatch: expected {}, got {}",
242+
params.receiver, single.receiver
243+
)));
244+
}
245+
let amount_in = params.amount_in.to_string();
246+
if single.taker_amount != amount_in {
247+
return Err(RFQError::FatalError(format!(
248+
"Base token amount mismatch: expected {}, got {}",
249+
amount_in, single.taker_amount
250+
)));
251+
}
252+
}
253+
BebopOrderToSign::Aggregate(aggregate) => {
254+
if aggregate.taker_address != params.sender {
255+
return Err(RFQError::FatalError(format!(
256+
"Taker address mismatch: expected {}, got {}",
257+
params.sender, aggregate.taker_address
258+
)));
259+
}
260+
if aggregate.receiver != params.receiver {
261+
return Err(RFQError::FatalError(format!(
262+
"Receiver address mismatch: expected {}, got {}",
263+
params.receiver, aggregate.receiver
264+
)));
265+
}
266+
}
267+
}
268+
Ok(())
269+
}
270+
}
271+
215272
#[derive(Debug, Clone, Serialize, Deserialize)]
216273
#[serde(untagged)]
217274
pub enum BebopOrderToSign {
@@ -351,4 +408,177 @@ mod tests {
351408
let insufficient_mid = price_data.get_mid_price(10.0);
352409
assert_eq!(insufficient_mid, Some(2000.325));
353410
}
411+
412+
#[cfg(test)]
413+
mod bebop_quote_partial_validate_tests {
414+
use std::str::FromStr;
415+
416+
use num_bigint::BigUint;
417+
use tycho_common::models::protocol::GetAmountOutParams;
418+
419+
use super::*;
420+
421+
fn hex_to_bytes(hex: &str) -> Bytes {
422+
Bytes::from_str(hex).unwrap()
423+
}
424+
425+
fn single_order() -> SingleOrderToSign {
426+
SingleOrderToSign {
427+
maker_address: hex_to_bytes("0x1111111111111111111111111111111111111111"),
428+
taker_address: hex_to_bytes("0x2222222222222222222222222222222222222222"),
429+
maker_token: hex_to_bytes("0x3333333333333333333333333333333333333333"),
430+
taker_token: hex_to_bytes("0x4444444444444444444444444444444444444444"),
431+
maker_amount: "2000".to_string(),
432+
taker_amount: "1000".to_string(),
433+
maker_nonce: "1".to_string(),
434+
expiry: 123456,
435+
receiver: hex_to_bytes("0x5555555555555555555555555555555555555555"),
436+
}
437+
}
438+
439+
fn aggregate_order() -> AggregateOrderToSign {
440+
AggregateOrderToSign {
441+
taker_address: hex_to_bytes("0x2222222222222222222222222222222222222222"),
442+
maker_amounts: vec![vec!["2000".to_string()]],
443+
taker_amounts: vec![vec!["1000".to_string()]],
444+
expiry: 123456,
445+
receiver: hex_to_bytes("0x5555555555555555555555555555555555555555"),
446+
}
447+
}
448+
449+
fn params() -> GetAmountOutParams {
450+
GetAmountOutParams {
451+
amount_in: BigUint::from(1000u32),
452+
token_in: hex_to_bytes("0x4444444444444444444444444444444444444444"),
453+
token_out: hex_to_bytes("0x3333333333333333333333333333333333333333"),
454+
sender: hex_to_bytes("0x2222222222222222222222222222222222222222"),
455+
receiver: hex_to_bytes("0x5555555555555555555555555555555555555555"),
456+
}
457+
}
458+
459+
fn quote_partial_single() -> BebopQuotePartial {
460+
BebopQuotePartial {
461+
status: "success".to_string(),
462+
settlement_address: hex_to_bytes("0x9999999999999999999999999999999999999999"),
463+
tx: TxData {
464+
to: hex_to_bytes("0x8888888888888888888888888888888888888888"),
465+
data: hex_to_bytes("0x1234"),
466+
value: "0".to_string(),
467+
from: hex_to_bytes("0x7777777777777777777777777777777777777777"),
468+
gas: 21000,
469+
gas_price: 100,
470+
},
471+
to_sign: BebopOrderToSign::Single(Box::new(single_order())),
472+
partial_fill_offset: 0,
473+
}
474+
}
475+
476+
fn quote_partial_aggregate() -> BebopQuotePartial {
477+
BebopQuotePartial {
478+
status: "success".to_string(),
479+
settlement_address: hex_to_bytes("0x9999999999999999999999999999999999999999"),
480+
tx: TxData {
481+
to: hex_to_bytes("0x8888888888888888888888888888888888888888"),
482+
data: hex_to_bytes("0x1234"),
483+
value: "0".to_string(),
484+
from: hex_to_bytes("0x7777777777777777777777777777777777777777"),
485+
gas: 21000,
486+
gas_price: 100,
487+
},
488+
to_sign: BebopOrderToSign::Aggregate(Box::new(aggregate_order())),
489+
partial_fill_offset: 0,
490+
}
491+
}
492+
493+
#[test]
494+
fn test_validate_single_success() {
495+
let quote = quote_partial_single();
496+
let params = params();
497+
assert!(quote.validate(&params).is_ok());
498+
}
499+
500+
#[test]
501+
fn test_validate_single_base_token_mismatch() {
502+
let mut quote = quote_partial_single();
503+
if let BebopOrderToSign::Single(ref mut single) = quote.to_sign {
504+
single.taker_token = hex_to_bytes("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
505+
}
506+
let params = params();
507+
let err = quote.validate(&params).unwrap_err();
508+
assert!(format!("{:?}", err).contains("Base token mismatch"));
509+
}
510+
511+
#[test]
512+
fn test_validate_single_quote_token_mismatch() {
513+
let mut quote = quote_partial_single();
514+
if let BebopOrderToSign::Single(ref mut single) = quote.to_sign {
515+
single.maker_token = hex_to_bytes("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
516+
}
517+
let params = params();
518+
let err = quote.validate(&params).unwrap_err();
519+
assert!(format!("{:?}", err).contains("Quote token mismatch"));
520+
}
521+
522+
#[test]
523+
fn test_validate_single_taker_address_mismatch() {
524+
let mut quote = quote_partial_single();
525+
if let BebopOrderToSign::Single(ref mut single) = quote.to_sign {
526+
single.taker_address = hex_to_bytes("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd");
527+
}
528+
let params = params();
529+
let err = quote.validate(&params).unwrap_err();
530+
assert!(format!("{:?}", err).contains("Taker address mismatch"));
531+
}
532+
533+
#[test]
534+
fn test_validate_single_receiver_mismatch() {
535+
let mut quote = quote_partial_single();
536+
if let BebopOrderToSign::Single(ref mut single) = quote.to_sign {
537+
single.receiver = hex_to_bytes("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd");
538+
}
539+
let params = params();
540+
let err = quote.validate(&params).unwrap_err();
541+
assert!(format!("{:?}", err).contains("Receiver address mismatch"));
542+
}
543+
544+
#[test]
545+
fn test_validate_single_base_token_amount_mismatch() {
546+
let mut quote = quote_partial_single();
547+
if let BebopOrderToSign::Single(ref mut single) = quote.to_sign {
548+
single.taker_amount = "9999".to_string();
549+
}
550+
let params = params();
551+
let err = quote.validate(&params).unwrap_err();
552+
assert!(format!("{:?}", err).contains("Base token amount mismatch"));
553+
}
554+
555+
#[test]
556+
fn test_validate_aggregate_success() {
557+
let quote = quote_partial_aggregate();
558+
let params = params();
559+
assert!(quote.validate(&params).is_ok());
560+
}
561+
562+
#[test]
563+
fn test_validate_aggregate_taker_address_mismatch() {
564+
let mut quote = quote_partial_aggregate();
565+
if let BebopOrderToSign::Aggregate(ref mut agg) = quote.to_sign {
566+
agg.taker_address = hex_to_bytes("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd");
567+
}
568+
let params = params();
569+
let err = quote.validate(&params).unwrap_err();
570+
assert!(format!("{:?}", err).contains("Taker address mismatch"));
571+
}
572+
573+
#[test]
574+
fn test_validate_aggregate_receiver_mismatch() {
575+
let mut quote = quote_partial_aggregate();
576+
if let BebopOrderToSign::Aggregate(ref mut agg) = quote.to_sign {
577+
agg.receiver = hex_to_bytes("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd");
578+
}
579+
let params = params();
580+
let err = quote.validate(&params).unwrap_err();
581+
assert!(format!("{:?}", err).contains("Receiver address mismatch"));
582+
}
583+
}
354584
}

src/rfq/protocols/hashflow/client.rs

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -429,14 +429,7 @@ impl RFQClient for HashflowClient {
429429
}
430430
// We assume there will be only one quote request at a time
431431
let quote = quotes[0].clone();
432-
433-
if (quote.quote_data.base_token != params.token_in) ||
434-
(quote.quote_data.quote_token != params.token_out)
435-
{
436-
return Err(RFQError::FatalError(
437-
"Quote tokens don't match request tokens".to_string(),
438-
))
439-
}
432+
quote.validate(params)?;
440433

441434
let mut quote_attributes: HashMap<String, Bytes> = HashMap::new();
442435
quote_attributes.insert("pool".to_string(), quote.quote_data.pool);
@@ -529,21 +522,16 @@ impl RFQClient for HashflowClient {
529522
};
530523
Ok(signed_quote)
531524
} else {
532-
return Err(RFQError::QuoteNotFound(format!(
525+
Err(RFQError::QuoteNotFound(format!(
533526
"Hashflow quote not found for {} {} ->{}",
534527
params.amount_in, params.token_in, params.token_out,
535528
)))
536529
}
537530
}
538531
"fail" => {
539-
return Err(RFQError::FatalError(format!(
540-
"Hashflow API error: {:?}",
541-
quote_response.error
542-
)));
543-
}
544-
_ => {
545-
return Err(RFQError::FatalError("Hashflow API error: Unknown status".to_string()));
532+
Err(RFQError::FatalError(format!("Hashflow API error: {:?}", quote_response.error)))
546533
}
534+
_ => Err(RFQError::FatalError("Hashflow API error: Unknown status".to_string())),
547535
}
548536
}
549537
}

0 commit comments

Comments
 (0)