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
4 changes: 2 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ test-e2e-local: (test-e2e "local_node")
test-e2e-forked: (test-e2e "forked_node")

# Run End-to-end tests with custom filters
test-e2e *filters:
cargo nextest run -p e2e {{filters}} --test-threads 1 --failure-output final --run-ignored ignored-only
test-e2e filters="" *extra="":
cargo nextest run -p e2e '{{filters}}' --test-threads 1 --failure-output final --run-ignored ignored-only {{extra}}

test-driver:
RUST_MIN_STACK=3145728 cargo nextest run -p driver --test-threads 1 --run-ignored ignored-only
Expand Down
27 changes: 10 additions & 17 deletions crates/driver/src/domain/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ impl Order {
liquidity: &infra::liquidity::Fetcher,
tokens: &infra::tokens::Fetcher,
) -> Result<Quote, Error> {
let liquidity = match solver.liquidity() {
solver::Liquidity::Fetch => {
let liquidity = match (solver.liquidity(), self.liquidity_pairs()) {
(solver::Liquidity::Fetch, Some(pairs)) => {
liquidity
.fetch(&self.liquidity_pairs(), infra::liquidity::AtBlock::Recent)
.fetch(&pairs, infra::liquidity::AtBlock::Recent)
.await
}
solver::Liquidity::Skip => Default::default(),
_ => Default::default(),
};

let auction = self
Expand Down Expand Up @@ -226,29 +226,22 @@ impl Order {
}

/// Returns the token pairs to fetch liquidity for.
fn liquidity_pairs(&self) -> HashSet<liquidity::TokenPair> {
let pair = liquidity::TokenPair::try_new(self.tokens.sell(), self.tokens.buy())
.expect("sell != buy by construction");
iter::once(pair).collect()
fn liquidity_pairs(&self) -> Option<HashSet<liquidity::TokenPair>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not related to your changes, but it's super confusing that the method is called liquidity_pairs (plural) when it only returns one pair. 😵‍💫

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will figure out a more fitting name

liquidity::TokenPair::try_new(self.tokens.sell(), self.tokens.buy())
.map(|pair| iter::once(pair).collect())
.ok()
}
}

/// The sell and buy tokens to quote for. This type maintains the invariant that
/// the sell and buy tokens are distinct.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Tokens {
sell: eth::TokenAddress,
buy: eth::TokenAddress,
}

impl Tokens {
/// Creates a new instance of [`Tokens`], verifying that the input buy and
/// sell tokens are distinct.
pub fn try_new(sell: eth::TokenAddress, buy: eth::TokenAddress) -> Result<Self, SameTokens> {
if sell == buy {
return Err(SameTokens);
}
Ok(Self { sell, buy })
pub fn new(sell: eth::TokenAddress, buy: eth::TokenAddress) -> Self {
Self { sell, buy }
}

pub fn sell(&self) -> eth::TokenAddress {
Expand Down
9 changes: 4 additions & 5 deletions crates/driver/src/infra/api/routes/quote/dto/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@ use {
};

impl Order {
pub fn into_domain(self) -> Result<quote::Order, Error> {
Ok(quote::Order {
tokens: quote::Tokens::try_new(self.sell_token.into(), self.buy_token.into())
.map_err(|quote::SameTokens| Error::SameTokens)?,
pub fn into_domain(self) -> quote::Order {
quote::Order {
tokens: quote::Tokens::new(self.sell_token.into(), self.buy_token.into()),
amount: self.amount.into(),
side: match self.kind {
Kind::Sell => competition::order::Side::Sell,
Kind::Buy => competition::order::Side::Buy,
},
deadline: self.deadline,
})
}
}
}

Expand Down
4 changes: 1 addition & 3 deletions crates/driver/src/infra/api/routes/quote/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ async fn route(
order: axum::extract::Query<dto::Order>,
) -> Result<axum::Json<dto::Quote>, (hyper::StatusCode, axum::Json<Error>)> {
let handle_request = async {
let order = order.0.into_domain().inspect_err(|err| {
observe::invalid_dto(err, "order");
})?;
let order = order.0.into_domain();
observe::quoting(&order);
let quote = order
.quote(
Expand Down
147 changes: 146 additions & 1 deletion crates/e2e/tests/e2e/place_order_with_quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,22 @@ use {

#[tokio::test]
#[ignore]
async fn local_node_test() {
async fn local_node_place_order_with_quote_basic() {
run_test(place_order_with_quote).await;
}

#[tokio::test]
#[ignore]
async fn local_node_place_order_with_quote_same_token_pair() {
run_test(place_order_with_quote_same_token_pair).await;
}

#[tokio::test]
#[ignore]
async fn local_node_place_order_with_quote_same_token_pair_error() {
run_test(place_order_with_quote_same_token_pair_error).await;
}

async fn place_order_with_quote(web3: Web3) {
let mut onchain = OnchainComponents::deploy(web3.clone()).await;

Expand Down Expand Up @@ -118,3 +130,136 @@ async fn place_order_with_quote(web3: Web3) {
assert_eq!(quote_response.verified, order_quote.verified);
assert_eq!(quote_metadata.unwrap().0, order_quote.metadata);
}

async fn place_order_with_quote_same_token_pair_error(web3: Web3) {
let mut onchain = OnchainComponents::deploy(web3.clone()).await;

let [solver] = onchain.make_solvers(to_wei(10)).await;
let [trader] = onchain.make_accounts(to_wei(10)).await;
let [token] = onchain
.deploy_tokens_with_weth_uni_v2_pools(to_wei(1_000), to_wei(1_000))
.await;

token.mint(trader.address(), to_wei(10)).await;

token
.approve(onchain.contracts().allowance.into_alloy(), eth(10))
.from(trader.address().into_alloy())
.send_and_watch()
.await
.unwrap();

tracing::info!("Starting services.");
let services = Services::new(&onchain).await;
services.start_protocol(solver.clone()).await;

// Disable auto-mine so we don't accidentally mine a settlement
web3.api::<TestNodeApi<_>>()
.set_automine_enabled(false)
.await
.expect("Must be able to disable automine");

tracing::info!("Quoting");
let quote_sell_amount = to_wei(1);
let quote_request = OrderQuoteRequest {
from: trader.address(),
sell_token: token.address().into_legacy(),
buy_token: token.address().into_legacy(),
side: OrderQuoteSide::Sell {
sell_amount: SellAmount::BeforeFee {
value: NonZeroU256::try_from(quote_sell_amount).unwrap(),
},
},
..Default::default()
};
assert!(services.submit_quote(&quote_request).await.is_err());
}

async fn place_order_with_quote_same_token_pair(web3: Web3) {
let mut onchain = OnchainComponents::deploy(web3.clone()).await;

let [solver] = onchain.make_solvers(to_wei(10)).await;
let [trader] = onchain.make_accounts(to_wei(10)).await;
let [token] = onchain
.deploy_tokens_with_weth_uni_v2_pools(to_wei(1_000), to_wei(1_000))
.await;

token.mint(trader.address(), to_wei(10)).await;

token
.approve(onchain.contracts().allowance.into_alloy(), eth(10))
.from(trader.address().into_alloy())
.send_and_watch()
.await
.unwrap();

tracing::info!("Starting services.");
let services = Services::new(&onchain).await;
services
.start_protocol_with_args(
ExtraServiceArgs {
api: vec!["--allow-same-sell-and-buy-token=true".to_string()],
..Default::default()
},
solver.clone(),
)
.await;

// Disable auto-mine so we don't accidentally mine a settlement
web3.api::<TestNodeApi<_>>()
.set_automine_enabled(false)
.await
.expect("Must be able to disable automine");

tracing::info!("Quoting");
let quote_sell_amount = to_wei(1);
let quote_request = OrderQuoteRequest {
from: trader.address(),
sell_token: token.address().into_legacy(),
buy_token: token.address().into_legacy(),
side: OrderQuoteSide::Sell {
sell_amount: SellAmount::BeforeFee {
value: NonZeroU256::try_from(quote_sell_amount).unwrap(),
},
},
..Default::default()
};
let quote_response = services.submit_quote(&quote_request).await.unwrap();
tracing::debug!(?quote_response);
assert!(quote_response.id.is_some());
assert!(quote_response.verified);

let quote_metadata =
crate::database::quote_metadata(services.db(), quote_response.id.unwrap()).await;
assert!(quote_metadata.is_some());
tracing::debug!(?quote_metadata);

tracing::info!("Placing order");
let order = OrderCreation {
quote_id: quote_response.id,
sell_token: token.address().into_legacy(),
sell_amount: quote_sell_amount,
buy_token: token.address().into_legacy(),
buy_amount: quote_response.quote.buy_amount,
valid_to: model::time::now_in_epoch_seconds() + 300,
kind: OrderKind::Sell,
..Default::default()
}
.sign(
EcdsaSigningScheme::Eip712,
&onchain.contracts().domain_separator,
SecretKeyRef::from(&SecretKey::from_slice(trader.private_key()).unwrap()),
);
let order_uid = services.create_order(&order).await.unwrap();

tracing::info!("Order quote verification");
let order_quote = database::orders::read_quote(
services.db().acquire().await.unwrap().deref_mut(),
&database::byte_array::ByteArray(order_uid.0),
)
.await
.unwrap()
.unwrap();
assert_eq!(quote_response.verified, order_quote.verified);
Copy link
Contributor

@fafk fafk Nov 13, 2025

Choose a reason for hiding this comment

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

What are these asserts for? 🤔 What does it signify that the order has a verified quote and has metadata?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These assertions are also present on the normal place_order_with_quote and it makes sure that the quote and order's quote are verified.

assert_eq!(quote_metadata.unwrap().0, order_quote.metadata);
}
9 changes: 9 additions & 0 deletions crates/orderbook/src/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ pub struct Arguments {
/// whether an order is actively being bid on.
#[clap(long, env, default_value = "5")]
pub active_order_competition_threshold: u32,

/// Allow same sell and buy token
#[clap(long, env, action = clap::ArgAction::Set, default_value = "false")]
pub allow_same_sell_and_buy_token: bool,
}

impl std::fmt::Display for Arguments {
Expand Down Expand Up @@ -172,6 +176,7 @@ impl std::fmt::Display for Arguments {
db_read_url,
max_gas_per_order,
active_order_competition_threshold,
allow_same_sell_and_buy_token,
} = self;

write!(f, "{shared}")?;
Expand Down Expand Up @@ -225,6 +230,10 @@ impl std::fmt::Display for Arguments {
f,
"active_order_competition_threshold: {active_order_competition_threshold}"
)?;
writeln!(
f,
"allow_same_sell_and_buy_token: {allow_same_sell_and_buy_token}"
)?;

Ok(())
}
Expand Down
44 changes: 24 additions & 20 deletions crates/orderbook/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,26 +409,30 @@ pub async fn run(args: Arguments) {
let chainalysis_oracle = ChainalysisOracle::Instance::deployed(&web3.alloy)
.await
.ok();
let order_validator = Arc::new(OrderValidator::new(
native_token.clone(),
Arc::new(order_validation::banned::Users::new(
chainalysis_oracle,
args.banned_users,
args.banned_users_max_cache_size.get().to_u64().unwrap(),
)),
validity_configuration,
args.eip1271_skip_creation_validation,
bad_token_detector.clone(),
hooks_contract,
optimal_quoter.clone(),
balance_fetcher,
signature_validator,
Arc::new(postgres_write.clone()),
args.max_limit_orders_per_user,
code_fetcher,
app_data_validator.clone(),
args.max_gas_per_order,
));
let order_validator = Arc::new(
OrderValidator::new(
Arc::new(order_validation::banned::Users::new(
chainalysis_oracle,
args.banned_users,
args.banned_users_max_cache_size.get().to_u64().unwrap(),
)),
validity_configuration,
args.eip1271_skip_creation_validation,
bad_token_detector.clone(),
hooks_contract,
optimal_quoter.clone(),
balance_fetcher,
signature_validator,
Arc::new(postgres_write.clone()),
args.max_limit_orders_per_user,
code_fetcher,
app_data_validator.clone(),
args.max_gas_per_order,
)
.with_sell_and_buy_validation(
(!args.allow_same_sell_and_buy_token).then(|| native_token.clone()),
),
);
let ipfs = args
.ipfs_gateway
.map(|url| {
Expand Down
Loading
Loading