diff --git a/crates/e2e/tests/e2e/place_order_with_quote.rs b/crates/e2e/tests/e2e/place_order_with_quote.rs index 63ecc2dfac..313fa7c4a5 100644 --- a/crates/e2e/tests/e2e/place_order_with_quote.rs +++ b/crates/e2e/tests/e2e/place_order_with_quote.rs @@ -4,13 +4,14 @@ use { configs::{ autopilot::Configuration, order_quoting::{ExternalSolver, OrderQuoting}, + orderbook::order_validation::{OrderValidationConfig, SameTokensPolicy}, shared::SharedConfig, test_util::TestDefault, }, e2e::setup::{colocation, wait_for_condition, *}, ethrpc::alloy::{CallBuilderExt, EvmProviderExt}, model::{ - order::{OrderCreation, OrderKind}, + order::{BUY_ETH_ADDRESS, OrderCreation, OrderKind}, quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount}, signature::EcdsaSigningScheme, }, @@ -37,6 +38,18 @@ async fn local_node_fallback_native_price_estimator() { run_test(fallback_native_price_estimator).await; } +#[tokio::test] +#[ignore] +async fn local_node_native_same_token_sell_with_eth_buy_marker() { + run_test(native_same_token_sell_with_eth_buy_marker).await; +} + +#[tokio::test] +#[ignore] +async fn local_node_native_same_token_buy_with_eth_buy_marker_rejected() { + run_test(native_same_token_buy_with_eth_buy_marker_rejected).await; +} + async fn place_order_with_quote(web3: Web3) { let mut onchain = OnchainComponents::deploy(web3.clone()).await; @@ -175,6 +188,151 @@ async fn disabled_same_sell_and_buy_token_order_feature(web3: Web3) { ); } +async fn native_same_token_sell_with_eth_buy_marker(web3: Web3) { + let mut onchain = OnchainComponents::deploy(web3.clone()).await; + + let [solver] = onchain.make_solvers(10u64.eth()).await; + let [trader] = onchain.make_accounts(10u64.eth()).await; + + onchain + .contracts() + .weth + .approve(onchain.contracts().allowance, 3u64.eth()) + .from(trader.address()) + .send_and_watch() + .await + .unwrap(); + onchain + .contracts() + .weth + .deposit() + .from(trader.address()) + .value(3u64.eth()) + .send_and_watch() + .await + .unwrap(); + + tracing::info!("Starting services."); + let services = Services::new(&onchain).await; + let orderbook_config = configs::orderbook::Configuration { + order_validation: OrderValidationConfig { + same_tokens_policy: SameTokensPolicy::AllowSell, + ..Default::default() + }, + ..configs::orderbook::Configuration::test_default() + }; + services + .start_protocol_with_args( + Default::default(), + Configuration::test("test_solver", solver.address()), + orderbook_config, + solver.clone(), + ) + .await; + + web3.provider + .evm_set_automine(false) + .await + .expect("Must be able to disable automine"); + + let quote_sell_amount = 1u64.eth(); + let quote_request = OrderQuoteRequest { + from: trader.address(), + sell_token: *onchain.contracts().weth.address(), + buy_token: BUY_ETH_ADDRESS, + side: OrderQuoteSide::Sell { + sell_amount: SellAmount::BeforeFee { + value: NonZeroU256::try_from(quote_sell_amount).unwrap(), + }, + }, + ..Default::default() + }; + + let quote_response = services.submit_quote("e_request).await.unwrap(); + assert!(quote_response.verified); + assert!(quote_response.id.is_some()); + + let order = OrderCreation { + quote_id: quote_response.id, + sell_token: *onchain.contracts().weth.address(), + sell_amount: quote_sell_amount, + buy_token: BUY_ETH_ADDRESS, + 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, + &trader.signer, + ); + + services.create_order(&order).await.unwrap(); +} + +async fn native_same_token_buy_with_eth_buy_marker_rejected(web3: Web3) { + let mut onchain = OnchainComponents::deploy(web3.clone()).await; + + let [solver] = onchain.make_solvers(10u64.eth()).await; + let [trader] = onchain.make_accounts(10u64.eth()).await; + + onchain + .contracts() + .weth + .approve(onchain.contracts().allowance, 3u64.eth()) + .from(trader.address()) + .send_and_watch() + .await + .unwrap(); + onchain + .contracts() + .weth + .deposit() + .from(trader.address()) + .value(3u64.eth()) + .send_and_watch() + .await + .unwrap(); + + tracing::info!("Starting services."); + let services = Services::new(&onchain).await; + let orderbook_config = configs::orderbook::Configuration { + order_validation: OrderValidationConfig { + same_tokens_policy: SameTokensPolicy::AllowSell, + ..Default::default() + }, + ..configs::orderbook::Configuration::test_default() + }; + services + .start_protocol_with_args( + Default::default(), + Configuration::test("test_solver", solver.address()), + orderbook_config, + solver.clone(), + ) + .await; + + web3.provider + .evm_set_automine(false) + .await + .expect("Must be able to disable automine"); + + let quote_request = OrderQuoteRequest { + from: trader.address(), + sell_token: *onchain.contracts().weth.address(), + buy_token: BUY_ETH_ADDRESS, + side: OrderQuoteSide::Buy { + buy_amount_after_fee: NonZeroU256::try_from(1u64.eth()).unwrap(), + }, + ..Default::default() + }; + + assert!( + matches!(services.submit_quote("e_request).await, Err((reqwest::StatusCode::BAD_REQUEST, response)) if response.contains("SameBuyAndSellToken")) + ); +} + async fn fallback_native_price_estimator(web3: Web3) { let mut onchain = OnchainComponents::deploy(web3.clone()).await; diff --git a/crates/shared/src/order_validation.rs b/crates/shared/src/order_validation.rs index 3846be7df8..12b9764409 100644 --- a/crates/shared/src/order_validation.rs +++ b/crates/shared/src/order_validation.rs @@ -217,12 +217,10 @@ fn validate_same_sell_and_buy_token( order: &PreOrderData, native_token: &Address, ) -> Result<(), PartialValidationError> { - // Check for orders selling wrapped native token for native token. - if &order.sell_token == native_token && order.buy_token == BUY_ETH_ADDRESS { - return Err(PartialValidationError::SameBuyAndSellToken); - } + let same_token = order.sell_token == order.buy_token + || (&order.sell_token == native_token && order.buy_token == BUY_ETH_ADDRESS); - if order.sell_token != order.buy_token { + if !same_token { return Ok(()); } @@ -1075,6 +1073,7 @@ mod tests { #[tokio::test] async fn pre_validate_err() { let native_token = WETH9::Instance::new([0xef; 20].into(), ethrpc::mock::web3().provider); + let native_token_address = *native_token.address(); let validity_configuration = OrderValidPeriodConfiguration { min: Duration::from_secs(1), max_market: Duration::from_secs(100), @@ -1206,6 +1205,17 @@ mod tests { .await, Err(PartialValidationError::SameBuyAndSellToken) )); + assert!(matches!( + validator + .partial_validate(PreOrderData { + valid_to: legit_valid_to, + buy_token: BUY_ETH_ADDRESS, + sell_token: native_token_address, + ..Default::default() + }) + .await, + Err(PartialValidationError::SameBuyAndSellToken) + )); assert!(matches!( validator .partial_validate(PreOrderData { @@ -1366,6 +1376,35 @@ mod tests { ) .is_ok() ); + + let native_order = || PreOrderData { + buy_token: BUY_ETH_ADDRESS, + sell_token: *native_token.address(), + valid_to: time::now_in_epoch_seconds() + + validity_configuration.min.as_secs() as u32 + + 2, + ..Default::default() + }; + + assert!(matches!( + validator + .partial_validate(PreOrderData { + kind: OrderKind::Buy, + ..native_order() + }) + .await, + Err(PartialValidationError::SameBuyAndSellToken) + )); + + assert!( + validator + .partial_validate(PreOrderData { + kind: OrderKind::Sell, + ..native_order() + }) + .await + .is_ok() + ); } #[tokio::test]