From df9ca2b195f18da80b654369f8d404dc5acb254c Mon Sep 17 00:00:00 2001 From: Martin Magnus Date: Thu, 12 Dec 2024 12:27:18 +0100 Subject: [PATCH] Remove naive solver (#3161) # Description The naive solver is extremely basic and never wins any auctions anymore. Also AFAICT it's no longer used in e2e tests either so by now it's just dead weight. Sparked by this [thread](https://cowservices.slack.com/archives/C036JAGRQ04/p1733911881085109) # Changes Removes: * original algorithm in legacy code * wrapper logic in `solvers` crate * config code * test cases * documentation Also in many cases we had an enum with 2 variants (`baseline` and `naive`). Since we now only have 1 variant left I collapsed all the logic and got rid of a few modules which are no longer necessary. --- README.md | 11 - crates/solver/src/settlement/mod.rs | 8 - .../{solver/baseline_solver.rs => solver.rs} | 20 + crates/solver/src/solver/mod.rs | 25 - crates/solver/src/solver/naive_solver.rs | 1086 ----------------- crates/solvers/src/boundary/baseline.rs | 14 +- crates/solvers/src/boundary/mod.rs | 1 - crates/solvers/src/boundary/naive.rs | 249 ---- .../domain/{solver/baseline.rs => solver.rs} | 12 +- crates/solvers/src/domain/solver/mod.rs | 30 - crates/solvers/src/domain/solver/naive.rs | 107 -- crates/solvers/src/infra/cli.rs | 2 - .../infra/{config/baseline.rs => config.rs} | 28 +- crates/solvers/src/infra/config/mod.rs | 21 - crates/solvers/src/run.rs | 7 +- .../{baseline => cases}/bal_liquidity.rs | 0 .../{baseline => cases}/buy_order_rounding.rs | 0 .../tests/{baseline => cases}/direct_swap.rs | 0 .../{baseline => cases}/internalization.rs | 0 .../limit_order_quoting.rs | 0 .../src/tests/{baseline => cases}/mod.rs | 0 .../tests/{baseline => cases}/partial_fill.rs | 0 crates/solvers/src/tests/mod.rs | 3 +- .../src/tests/naive/extract_deepest_pool.rs | 139 --- .../naive/filters_out_of_price_orders.rs | 172 --- .../src/tests/naive/limit_order_price.rs | 70 -- .../solvers/src/tests/naive/matches_orders.rs | 508 -------- crates/solvers/src/tests/naive/mod.rs | 8 - .../src/tests/naive/reserves_too_small.rs | 123 -- .../rounds_prices_in_favour_of_traders.rs | 137 --- .../tests/naive/swap_less_than_reserves.rs | 99 -- .../solvers/src/tests/naive/without_pool.rs | 120 -- 32 files changed, 60 insertions(+), 2940 deletions(-) rename crates/solver/src/{solver/baseline_solver.rs => solver.rs} (73%) delete mode 100644 crates/solver/src/solver/mod.rs delete mode 100644 crates/solver/src/solver/naive_solver.rs delete mode 100644 crates/solvers/src/boundary/naive.rs rename crates/solvers/src/domain/{solver/baseline.rs => solver.rs} (97%) delete mode 100644 crates/solvers/src/domain/solver/mod.rs delete mode 100644 crates/solvers/src/domain/solver/naive.rs rename crates/solvers/src/infra/{config/baseline.rs => config.rs} (81%) delete mode 100644 crates/solvers/src/infra/config/mod.rs rename crates/solvers/src/tests/{baseline => cases}/bal_liquidity.rs (100%) rename crates/solvers/src/tests/{baseline => cases}/buy_order_rounding.rs (100%) rename crates/solvers/src/tests/{baseline => cases}/direct_swap.rs (100%) rename crates/solvers/src/tests/{baseline => cases}/internalization.rs (100%) rename crates/solvers/src/tests/{baseline => cases}/limit_order_quoting.rs (100%) rename crates/solvers/src/tests/{baseline => cases}/mod.rs (100%) rename crates/solvers/src/tests/{baseline => cases}/partial_fill.rs (100%) delete mode 100644 crates/solvers/src/tests/naive/extract_deepest_pool.rs delete mode 100644 crates/solvers/src/tests/naive/filters_out_of_price_orders.rs delete mode 100644 crates/solvers/src/tests/naive/limit_order_price.rs delete mode 100644 crates/solvers/src/tests/naive/matches_orders.rs delete mode 100644 crates/solvers/src/tests/naive/mod.rs delete mode 100644 crates/solvers/src/tests/naive/reserves_too_small.rs delete mode 100644 crates/solvers/src/tests/naive/rounds_prices_in_favour_of_traders.rs delete mode 100644 crates/solvers/src/tests/naive/swap_less_than_reserves.rs delete mode 100644 crates/solvers/src/tests/naive/without_pool.rs diff --git a/README.md b/README.md index 844da11f15..016bcf40d6 100644 --- a/README.md +++ b/README.md @@ -27,17 +27,6 @@ Concretely, it is responsible for "cutting" new auctions (i.e. determining aucti The `autopilot` connects to the same PostgreSQL database as the `orderbook` and uses it to query orders as well as storing the most recent auction and settlement competition. -## Solver - -The `solver` crate is responsible for submitting on-chain settlements based on the orders it gets from the order book and other liquidity sources like Balancer or Uniswap pools. - -It implements a few settlement strategies directly in Rust: - -- Naive Solver: Can match to overlapping opposing orders (e.g. DAI for WETH & WETH for DAI) with one another settling the excess with Uniswap -- Uniswap Baseline: Same path finding as used by the Uniswap frontend (settling orders individually instead of batching them together) - -It can also interact with a more advanced, Gnosis internal, closed source solver which tries to settle all orders using the combinatorial optimization formulations described in [Multi-Token Batch Auctions with Uniform Clearing Price](https://github.com/gnosis/dex-research/blob/master/BatchAuctionOptimization/batchauctions.pdf) - ## Other Crates There are additional crates that live in the cargo workspace. diff --git a/crates/solver/src/settlement/mod.rs b/crates/solver/src/settlement/mod.rs index 01dda9c696..a63db21a85 100644 --- a/crates/solver/src/settlement/mod.rs +++ b/crates/solver/src/settlement/mod.rs @@ -131,14 +131,6 @@ impl Settlement { self.encoder.clearing_prices() } - /// Returns the clearing price for the specified token. - /// - /// Returns `None` if the token is not part of the settlement. - #[cfg(test)] - pub(crate) fn clearing_price(&self, token: H160) -> Option { - self.clearing_prices().get(&token).copied() - } - /// Returns all orders included in the settlement. pub fn traded_orders(&self) -> impl Iterator + '_ { self.encoder.all_trades().map(|trade| &trade.data.order) diff --git a/crates/solver/src/solver/baseline_solver.rs b/crates/solver/src/solver.rs similarity index 73% rename from crates/solver/src/solver/baseline_solver.rs rename to crates/solver/src/solver.rs index be79d85a0e..3c694e7048 100644 --- a/crates/solver/src/solver/baseline_solver.rs +++ b/crates/solver/src/solver.rs @@ -1,12 +1,32 @@ use { crate::liquidity::{ConstantProductOrder, WeightedProductOrder}, + anyhow::anyhow, ethcontract::{H160, U256}, shared::{ baseline_solver::BaselineSolvable, sources::{balancer_v2::swap::WeightedPoolRef, uniswap_v2::pool_fetching::Pool}, }, + std::{fmt::Debug, str::FromStr}, }; +// Wrapper type for AWS ARN identifiers +#[derive(Debug, Clone)] +pub struct Arn(pub String); + +impl FromStr for Arn { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + // Could be more strict here, but this should suffice to catch unintended + // configuration mistakes + if s.starts_with("arn:aws:kms:") { + Ok(Self(s.to_string())) + } else { + Err(anyhow!("Invalid ARN identifier: {}", s)) + } + } +} + impl BaselineSolvable for ConstantProductOrder { fn get_amount_out(&self, out_token: H160, input: (U256, H160)) -> Option { amm_to_pool(self).get_amount_out(out_token, input) diff --git a/crates/solver/src/solver/mod.rs b/crates/solver/src/solver/mod.rs deleted file mode 100644 index e285094723..0000000000 --- a/crates/solver/src/solver/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -use { - anyhow::anyhow, - std::{fmt::Debug, str::FromStr}, -}; - -mod baseline_solver; -pub mod naive_solver; - -// Wrapper type for AWS ARN identifiers -#[derive(Debug, Clone)] -pub struct Arn(pub String); - -impl FromStr for Arn { - type Err = anyhow::Error; - - fn from_str(s: &str) -> std::result::Result { - // Could be more strict here, but this should suffice to catch unintended - // configuration mistakes - if s.starts_with("arn:aws:kms:") { - Ok(Self(s.to_string())) - } else { - Err(anyhow!("Invalid ARN identifier: {}", s)) - } - } -} diff --git a/crates/solver/src/solver/naive_solver.rs b/crates/solver/src/solver/naive_solver.rs deleted file mode 100644 index dc0fba6243..0000000000 --- a/crates/solver/src/solver/naive_solver.rs +++ /dev/null @@ -1,1086 +0,0 @@ -use { - crate::{ - liquidity::{self, slippage::SlippageContext, LimitOrderExecution}, - settlement::{PricedTrade, Settlement}, - }, - anyhow::Result, - liquidity::{AmmOrderExecution, ConstantProductOrder, LimitOrder}, - model::order::OrderKind, - num::{rational::Ratio, BigInt, BigRational, CheckedDiv}, - number::conversions::{big_int_to_u256, big_rational_to_u256, u256_to_big_int}, - primitive_types::U256, - shared::{ - conversions::{RatioExt, U256Ext}, - http_solver::model::TokenAmount, - }, - std::collections::HashMap, - web3::types::Address, -}; - -#[derive(Debug, Clone)] -struct TokenContext { - address: Address, - reserve: U256, - buy_volume: U256, - sell_volume: U256, -} - -impl TokenContext { - pub fn is_excess_after_fees(&self, deficit: &TokenContext, fee: Ratio) -> bool { - fee.denom() - * u256_to_big_int(&self.reserve) - * (u256_to_big_int(&deficit.sell_volume) - u256_to_big_int(&deficit.buy_volume)) - < (fee.denom() - fee.numer()) - * u256_to_big_int(&deficit.reserve) - * (u256_to_big_int(&self.sell_volume) - u256_to_big_int(&self.buy_volume)) - } - - pub fn is_excess_before_fees(&self, deficit: &TokenContext) -> bool { - u256_to_big_int(&self.reserve) - * (u256_to_big_int(&deficit.sell_volume) - u256_to_big_int(&deficit.buy_volume)) - < u256_to_big_int(&deficit.reserve) - * (u256_to_big_int(&self.sell_volume) - u256_to_big_int(&self.buy_volume)) - } -} - -pub fn solve( - slippage: &SlippageContext, - orders: impl IntoIterator, - pool: &ConstantProductOrder, -) -> Option { - let mut orders: Vec = orders.into_iter().collect(); - while !orders.is_empty() { - let (context_a, context_b) = split_into_contexts(&orders, pool); - if let Some(valid_solution) = - solve_orders(slippage, &orders, pool, &context_a, &context_b).filter(is_valid_solution) - { - return Some(valid_solution); - } else { - // remove order with worst limit price that is selling excess token (to make it - // less excessive) and try again - let excess_token = if context_a.is_excess_before_fees(&context_b) { - context_a.address - } else { - context_b.address - }; - let order_to_remove = orders - .iter() - .enumerate() - .filter(|o| o.1.sell_token == excess_token) - .max_by(|lhs, rhs| { - (lhs.1.buy_amount * rhs.1.sell_amount) - .cmp(&(lhs.1.sell_amount * rhs.1.buy_amount)) - }); - match order_to_remove { - Some((index, _)) => orders.swap_remove(index), - None => break, - }; - } - } - - None -} - -/// -/// Computes a settlement using orders of a single pair and the direct AMM -/// between those tokens.get(. Panics if orders are not already filtered for a -/// specific token pair, or the reserve information for that pair is not -/// available. -fn solve_orders( - slippage: &SlippageContext, - orders: &[LimitOrder], - pool: &ConstantProductOrder, - context_a: &TokenContext, - context_b: &TokenContext, -) -> Option { - if context_a.is_excess_after_fees(context_b, pool.fee) { - solve_with_uniswap(slippage, orders, pool, context_b, context_a) - } else if context_b.is_excess_after_fees(context_a, pool.fee) { - solve_with_uniswap(slippage, orders, pool, context_a, context_b) - } else { - solve_without_uniswap(orders, context_a, context_b).ok() - } -} - -/// -/// Creates a solution using the current AMM spot price, without using any of -/// its liquidity -fn solve_without_uniswap( - orders: &[LimitOrder], - context_a: &TokenContext, - context_b: &TokenContext, -) -> Result { - let mut settlement = Settlement::new(maplit::hashmap! { - context_a.address => context_b.reserve, - context_b.address => context_a.reserve, - }); - for order in orders { - let execution = LimitOrderExecution { - filled: order.full_execution_amount(), - fee: order.user_fee, - }; - settlement.with_liquidity(order, execution)?; - } - - Ok(settlement) -} - -/// -/// Creates a solution using the current AMM's liquidity to balance excess and -/// shortage. The clearing price is the effective exchange rate used by the AMM -/// interaction. -fn solve_with_uniswap( - slippage: &SlippageContext, - orders: &[LimitOrder], - pool: &ConstantProductOrder, - shortage: &TokenContext, - excess: &TokenContext, -) -> Option { - let uniswap_in_token = excess.address; - let uniswap_out_token = shortage.address; - - let uniswap_out = compute_uniswap_out(shortage, excess, pool.fee)?; - let uniswap_in = compute_uniswap_in(uniswap_out.clone(), shortage, excess, pool.fee)?; - - let uniswap_out = big_rational_to_u256(&uniswap_out).ok()?; - let uniswap_in = big_rational_to_u256(&uniswap_in).ok()?; - - let mut settlement = Settlement::new(maplit::hashmap! { - uniswap_in_token => uniswap_out, - uniswap_out_token => uniswap_in, - }); - for order in orders { - let execution = LimitOrderExecution { - filled: order.full_execution_amount(), - // TODO: We still need to compute a fee for partially fillable limit orders. - fee: order.user_fee, - }; - settlement.with_liquidity(order, execution).ok()?; - } - - // Because the smart contracts round in the favour of the traders, it could - // be that we actually require a bit more from the Uniswap pool in order to - // pay out all proceeds. We move the rounding error to the sell token so that - // it either comes out of the fees or existing buffers. It is also important - // to use the **synthetic** order amounts for this, as this output amount - // needs to be computed for what is actually intented to be traded against - // an AMM and not how the order will be executed (including things like - // surplus fees). - let uniswap_out_required_by_orders = big_int_to_u256(&orders.iter().try_fold( - BigInt::default(), - |mut total, order| { - if order.sell_token == uniswap_out_token { - total -= match order.kind { - OrderKind::Sell => order.sell_amount, - OrderKind::Buy => order - .buy_amount - .checked_mul(uniswap_out)? - .checked_div(uniswap_in)?, - } - .to_big_int(); - } else { - total += match order.kind { - OrderKind::Sell => order - .sell_amount - .checked_mul(uniswap_out)? - .checked_ceil_div(&uniswap_in)?, - OrderKind::Buy => order.buy_amount, - } - .to_big_int(); - }; - Some(total) - }, - )?) - .ok()?; - - // In theory, not all limit orders are GPv2 trades (although in practice - // they are). Since those orders don't generate trades, they aren't - // considered in the `uniswap_out_with_rounding` computation. Luckily, they - // don't get any surplus anyway, so we can just use the original output - // amount as the rounding error will come from the surplus that those orders - // would have otherwise received. - let uniswap_out_with_rounding = uniswap_out_required_by_orders.max(uniswap_out); - - settlement - .with_liquidity( - pool, - slippage - .apply_to_amm_execution(AmmOrderExecution { - input_max: TokenAmount::new(uniswap_in_token, uniswap_in), - output: TokenAmount::new(uniswap_out_token, uniswap_out_with_rounding), - internalizable: false, - }) - .ok()?, - ) - .ok()?; - - Some(settlement) -} - -impl ConstantProductOrder { - fn get_reserve(&self, token: &Address) -> Option { - if &self.tokens.get().0 == token { - Some(self.reserves.0.into()) - } else if &self.tokens.get().1 == token { - Some(self.reserves.1.into()) - } else { - None - } - } -} - -fn split_into_contexts( - orders: &[LimitOrder], - pool: &ConstantProductOrder, -) -> (TokenContext, TokenContext) { - let mut contexts = HashMap::new(); - for order in orders { - let buy_context = contexts - .entry(order.buy_token) - .or_insert_with(|| TokenContext { - address: order.buy_token, - reserve: pool - .get_reserve(&order.buy_token) - .unwrap_or_else(|| panic!("No reserve for token {}", &order.buy_token)), - buy_volume: U256::zero(), - sell_volume: U256::zero(), - }); - if matches!(order.kind, OrderKind::Buy) { - buy_context.buy_volume += order.buy_amount - } - - let sell_context = contexts - .entry(order.sell_token) - .or_insert_with(|| TokenContext { - address: order.sell_token, - reserve: pool - .get_reserve(&order.sell_token) - .unwrap_or_else(|| panic!("No reserve for token {}", &order.sell_token)), - buy_volume: U256::zero(), - sell_volume: U256::zero(), - }); - if matches!(order.kind, OrderKind::Sell) { - sell_context.sell_volume += order.sell_amount - } - } - assert_eq!(contexts.len(), 2, "Orders contain more than two tokens"); - let mut contexts = contexts.drain().map(|(_, v)| v); - (contexts.next().unwrap(), contexts.next().unwrap()) -} - -/// -/// Given information about the shortage token (the one we need to take from -/// Uniswap) and the excess token (the one we give to Uniswap), this function -/// computes the exact out_amount required from Uniswap to perfectly match -/// demand and supply at the effective Uniswap price (the one used for that -/// in/out swap). -/// -/// The derivation of this formula is described in https://docs.google.com/document/d/1jS22wxbCqo88fGsqEMZgRQgiAcHlPqxoMw3CJTHst6c/edit -/// It assumes GP fee (φ) to be 1 -fn compute_uniswap_out( - shortage: &TokenContext, - excess: &TokenContext, - amm_fee: Ratio, -) -> Option { - let numerator_minuend = (amm_fee.denom() - amm_fee.numer()) - * (u256_to_big_int(&excess.sell_volume) - u256_to_big_int(&excess.buy_volume)) - * u256_to_big_int(&shortage.reserve); - let numerator_subtrahend = amm_fee.denom() - * (u256_to_big_int(&shortage.sell_volume) - u256_to_big_int(&shortage.buy_volume)) - * u256_to_big_int(&excess.reserve); - let denominator: BigInt = amm_fee.denom() * u256_to_big_int(&excess.reserve) - + (amm_fee.denom() - amm_fee.numer()) - * (u256_to_big_int(&excess.sell_volume) - u256_to_big_int(&excess.buy_volume)); - BigRational::new_checked(numerator_minuend - numerator_subtrahend, denominator).ok() -} - -/// -/// Given the desired amount to receive and the state of the pool, this computes -/// the required amount of tokens to be sent to the pool. -/// Taken from: https://github.com/Uniswap/uniswap-v2-periphery/blob/4123f93278b60bcf617130629c69d4016f9e7584/contracts/libraries/UniswapV2Library.sol#L53 -/// Not adding + 1 in the end, because we are working with rationals and thus -/// don't round up. -fn compute_uniswap_in( - out: BigRational, - shortage: &TokenContext, - excess: &TokenContext, - amm_fee: Ratio, -) -> Option { - let numerator = U256::from(*amm_fee.denom()).to_big_rational() - * out.clone() - * u256_to_big_int(&excess.reserve); - let denominator = U256::from(amm_fee.denom() - amm_fee.numer()).to_big_rational() - * (shortage.reserve.to_big_rational() - out); - numerator.checked_div(&denominator) -} - -/// -/// Returns true if for each trade the executed price is not smaller than the -/// limit price Thus we ensure that `buy_token_price / sell_token_price >= -/// limit_buy_amount / limit_sell_amount` -fn is_valid_solution(solution: &Settlement) -> bool { - for PricedTrade { - data, - sell_token_price, - buy_token_price, - } in solution.encoder.all_trades() - { - let order = &data.order.data; - - // Check execution respects individual order's limit price - match ( - order.sell_amount.checked_mul(sell_token_price), - order.buy_amount.checked_mul(buy_token_price), - ) { - (Some(sell_volume), Some(buy_volume)) if sell_volume >= buy_volume => (), - _ => return false, - } - - // Check individual order's execution price satisfies uniform clearing price - // E.g. liquidity orders may have a different executed price. - let clearing_prices = solution.encoder.clearing_prices(); - match ( - clearing_prices - .get(&order.buy_token) - .map(|clearing_sell_price| clearing_sell_price.checked_mul(sell_token_price)), - clearing_prices - .get(&order.sell_token) - .map(|clearing_buy_price| clearing_buy_price.checked_mul(buy_token_price)), - ) { - (Some(execution_sell_value), Some(clearing_buy_value)) - if execution_sell_value <= clearing_buy_value => {} - _ => return false, - } - } - - true -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::liquidity::slippage::SlippageCalculator, - ethcontract::H160, - liquidity::tests::CapturingSettlementHandler, - maplit::hashmap, - model::{ - order::{Order, OrderData}, - TokenPair, - }, - num::rational::Ratio, - once_cell::sync::OnceCell, - shared::{ - baseline_solver::BaselineSolvable, - external_prices::ExternalPrices, - sources::uniswap_v2::pool_fetching::Pool, - }, - }; - - fn to_wei(base: u128) -> U256 { - U256::from(base) * U256::from(10).pow(18.into()) - } - - fn without_slippage() -> SlippageContext<'static> { - static CONTEXT: OnceCell<(ExternalPrices, SlippageCalculator)> = OnceCell::new(); - let (prices, calculator) = - CONTEXT.get_or_init(|| (Default::default(), SlippageCalculator::from_bps(0, None))); - calculator.context(prices) - } - - #[test] - fn finds_clearing_price_with_sell_orders_on_both_sides() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - LimitOrder { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(40), - buy_amount: to_wei(30), - kind: OrderKind::Sell, - id: 0.into(), - ..Default::default() - }, - LimitOrder { - sell_token: token_b, - buy_token: token_a, - sell_amount: to_wei(100), - buy_amount: to_wei(90), - kind: OrderKind::Sell, - id: 1.into(), - ..Default::default() - }, - ]; - - let amm_handler = CapturingSettlementHandler::arc(); - let pool = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (to_wei(1000).as_u128(), to_wei(1000).as_u128()), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler.clone(), - }; - let result = solve(&without_slippage(), orders.clone(), &pool).unwrap(); - - // Make sure the uniswap interaction is using the correct direction - let interaction = amm_handler.calls()[0].clone(); - assert_eq!(interaction.input_max.token, token_b); - assert_eq!(interaction.output.token, token_a); - - // Make sure the sell amounts +/- uniswap interaction satisfy min_buy amounts - assert!(orders[0].sell_amount + interaction.output.amount >= orders[1].buy_amount); - assert!(orders[1].sell_amount - interaction.input_max.amount > orders[0].buy_amount); - - // Make sure the sell amounts +/- uniswap interaction satisfy expected buy - // amounts given clearing price - let price_a = result.clearing_price(token_a).unwrap(); - let price_b = result.clearing_price(token_b).unwrap(); - - // Multiplying sellAmount with priceA, gives us sell value in "$", divided by - // priceB gives us value in buy token We should have at least as much to - // give (sell amount +/- uniswap) as is expected by the buyer - let expected_buy = (orders[0].sell_amount * price_a).ceil_div(&price_b); - assert!(orders[1].sell_amount - interaction.input_max.amount >= expected_buy); - - let expected_buy = (orders[1].sell_amount * price_b).ceil_div(&price_a); - assert!(orders[0].sell_amount + interaction.input_max.amount >= expected_buy); - } - - #[test] - fn finds_clearing_price_with_sell_orders_on_one_side() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - LimitOrder { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(40), - buy_amount: to_wei(30), - kind: OrderKind::Sell, - id: 0.into(), - ..Default::default() - }, - LimitOrder { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(100), - buy_amount: to_wei(90), - kind: OrderKind::Sell, - id: 1.into(), - ..Default::default() - }, - ]; - - let amm_handler = CapturingSettlementHandler::arc(); - let pool = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (to_wei(1_000_000).as_u128(), to_wei(1_000_000).as_u128()), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler.clone(), - }; - let result = solve(&without_slippage(), orders.clone(), &pool).unwrap(); - - // Make sure the uniswap interaction is using the correct direction - let interaction = amm_handler.calls()[0].clone(); - assert_eq!(interaction.input_max.token, token_a); - assert_eq!(interaction.output.token, token_b); - - // Make sure the sell amounts cover the uniswap in, and min buy amounts are - // covered by uniswap out - assert!(orders[0].sell_amount + orders[1].sell_amount >= interaction.input_max.amount); - assert!(interaction.output.amount >= orders[0].buy_amount + orders[1].buy_amount); - - // Make sure expected buy amounts (given prices) are also covered by uniswap out - // amounts - let price_a = result.clearing_price(token_a).unwrap(); - let price_b = result.clearing_price(token_b).unwrap(); - - let first_expected_buy = orders[0].sell_amount * price_a / price_b; - let second_expected_buy = orders[1].sell_amount * price_a / price_b; - assert!(interaction.output.amount >= first_expected_buy + second_expected_buy); - } - - #[test] - fn finds_clearing_price_with_buy_orders_on_both_sides() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - LimitOrder { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(40), - buy_amount: to_wei(30), - kind: OrderKind::Buy, - id: 0.into(), - ..Default::default() - }, - LimitOrder { - sell_token: token_b, - buy_token: token_a, - sell_amount: to_wei(100), - buy_amount: to_wei(90), - kind: OrderKind::Buy, - id: 1.into(), - ..Default::default() - }, - ]; - - let amm_handler = CapturingSettlementHandler::arc(); - let pool = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (to_wei(1000).as_u128(), to_wei(1000).as_u128()), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler.clone(), - }; - let result = solve(&SlippageContext::default(), orders.clone(), &pool).unwrap(); - - // Make sure the uniswap interaction is using the correct direction - let interaction = amm_handler.calls()[0].clone(); - assert_eq!(interaction.input_max.token, token_b); - assert_eq!(interaction.output.token, token_a); - - // Make sure the buy amounts +/- uniswap interaction satisfy max_sell amounts - assert!(orders[0].sell_amount >= orders[1].buy_amount - interaction.output.amount); - assert!(orders[1].sell_amount >= orders[0].buy_amount + interaction.input_max.amount); - - // Make sure buy sell amounts +/- uniswap interaction satisfy expected sell - // amounts given clearing price - let price_a = result.clearing_price(token_a).unwrap(); - let price_b = result.clearing_price(token_b).unwrap(); - - // Multiplying buyAmount with priceB, gives us sell value in "$", divided by - // priceA gives us value in sell token The seller should expect to sell - // at least as much as we require for the buyer + uniswap. - let expected_sell = orders[0].buy_amount * price_b / price_a; - assert!(orders[1].buy_amount - interaction.input_max.amount <= expected_sell); - - let expected_sell = orders[1].buy_amount * price_a / price_b; - assert!(orders[0].buy_amount + interaction.output.amount <= expected_sell); - } - - #[test] - fn finds_clearing_price_with_buy_orders_and_sell_orders() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - LimitOrder { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(40), - buy_amount: to_wei(30), - kind: OrderKind::Buy, - id: 0.into(), - ..Default::default() - }, - LimitOrder { - sell_token: token_b, - buy_token: token_a, - sell_amount: to_wei(100), - buy_amount: to_wei(90), - kind: OrderKind::Sell, - id: 1.into(), - ..Default::default() - }, - ]; - - let amm_handler = CapturingSettlementHandler::arc(); - let pool = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (to_wei(1000).as_u128(), to_wei(1000).as_u128()), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler.clone(), - }; - let result = solve(&SlippageContext::default(), orders.clone(), &pool).unwrap(); - - // Make sure the uniswap interaction is using the correct direction - let interaction = amm_handler.calls()[0].clone(); - assert_eq!(interaction.input_max.token, token_b); - assert_eq!(interaction.output.token, token_a); - - // Make sure the buy order's sell amount - uniswap interaction satisfies sell - // order's limit - assert!(orders[0].sell_amount >= orders[1].buy_amount - interaction.output.amount); - - // Make sure the sell order's buy amount + uniswap interaction satisfies buy - // order's limit - assert!(orders[1].buy_amount + interaction.input_max.amount >= orders[0].sell_amount); - - // Make sure buy sell amounts +/- uniswap interaction satisfy expected sell - // amounts given clearing price - let price_a = result.clearing_price(token_a).unwrap(); - let price_b = result.clearing_price(token_b).unwrap(); - - // Multiplying buy_amount with priceB, gives us sell value in "$", divided by - // priceA gives us value in sell token The seller should expect to sell - // at least as much as we require for the buyer + uniswap. - let expected_sell = orders[0].buy_amount * price_b / price_a; - assert!(orders[1].buy_amount - interaction.input_max.amount <= expected_sell); - - // Multiplying sell_amount with priceA, gives us sell value in "$", divided by - // priceB gives us value in buy token We should have at least as much to - // give (sell amount + uniswap out) as is expected by the buyer - let expected_buy = orders[1].sell_amount * price_b / price_a; - assert!(orders[0].sell_amount + interaction.output.amount >= expected_buy); - } - - #[test] - fn finds_clearing_without_using_uniswap() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - LimitOrder { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(1001), - buy_amount: to_wei(1000), - kind: OrderKind::Sell, - id: 0.into(), - ..Default::default() - }, - LimitOrder { - sell_token: token_b, - buy_token: token_a, - sell_amount: to_wei(1001), - buy_amount: to_wei(1000), - kind: OrderKind::Sell, - id: 1.into(), - ..Default::default() - }, - ]; - - let amm_handler = CapturingSettlementHandler::arc(); - let pool = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (to_wei(1_000_001).as_u128(), to_wei(1_000_000).as_u128()), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler.clone(), - }; - let result = solve(&SlippageContext::default(), orders, &pool).unwrap(); - assert!(amm_handler.calls().is_empty()); - assert_eq!( - result.clearing_prices(), - &maplit::hashmap! { - token_a => to_wei(1_000_000), - token_b => to_wei(1_000_001) - } - ); - } - - #[test] - fn finds_solution_excluding_orders_whose_limit_price_is_not_satisfiable() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - // Unreasonable order a -> b - Order { - data: OrderData { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(1), - buy_amount: to_wei(1000), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - // Reasonable order a -> b - Order { - data: OrderData { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(1000), - buy_amount: to_wei(1000), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - // Reasonable order b -> a - Order { - data: OrderData { - sell_token: token_b, - buy_token: token_a, - sell_amount: to_wei(1000), - buy_amount: to_wei(1000), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - // Unreasonable order b -> a - Order { - data: OrderData { - sell_token: token_b, - buy_token: token_a, - sell_amount: to_wei(2), - buy_amount: to_wei(1000), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - ]; - - let amm_handler = CapturingSettlementHandler::arc(); - let pool = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (to_wei(1_000_000).as_u128(), to_wei(1_000_000).as_u128()), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler, - }; - let result = solve(&SlippageContext::default(), orders, &pool).unwrap(); - - assert_eq!(result.traded_orders().count(), 2); - assert!(is_valid_solution(&result)); - } - - #[test] - fn returns_empty_solution_if_orders_have_no_overlap() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - Order { - data: OrderData { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(900), - buy_amount: to_wei(1000), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - Order { - data: OrderData { - sell_token: token_b, - buy_token: token_a, - sell_amount: to_wei(900), - buy_amount: to_wei(1000), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - ]; - - let amm_handler = CapturingSettlementHandler::arc(); - let pool = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (to_wei(1_000_001).as_u128(), to_wei(1_000_000).as_u128()), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler, - }; - assert!(solve(&SlippageContext::default(), orders, &pool).is_none()); - } - - #[test] - fn test_is_valid_solution() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - Order { - data: OrderData { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(10), - buy_amount: to_wei(8), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - }, - Order { - data: OrderData { - sell_token: token_b, - buy_token: token_a, - sell_amount: to_wei(10), - buy_amount: to_wei(9), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - }, - ]; - - let settlement_with_prices = |prices: HashMap| { - let mut settlement = Settlement::new(prices); - for order in orders.iter().cloned() { - let limit_order = LimitOrder::from(order); - let execution = LimitOrderExecution::new( - limit_order.full_execution_amount(), - limit_order.user_fee, - ); - settlement.with_liquidity(&limit_order, execution).unwrap(); - } - settlement - }; - - // Price in the middle is ok - assert!(is_valid_solution(&settlement_with_prices( - maplit::hashmap! { - token_a => to_wei(1), - token_b => to_wei(1) - } - ),)); - - // Price at the limit of first order is ok - assert!(is_valid_solution(&settlement_with_prices( - maplit::hashmap! { - token_a => to_wei(8), - token_b => to_wei(10) - } - ),)); - - // Price at the limit of second order is ok - assert!(is_valid_solution(&settlement_with_prices( - maplit::hashmap! { - token_a => to_wei(10), - token_b => to_wei(9) - } - ),)); - - // Price violating first order is not ok - assert!(!is_valid_solution(&settlement_with_prices( - maplit::hashmap! { - token_a => to_wei(7), - token_b => to_wei(10) - } - ),)); - - // Price violating second order is not ok - assert!(!is_valid_solution(&settlement_with_prices( - maplit::hashmap! { - token_a => to_wei(10), - token_b => to_wei(8) - } - ),)); - } - - #[test] - fn does_not_panic() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - Order { - data: OrderData { - sell_token: token_a, - buy_token: token_b, - sell_amount: U256::MAX, - buy_amount: 1.into(), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - Order { - data: OrderData { - sell_token: token_b, - buy_token: token_a, - sell_amount: 1.into(), - buy_amount: 1.into(), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - ]; - - let amm_handler = CapturingSettlementHandler::arc(); - let pool = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (u128::MAX, u128::MAX), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler, - }; - // This line should not panic. - solve(&SlippageContext::default(), orders, &pool); - } - - #[test] - fn reserves_are_too_small() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - Order { - data: OrderData { - sell_token: token_a, - buy_token: token_b, - sell_amount: 70145218378783248142575u128.into(), - buy_amount: 70123226323u128.into(), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - Order { - data: OrderData { - sell_token: token_a, - buy_token: token_b, - sell_amount: 900_000_000_000_000u128.into(), - buy_amount: 100.into(), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - ]; - // Reserves are much smaller than buy amount - let pool = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (25000075, 2500007500), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }; - - // The first order by itself should not be matchable. - assert!(solve(&SlippageContext::default(), orders[0..1].to_vec(), &pool).is_none()); - - // Only the second order should match - let result = solve(&SlippageContext::default(), orders, &pool).unwrap(); - assert_eq!(result.traded_orders().count(), 1); - } - - #[test] - fn rounds_prices_in_favour_of_traders() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![ - Order { - data: OrderData { - sell_token: token_a, - buy_token: token_b, - sell_amount: 9_000_000.into(), - buy_amount: 8_500_000.into(), - kind: OrderKind::Sell, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - Order { - data: OrderData { - buy_token: token_a, - sell_token: token_b, - buy_amount: 8_000_001.into(), - sell_amount: 8_500_000.into(), - kind: OrderKind::Buy, - partially_fillable: false, - ..Default::default() - }, - ..Default::default() - } - .into(), - ]; - - let pool = Pool::uniswap( - H160::from_low_u64_be(1), - TokenPair::new(token_a, token_b).unwrap(), - (1_000_000_000_000_000_000, 1_000_000_000_000_000_000), - ); - - // Note that these orders have an excess of ~1e6 T_a because the first - // order is a sell order selling 9e6 T_a and the second order is buying - // 8e6 T_a. Use this to compute the exact amount that will get swapped - // with the pool. This will define our prices. - let excess_amount_a = 999_999.into(); - let swapped_amount_b = pool - .get_amount_out(token_b, (excess_amount_a, token_a)) - .unwrap(); - - // The naive solver sets the prices to the the swapped amounts, so: - let expected_prices = hashmap! { - token_a => swapped_amount_b, - token_b => excess_amount_a, - }; - - let settlement = solve(&SlippageContext::default(), orders, &pool.into()).unwrap(); - let trades = settlement.trade_executions().collect::>(); - - // Check the prices are set according to the expected pool swap: - assert_eq!(settlement.clearing_prices(), &expected_prices); - - // Check the executed buy amount gets rounded up for the sell order: - assert_eq!( - trades[0].buy_amount, - U256::from(9_000_000) * expected_prices[&token_a] / expected_prices[&token_b] + 1, - ); - - // Check the executed sell amount gets rounded down for the buy order: - assert_eq!( - trades[1].sell_amount, - U256::from(8_000_001) * expected_prices[&token_a] / expected_prices[&token_b], - ); - } - - #[test] - fn applies_slippage_to_amm_execution() { - let token_a = Address::from_low_u64_be(0); - let token_b = Address::from_low_u64_be(1); - let orders = vec![LimitOrder { - sell_token: token_a, - buy_token: token_b, - sell_amount: to_wei(40), - buy_amount: to_wei(30), - kind: OrderKind::Sell, - id: 0.into(), - ..Default::default() - }]; - - let amm_handler = CapturingSettlementHandler::arc(); - let pool = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (to_wei(1000).as_u128(), to_wei(1000).as_u128()), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler.clone(), - }; - let slippage = SlippageContext::default(); - solve(&slippage, orders, &pool).unwrap(); - - assert_eq!( - amm_handler.calls(), - vec![slippage - .apply_to_amm_execution(AmmOrderExecution { - input_max: TokenAmount::new(token_a, to_wei(40)), - output: TokenAmount::new( - token_b, - pool.get_amount_out(token_b, (to_wei(40), token_a)).unwrap() - ), - internalizable: false - }) - .unwrap()], - ); - } -} diff --git a/crates/solvers/src/boundary/baseline.rs b/crates/solvers/src/boundary/baseline.rs index 51a4a8a2ab..b704475ce2 100644 --- a/crates/solvers/src/boundary/baseline.rs +++ b/crates/solvers/src/boundary/baseline.rs @@ -3,7 +3,7 @@ use { crate::{ boundary, - domain::{eth, liquidity, order, solver::baseline}, + domain::{eth, liquidity, order, solver}, }, ethereum_types::{H160, U256}, model::TokenPair, @@ -33,11 +33,7 @@ impl<'a> Solver<'a> { } } - pub fn route( - &self, - request: baseline::Request, - max_hops: usize, - ) -> Option> { + pub fn route(&self, request: solver::Request, max_hops: usize) -> Option> { let candidates = self.base_tokens.path_candidates_with_hops( request.sell.token.0, request.buy.token.0, @@ -95,7 +91,7 @@ impl<'a> Solver<'a> { .max_by_key(|(_, buy)| buy.value)?, }; - baseline::Route::new(segments) + solver::Route::new(segments) } fn traverse_path( @@ -103,7 +99,7 @@ impl<'a> Solver<'a> { path: &[&OnchainLiquidity], mut sell_token: H160, mut sell_amount: U256, - ) -> Option>> { + ) -> Option>> { let mut segments = Vec::new(); for liquidity in path { let reference_liquidity = self @@ -117,7 +113,7 @@ impl<'a> Solver<'a> { .expect("Inconsistent path"); let buy_amount = liquidity.get_amount_out(buy_token, (sell_amount, sell_token))?; - segments.push(baseline::Segment { + segments.push(solver::Segment { liquidity: reference_liquidity, input: eth::Asset { token: eth::TokenAddress(sell_token), diff --git a/crates/solvers/src/boundary/mod.rs b/crates/solvers/src/boundary/mod.rs index 74319cdcdd..3be2864e8d 100644 --- a/crates/solvers/src/boundary/mod.rs +++ b/crates/solvers/src/boundary/mod.rs @@ -3,6 +3,5 @@ pub mod baseline; pub mod liquidity; -pub mod naive; pub type Result = anyhow::Result; diff --git a/crates/solvers/src/boundary/naive.rs b/crates/solvers/src/boundary/naive.rs deleted file mode 100644 index 5a463040b2..0000000000 --- a/crates/solvers/src/boundary/naive.rs +++ /dev/null @@ -1,249 +0,0 @@ -use { - crate::{ - boundary::liquidity::constant_product::to_boundary_pool, - domain::{ - eth, - liquidity, - order, - solution::{self}, - }, - }, - ethereum_types::H160, - itertools::Itertools, - model::order::{Order, OrderClass, OrderData, OrderKind, OrderMetadata, OrderUid}, - num::{BigRational, One}, - shared::external_prices::ExternalPrices, - solver::{ - liquidity::{ - slippage::{SlippageCalculator, SlippageContext}, - AmmOrderExecution, - ConstantProductOrder, - Exchange, - LimitOrder, - LimitOrderExecution, - LimitOrderId, - SettlementHandling, - }, - settlement::SettlementEncoder, - solver::naive_solver, - }, - std::sync::{Arc, Mutex}, -}; - -pub fn solve( - orders: &[&order::Order], - liquidity: &liquidity::Liquidity, -) -> Option { - let pool = match &liquidity.state { - liquidity::State::ConstantProduct(pool) => pool, - _ => return None, - }; - - // Note that the `order::Order` -> `boundary::LimitOrder` mapping here is - // not exact. Among other things, the signature and various signed order - // fields are missing from the `order::Order` data that the solver engines - // have access to. This means that the naive solver in the `solver` crate - // will encode "incorrect" settlements. This is fine, since we give it just - // enough data to compute the correct swapped orders and the swap amounts - // which is what the naive solver in the `solvers` crate cares about. The - // `driver` is then responsible for encoding the solution into a valid - // settlement transaction anyway. - let boundary_orders = orders - .iter() - // The naive solver currently doesn't support limit orders, so filter them out. - .filter(|order| !order.solver_determines_fee()) - .map(|order| LimitOrder { - id: match order.class { - order::Class::Market => LimitOrderId::Market(OrderUid(order.uid.0)), - order::Class::Limit => LimitOrderId::Limit(OrderUid(order.uid.0)), - }, - sell_token: order.sell.token.0, - buy_token: order.buy.token.0, - sell_amount: order.sell.amount, - buy_amount: order.buy.amount, - kind: match order.side { - order::Side::Buy => OrderKind::Buy, - order::Side::Sell => OrderKind::Sell, - }, - partially_fillable: order.partially_fillable, - user_fee: 0.into(), - settlement_handling: Arc::new(OrderHandler { - order: Order { - metadata: OrderMetadata { - uid: OrderUid(order.uid.0), - class: match order.class { - order::Class::Market => OrderClass::Market, - order::Class::Limit => OrderClass::Limit, - }, - solver_fee: 0.into(), - ..Default::default() - }, - data: OrderData { - sell_token: order.sell.token.0, - buy_token: order.buy.token.0, - sell_amount: order.sell.amount, - buy_amount: order.buy.amount, - fee_amount: 0.into(), - kind: match order.side { - order::Side::Buy => OrderKind::Buy, - order::Side::Sell => OrderKind::Sell, - }, - partially_fillable: order.partially_fillable, - ..Default::default() - }, - ..Default::default() - }, - }), - exchange: Exchange::GnosisProtocol, - }) - .collect_vec(); - - let slippage = Slippage::new(pool.tokens()); - let pool_handler = Arc::new(PoolHandler::default()); - let boundary_pool = ConstantProductOrder::for_pool( - to_boundary_pool(liquidity.address, pool)?, - pool_handler.clone(), - ); - - let boundary_solution = - naive_solver::solve(&slippage.context(), boundary_orders, &boundary_pool)?; - - let swap = pool_handler.swap.lock().unwrap().take(); - Some(solution::Solution { - id: Default::default(), - prices: solution::ClearingPrices::new( - boundary_solution - .clearing_prices() - .iter() - .map(|(token, price)| (eth::TokenAddress(*token), *price)), - ), - trades: boundary_solution - .traded_orders() - .map(|order| { - solution::Trade::Fulfillment( - solution::Fulfillment::fill( - orders - .iter() - .copied() - .find(|o| o.uid.0 == order.metadata.uid.0) - .unwrap() - .clone(), - ) - .expect("all orders can be filled, as limit orders are filtered out"), - ) - }) - .collect(), - // We can skip computing a gas estimate here because that is only used by the protocol - // when quoting trades. And because the naive solver is not able to solve single order - // auctions (which is how we model a /quote request) it can not be used for - // quoting anyway. - gas: None, - pre_interactions: vec![], - interactions: swap - .into_iter() - .map(|(input, output)| { - solution::Interaction::Liquidity(solution::LiquidityInteraction { - liquidity: liquidity.clone(), - input, - output, - internalize: false, - }) - }) - .collect(), - post_interactions: vec![], - }) -} - -// Beyond this point is... well... nameless and boundless chaos. The -// unfathomable horrors that follow are not for the faint of heart! -// -// Joking aside, the existing naive solver implementation is tightly coupled -// with the `Settlement` and `SettlementEncoder` types in the `solver` crate. -// This means that there is no convenient way to say: "please compute a solution -// given this list of orders and constant product pool" without it creating a -// full settlement for encoding. In order to adapt that API into something that -// is useful in this boundary module, we create a fake slippage context that -// applies 0 slippage (so that we can recover the exact executed amounts from -// the constant product pool) and we create capturing settlement handler -// implementations that record the swap that gets added to each settlement so -// that it can be recovered later to build a solution. - -struct Slippage { - calculator: SlippageCalculator, - prices: ExternalPrices, -} - -impl Slippage { - fn new(tokens: liquidity::TokenPair) -> Self { - // We don't actually want to include slippage yet. This is because the - // Naive solver encodes liquidity interactions and the driver is - // responsible for applying slippage to those. Create a dummy slippage - // context for use with the legacy Naive solver. - let (token0, token1) = tokens.get(); - Self { - calculator: SlippageCalculator::from_bps(0, None), - prices: ExternalPrices::new( - H160::default(), - [ - (token0.0, BigRational::one()), - (token1.0, BigRational::one()), - ] - .into_iter() - .collect(), - ) - .unwrap(), - } - } - - fn context(&self) -> SlippageContext { - self.calculator.context(&self.prices) - } -} - -struct OrderHandler { - order: Order, -} - -impl SettlementHandling for OrderHandler { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn encode( - &self, - execution: LimitOrderExecution, - encoder: &mut SettlementEncoder, - ) -> anyhow::Result<()> { - encoder.add_trade(self.order.clone(), execution.filled, execution.fee)?; - Ok(()) - } -} - -#[derive(Default)] -struct PoolHandler { - swap: Mutex>, -} - -impl SettlementHandling for PoolHandler { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn encode( - &self, - execution: AmmOrderExecution, - _: &mut SettlementEncoder, - ) -> anyhow::Result<()> { - *self.swap.lock().unwrap() = Some(( - eth::Asset { - token: eth::TokenAddress(execution.input_max.token), - amount: execution.input_max.amount, - }, - eth::Asset { - token: eth::TokenAddress(execution.output.token), - amount: execution.output.amount, - }, - )); - Ok(()) - } -} diff --git a/crates/solvers/src/domain/solver/baseline.rs b/crates/solvers/src/domain/solver.rs similarity index 97% rename from crates/solvers/src/domain/solver/baseline.rs rename to crates/solvers/src/domain/solver.rs index 83adb83127..9d991d5c60 100644 --- a/crates/solvers/src/domain/solver/baseline.rs +++ b/crates/solvers/src/domain/solver.rs @@ -15,12 +15,13 @@ use { order::{self, Order}, solution, }, + infra::metrics, }, ethereum_types::U256, std::{cmp, collections::HashSet, sync::Arc}, }; -pub struct Baseline(Arc); +pub struct Solver(Arc); /// The amount of time we aim the solver to finish before the final deadline is /// reached. @@ -66,7 +67,7 @@ struct Inner { native_token_price_estimation_amount: eth::U256, } -impl Baseline { +impl Solver { /// Creates a new baseline solver for the specified configuration. pub fn new(config: Config) -> Self { Self(Arc::new(Inner { @@ -82,12 +83,14 @@ impl Baseline { /// Solves the specified auction, returning a vector of all possible /// solutions. pub async fn solve(&self, auction: auction::Auction) -> Vec { + metrics::solve(&auction); + let deadline = auction.deadline.clone(); // Make sure to push the CPU-heavy code to a separate thread in order to // not lock up the [`tokio`] runtime and cause it to slow down handling // the real async things. For larger settlements, this can block in the // 100s of ms. let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); - let deadline = auction + let remaining = auction .deadline .clone() .reduce(DEADLINE_SLACK) @@ -101,7 +104,7 @@ impl Baseline { inner.solve(auction, sender); }; - if tokio::time::timeout(deadline, tokio::spawn(background_work)) + if tokio::time::timeout(remaining, tokio::spawn(background_work)) .await .is_err() { @@ -112,6 +115,7 @@ impl Baseline { while let Ok(solution) = receiver.try_recv() { solutions.push(solution); } + metrics::solved(&deadline, &solutions); solutions } } diff --git a/crates/solvers/src/domain/solver/mod.rs b/crates/solvers/src/domain/solver/mod.rs deleted file mode 100644 index cf4d134583..0000000000 --- a/crates/solvers/src/domain/solver/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{ - domain::{auction, solution}, - infra::metrics, -}; - -pub mod baseline; -pub mod naive; - -pub use self::{baseline::Baseline, naive::Naive}; - -pub enum Solver { - Baseline(Baseline), - Naive(Naive), -} - -impl Solver { - /// Solves a given auction and returns multiple solutions. We allow - /// returning multiple solutions to later merge multiple non-overlapping - /// solutions to get one big more gas efficient solution. - pub async fn solve(&self, auction: auction::Auction) -> Vec { - metrics::solve(&auction); - let deadline = auction.deadline.clone(); - let solutions = match self { - Solver::Baseline(solver) => solver.solve(auction).await, - Solver::Naive(solver) => solver.solve(auction).await, - }; - metrics::solved(&deadline, &solutions); - solutions - } -} diff --git a/crates/solvers/src/domain/solver/naive.rs b/crates/solvers/src/domain/solver/naive.rs deleted file mode 100644 index 8b31ff7227..0000000000 --- a/crates/solvers/src/domain/solver/naive.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! "Naive" solver implementation. -//! -//! The naive solver is a solver that collects all orders over a single token -//! pair, computing how many leftover tokens can't be matched peer-to-peer, and -//! matching that excess over a Uniswap V2 pool. This allows for naive -//! coincidence of wants over a single Uniswap V2 pools. - -use { - crate::{ - boundary, - domain::{auction, eth, liquidity, order, solution}, - }, - std::collections::HashMap, -}; - -pub struct Naive; - -impl Naive { - /// Solves the specified auction, returning a vector of all possible - /// solutions. - pub async fn solve(&self, auction: auction::Auction) -> Vec { - // Make sure to push the CPU-heavy code to a separate thread in order to - // not lock up the [`tokio`] runtime and cause it to slow down handling - // the real async things. - let span = tracing::Span::current(); - tokio::task::spawn_blocking(move || { - let _entered = span.enter(); - let groups = group_by_token_pair(&auction); - groups - .values() - .enumerate() - .filter_map(|(i, group)| { - boundary::naive::solve(&group.orders, group.liquidity).map(|solution| { - let gas = solution::INITIALIZATION_COST - + solution::SETTLEMENT - + solution::ERC20_TRANSFER * solution.trades.len() as u64 * 2 - + group.liquidity.gas.0.as_u64(); // this is pessimistic in case the pool is not used - solution - .with_gas(eth::Gas(gas.into())) - .with_id(solution::Id(i as u64)) - }) - }) - .map(|solution| solution.with_buffers_internalizations(&auction.tokens)) - .collect() - }) - .await - .expect("naive solver unexpected panic") - } -} - -#[derive(Debug)] -struct Group<'a> { - orders: Vec<&'a order::Order>, - liquidity: &'a liquidity::Liquidity, - pool: &'a liquidity::constant_product::Pool, -} - -type Groups<'a> = HashMap>; - -/// Groups an auction by token pairs, where each group contains all orders over -/// the token pair as well as the **deepest** constant product pool (i.e. most -/// liquidity, which translates to a higher `K` value for Uniswap V2 style -/// constant product pools). -fn group_by_token_pair(auction: &auction::Auction) -> Groups { - let mut groups = Groups::new(); - - for liquidity in &auction.liquidity { - let pool = match &liquidity.state { - liquidity::State::ConstantProduct(pool) => pool, - _ => continue, - }; - - groups - .entry(pool.tokens()) - .and_modify(|group| { - if group.pool.k() < pool.k() { - group.liquidity = liquidity; - group.pool = pool; - } - }) - .or_insert_with(|| Group { - orders: Vec::new(), - liquidity, - pool, - }); - } - - for order in &auction.orders { - // The naive solver algorithm is sensitive to 0-amount orders (i.e. they - // cause panics). Make sure we don't consider them. - if order.sell.amount.is_zero() || order.buy.amount.is_zero() { - continue; - } - - let tokens = match liquidity::TokenPair::new(order.sell.token, order.buy.token) { - Some(value) => value, - None => continue, - }; - - groups - .entry(tokens) - .and_modify(|group| group.orders.push(order)); - } - - groups.retain(|_, group| !group.orders.is_empty()); - groups -} diff --git a/crates/solvers/src/infra/cli.rs b/crates/solvers/src/infra/cli.rs index 3130bbd9cd..002ba141da 100644 --- a/crates/solvers/src/infra/cli.rs +++ b/crates/solvers/src/infra/cli.rs @@ -35,6 +35,4 @@ pub enum Command { #[clap(long, env)] config: PathBuf, }, - /// optimistically batch similar orders and get difference from AMMs - Naive, } diff --git a/crates/solvers/src/infra/config/baseline.rs b/crates/solvers/src/infra/config.rs similarity index 81% rename from crates/solvers/src/infra/config/baseline.rs rename to crates/solvers/src/infra/config.rs index 354d383bff..9f17f40c66 100644 --- a/crates/solvers/src/infra/config/baseline.rs +++ b/crates/solvers/src/infra/config.rs @@ -1,7 +1,7 @@ use { crate::{ - domain::{eth, solver::baseline}, - infra::{config::unwrap_or_log, contracts}, + domain::{eth, solver}, + infra::contracts, util::serialize, }, chain::Chain, @@ -9,7 +9,7 @@ use { serde::Deserialize, serde_with::serde_as, shared::price_estimation::gas::SETTLEMENT_OVERHEAD, - std::path::Path, + std::{fmt::Debug, path::Path}, tokio::fs, }; @@ -55,7 +55,7 @@ struct Config { /// # Panics /// /// This method panics if the config is invalid or on I/O errors. -pub async fn load(path: &Path) -> baseline::Config { +pub async fn load(path: &Path) -> solver::Config { let data = fs::read_to_string(path) .await .unwrap_or_else(|e| panic!("I/O error while reading {path:?}: {e:?}")); @@ -73,7 +73,7 @@ pub async fn load(path: &Path) -> baseline::Config { ), }; - baseline::Config { + solver::Config { weth, base_tokens: config .base_tokens @@ -87,6 +87,24 @@ pub async fn load(path: &Path) -> baseline::Config { } } +/// Unwraps result or logs a `TOML` parsing error. +fn unwrap_or_log(result: Result, path: &P) -> T +where + E: Debug, + P: Debug, +{ + result.unwrap_or_else(|err| { + if std::env::var("TOML_TRACE_ERROR").is_ok_and(|v| v == "1") { + panic!("failed to parse TOML config at {path:?}: {err:#?}") + } else { + panic!( + "failed to parse TOML config at: {path:?}. Set TOML_TRACE_ERROR=1 to print \ + parsing error but this may leak secrets." + ) + } + }) +} + /// Returns minimum gas used for settling a single order. /// (not accounting for the cost of additional interactions) fn default_gas_offset() -> i64 { diff --git a/crates/solvers/src/infra/config/mod.rs b/crates/solvers/src/infra/config/mod.rs deleted file mode 100644 index 0804e5834a..0000000000 --- a/crates/solvers/src/infra/config/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::fmt::Debug; - -pub mod baseline; - -/// Unwraps result or logs a `TOML` parsing error. -fn unwrap_or_log(result: Result, path: &P) -> T -where - E: Debug, - P: Debug, -{ - result.unwrap_or_else(|err| { - if std::env::var("TOML_TRACE_ERROR").is_ok_and(|v| v == "1") { - panic!("failed to parse TOML config at {path:?}: {err:#?}") - } else { - panic!( - "failed to parse TOML config at: {path:?}. Set TOML_TRACE_ERROR=1 to print \ - parsing error but this may leak secrets." - ) - } - }) -} diff --git a/crates/solvers/src/run.rs b/crates/solvers/src/run.rs index e047643453..1b2e212db9 100644 --- a/crates/solvers/src/run.rs +++ b/crates/solvers/src/run.rs @@ -2,7 +2,7 @@ use tokio::signal::unix::{self, SignalKind}; use { crate::{ - domain::solver::{self, Solver}, + domain::solver, infra::{cli, config}, }, clap::Parser, @@ -30,10 +30,9 @@ async fn run_with(args: cli::Args, bind: Option>) { let solver = match args.command { cli::Command::Baseline { config } => { - let config = config::baseline::load(&config).await; - Solver::Baseline(solver::Baseline::new(config)) + let config = config::load(&config).await; + solver::Solver::new(config) } - cli::Command::Naive => Solver::Naive(solver::Naive), }; crate::api::Api { diff --git a/crates/solvers/src/tests/baseline/bal_liquidity.rs b/crates/solvers/src/tests/cases/bal_liquidity.rs similarity index 100% rename from crates/solvers/src/tests/baseline/bal_liquidity.rs rename to crates/solvers/src/tests/cases/bal_liquidity.rs diff --git a/crates/solvers/src/tests/baseline/buy_order_rounding.rs b/crates/solvers/src/tests/cases/buy_order_rounding.rs similarity index 100% rename from crates/solvers/src/tests/baseline/buy_order_rounding.rs rename to crates/solvers/src/tests/cases/buy_order_rounding.rs diff --git a/crates/solvers/src/tests/baseline/direct_swap.rs b/crates/solvers/src/tests/cases/direct_swap.rs similarity index 100% rename from crates/solvers/src/tests/baseline/direct_swap.rs rename to crates/solvers/src/tests/cases/direct_swap.rs diff --git a/crates/solvers/src/tests/baseline/internalization.rs b/crates/solvers/src/tests/cases/internalization.rs similarity index 100% rename from crates/solvers/src/tests/baseline/internalization.rs rename to crates/solvers/src/tests/cases/internalization.rs diff --git a/crates/solvers/src/tests/baseline/limit_order_quoting.rs b/crates/solvers/src/tests/cases/limit_order_quoting.rs similarity index 100% rename from crates/solvers/src/tests/baseline/limit_order_quoting.rs rename to crates/solvers/src/tests/cases/limit_order_quoting.rs diff --git a/crates/solvers/src/tests/baseline/mod.rs b/crates/solvers/src/tests/cases/mod.rs similarity index 100% rename from crates/solvers/src/tests/baseline/mod.rs rename to crates/solvers/src/tests/cases/mod.rs diff --git a/crates/solvers/src/tests/baseline/partial_fill.rs b/crates/solvers/src/tests/cases/partial_fill.rs similarity index 100% rename from crates/solvers/src/tests/baseline/partial_fill.rs rename to crates/solvers/src/tests/cases/partial_fill.rs diff --git a/crates/solvers/src/tests/mod.rs b/crates/solvers/src/tests/mod.rs index f12dc63a5a..fcf64af0e0 100644 --- a/crates/solvers/src/tests/mod.rs +++ b/crates/solvers/src/tests/mod.rs @@ -10,8 +10,7 @@ use { tokio::{sync::oneshot, task::JoinHandle}, }; -mod baseline; -mod naive; +mod cases; /// A solver engine handle for E2E testing. pub struct SolverEngine { diff --git a/crates/solvers/src/tests/naive/extract_deepest_pool.rs b/crates/solvers/src/tests/naive/extract_deepest_pool.rs deleted file mode 100644 index 2f046cb6ba..0000000000 --- a/crates/solvers/src/tests/naive/extract_deepest_pool.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Test that demonstrates that the Naive solver will use the **deepest** pool -//! for solving when multiple different UniswapV2-like pools exist for a -//! specific token pair. -//! -//! The rationale behind this choise is that the deepest pools are typically the -//! most representative of the actual token price, and in general give better -//! prices for larger orders (in theory, for smaller orders it is possible for -//! the shallow pool to offer better prices, but this should be in exceptional -//! cases, and not worth considering in the solver in order to keep things -//! simple). - -use {crate::tests, serde_json::json}; - -#[tokio::test] -async fn test() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - { - "uid": "0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ - 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ - 2a2a2a2a", - "sellToken": "0x0101010101010101010101010101010101010101", - "buyToken": "0x0202020202020202020202020202020202020202", - "sellAmount": "100", - "fullSellAmount": "1000000000000000000", - "buyAmount": "1", - "fullBuyAmount": "1000000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0x0101010101010101010101010101010101010101": { - "balance": "100" - }, - "0x0202020202020202020202020202020202020202": { - "balance": "100" - } - }, - "fee": "0.003", - "id": "0", - "address": "0x2222222222222222222222222222222222222222", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "0" - }, - { - "kind": "constantProduct", - "tokens": { - "0x0101010101010101010101010101010101010101": { - "balance": "10000000" - }, - "0x0202020202020202020202020202020202020202": { - "balance": "10000000" - } - }, - "fee": "0.003", - "id": "1", - "address": "0x1111111111111111111111111111111111111111", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "0" - }, - { - "kind": "constantProduct", - "tokens": { - "0x0303030303030303030303030303030303030303": { - "balance": "10000000000000000" - }, - "0x0404040404040404040404040404040404040404": { - "balance": "10000000000000000" - } - }, - "fee": "0.003", - "id": "2", - "address": "0x3333333333333333333333333333333333333333", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "0" - }, - ], - "effectiveGasPrice": "0", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [{ - "id": 0, - "prices": { - "0x0101010101010101010101010101010101010101": "99", - "0x0202020202020202020202020202020202020202": "100", - }, - "trades": [ - { - "kind": "fulfillment", - "order": "0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ - 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ - 2a2a2a2a", - "executedAmount": "100", - }, - ], - "preInteractions": [], - "interactions": [ - { - "kind": "liquidity", - "internalize": false, - "id": "1", - "inputToken": "0x0101010101010101010101010101010101010101", - "outputToken": "0x0202020202020202020202020202020202020202", - "inputAmount": "100", - "outputAmount": "99" - }, - ], - "postInteractions": [], - "gas": 94391, - }] - }), - ); -} diff --git a/crates/solvers/src/tests/naive/filters_out_of_price_orders.rs b/crates/solvers/src/tests/naive/filters_out_of_price_orders.rs deleted file mode 100644 index 8ea6f874e3..0000000000 --- a/crates/solvers/src/tests/naive/filters_out_of_price_orders.rs +++ /dev/null @@ -1,172 +0,0 @@ -//! This test verifies that orders that are out of price get filtered out, but -//! a solution with the "reasonably" priced orders is produced. - -use {crate::tests, serde_json::json}; - -#[tokio::test] -async fn sell_orders_on_both_sides() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - // Unreasonable order a -> b - { - "uid": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "1000000000000000000", - "fullSellAmount": "1000000000000000000", - "buyAmount": "1000000000000000000000", - "fullBuyAmount": "1000000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - // Reasonable order a -> b - { - "uid": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "1000000000000000000000", - "fullSellAmount": "1000000000000000000000", - "buyAmount": "1000000000000000000000", - "fullBuyAmount": "1000000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - // Reasonable order a -> b - { - "uid": "0x0303030303030303030303030303030303030303030303030303030303030303\ - 0303030303030303030303030303030303030303\ - 03030303", - "sellToken": "0x000000000000000000000000000000000000000b", - "buyToken": "0x000000000000000000000000000000000000000a", - "sellAmount": "1000000000000000000000", - "fullSellAmount": "1000000000000000000000", - "buyAmount": "1000000000000000000000", - "fullBuyAmount": "1000000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - // Unreasonable order a -> b - { - "uid": "0x0404040404040404040404040404040404040404040404040404040404040404\ - 0404040404040404040404040404040404040404\ - 04040404", - "sellToken": "0x000000000000000000000000000000000000000b", - "buyToken": "0x000000000000000000000000000000000000000a", - "sellAmount": "2000000000000000000", - "fullSellAmount": "2000000000000000000", - "buyAmount": "1000000000000000000000", - "fullBuyAmount": "1000000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0x000000000000000000000000000000000000000a": { - "balance": "1000000000000000000000000" - }, - "0x000000000000000000000000000000000000000b": { - "balance": "1000000000000000000000000" - } - }, - "fee": "0.003", - "id": "0", - "address": "0xffffffffffffffffffffffffffffffffffffffff", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "110000" - }, - ], - "effectiveGasPrice": "15000000000", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [{ - "id": 0, - "prices": { - "0x000000000000000000000000000000000000000a": "1000000000000000000000000", - "0x000000000000000000000000000000000000000b": "1000000000000000000000000", - }, - "trades": [ - { - "kind": "fulfillment", - "order": "0x0303030303030303030303030303030303030303030303030303030303030303\ - 0303030303030303030303030303030303030303\ - 03030303", - "executedAmount": "1000000000000000000000", - }, - { - "kind": "fulfillment", - "order": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "executedAmount": "1000000000000000000000", - }, - ], - "preInteractions": [], - "interactions": [], - "postInteractions": [], - "gas": 259417, - }] - }), - ); -} diff --git a/crates/solvers/src/tests/naive/limit_order_price.rs b/crates/solvers/src/tests/naive/limit_order_price.rs deleted file mode 100644 index 0ac2df06ab..0000000000 --- a/crates/solvers/src/tests/naive/limit_order_price.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! This test verifies that the limit order's limit price is respected after -//! surplus fees are taken from the order. - -use {crate::tests, serde_json::json}; - -#[tokio::test] -async fn test() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - { - "uid": "0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ - 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a\ - 2a2a2a2a", - "sellToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "buyToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "sellAmount": "22397494", - "fullSellAmount": "1000000000000000000", - "buyAmount": "18477932550000000", - "fullBuyAmount": "1000000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "limit", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": { - "balance": "36338096110368" - }, - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": { - "balance": "30072348537379906026018" - } - }, - "fee": "0.003", - "id": "0", - "address": "0x0000000000000000000000000000000000000000", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "110000" - }, - ], - "effectiveGasPrice": "15000000000", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [] - }), - ); -} diff --git a/crates/solvers/src/tests/naive/matches_orders.rs b/crates/solvers/src/tests/naive/matches_orders.rs deleted file mode 100644 index 031224f057..0000000000 --- a/crates/solvers/src/tests/naive/matches_orders.rs +++ /dev/null @@ -1,508 +0,0 @@ -//! Tests for various permutiations of matching combinatinos of sell and buy -//! orders. - -use {crate::tests, serde_json::json}; - -#[tokio::test] -async fn sell_orders_on_both_sides() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - { - "uid": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "40000000000000000000", - "fullSellAmount": "40000000000000000000", - "buyAmount": "30000000000000000000", - "fullBuyAmount": "30000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - { - "uid": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "sellToken": "0x000000000000000000000000000000000000000b", - "buyToken": "0x000000000000000000000000000000000000000a", - "sellAmount": "100000000000000000000", - "fullSellAmount": "100000000000000000000", - "buyAmount": "90000000000000000000", - "fullBuyAmount": "90000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0x000000000000000000000000000000000000000a": { - "balance": "1000000000000000000000" - }, - "0x000000000000000000000000000000000000000b": { - "balance": "1000000000000000000000" - } - }, - "fee": "0.003", - "id": "0", - "address": "0xffffffffffffffffffffffffffffffffffffffff", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "110000" - }, - ], - "effectiveGasPrice": "15000000000", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [{ - "id": 0, - "prices": { - "0x000000000000000000000000000000000000000a": "57576575881490625723", - "0x000000000000000000000000000000000000000b": "54287532963535509684", - }, - "trades": [ - { - "kind": "fulfillment", - "order": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "executedAmount": "40000000000000000000", - }, - { - "kind": "fulfillment", - "order": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "executedAmount": "100000000000000000000", - }, - ], - "preInteractions": [], - "interactions": [ - { - "kind": "liquidity", - "internalize": false, - "id": "0", - "inputToken": "0x000000000000000000000000000000000000000b", - "outputToken": "0x000000000000000000000000000000000000000a", - "inputAmount": "57576575881490625723", - "outputAmount": "54287532963535509685" - }, - ], - "postInteractions": [], - "gas": 259417, - }] - }), - ); -} - -#[tokio::test] -async fn sell_orders_on_one_side() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - { - "uid": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "40000000000000000000", - "fullSellAmount": "40000000000000000000", - "buyAmount": "30000000000000000000", - "fullBuyAmount": "30000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - { - "uid": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "100000000000000000000", - "fullSellAmount": "100000000000000000000", - "buyAmount": "90000000000000000000", - "fullBuyAmount": "90000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0x000000000000000000000000000000000000000a": { - "balance": "1000000000000000000000000" - }, - "0x000000000000000000000000000000000000000b": { - "balance": "1000000000000000000000000" - } - }, - "fee": "0.003", - "id": "0", - "address": "0xffffffffffffffffffffffffffffffffffffffff", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "110000" - }, - ], - "effectiveGasPrice": "15000000000", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [{ - "id": 0, - "prices": { - "0x000000000000000000000000000000000000000a": "139560520142598496101", - "0x000000000000000000000000000000000000000b": "140000000000000000000", - }, - "trades": [ - { - "kind": "fulfillment", - "order": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "executedAmount": "40000000000000000000", - }, - { - "kind": "fulfillment", - "order": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "executedAmount": "100000000000000000000", - }, - ], - "preInteractions": [], - "interactions": [ - { - "kind": "liquidity", - "internalize": false, - "id": "0", - "inputToken": "0x000000000000000000000000000000000000000a", - "outputToken": "0x000000000000000000000000000000000000000b", - "inputAmount": "140000000000000000000", - "outputAmount": "139560520142598496102" - }, - ], - "postInteractions": [], - "gas": 259417, - }] - }), - ); -} - -#[tokio::test] -async fn buy_orders_on_both_sides() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - { - "uid": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "40000000000000000000", - "fullSellAmount": "40000000000000000000", - "buyAmount": "30000000000000000000", - "fullBuyAmount": "30000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "buy", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - { - "uid": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "sellToken": "0x000000000000000000000000000000000000000b", - "buyToken": "0x000000000000000000000000000000000000000a", - "sellAmount": "100000000000000000000", - "fullSellAmount": "100000000000000000000", - "buyAmount": "90000000000000000000", - "fullBuyAmount": "90000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "buy", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0x000000000000000000000000000000000000000a": { - "balance": "1000000000000000000000" - }, - "0x000000000000000000000000000000000000000b": { - "balance": "1000000000000000000000" - } - }, - "fee": "0.003", - "id": "0", - "address": "0xffffffffffffffffffffffffffffffffffffffff", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "110000" - }, - ], - "effectiveGasPrice": "15000000000", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [{ - "id": 0, - "prices": { - "0x000000000000000000000000000000000000000a": "66231662019024105282", - "0x000000000000000000000000000000000000000b": "61942706346833798925", - }, - "trades": [ - { - "kind": "fulfillment", - "order": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "executedAmount": "30000000000000000000", - }, - { - "kind": "fulfillment", - "order": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "executedAmount": "90000000000000000000", - }, - ], - "preInteractions": [], - "interactions": [ - { - "kind": "liquidity", - "internalize": false, - "id": "0", - "inputToken": "0x000000000000000000000000000000000000000b", - "outputToken": "0x000000000000000000000000000000000000000a", - "inputAmount": "66231662019024105282", - "outputAmount": "61942706346833798926" - }, - ], - "postInteractions": [], - "gas": 259417, - }] - }), - ); -} - -#[tokio::test] -async fn buy_and_sell_orders() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - { - "uid": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "40000000000000000000", - "fullSellAmount": "40000000000000000000", - "buyAmount": "30000000000000000000", - "fullBuyAmount": "30000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "buy", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - { - "uid": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "sellToken": "0x000000000000000000000000000000000000000b", - "buyToken": "0x000000000000000000000000000000000000000a", - "sellAmount": "100000000000000000000", - "fullSellAmount": "40000000000000000000", - "buyAmount": "90000000000000000000", - "fullBuyAmount": "30000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0x000000000000000000000000000000000000000a": { - "balance": "1000000000000000000000" - }, - "0x000000000000000000000000000000000000000b": { - "balance": "1000000000000000000000" - } - }, - "fee": "0.003", - "id": "0", - "address": "0xffffffffffffffffffffffffffffffffffffffff", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "110000" - }, - ], - "effectiveGasPrice": "15000000000", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [{ - "id": 0, - "prices": { - "0x000000000000000000000000000000000000000a": "70000000000000000000", - "0x000000000000000000000000000000000000000b": "65237102608923246618", - }, - "trades": [ - { - "kind": "fulfillment", - "order": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "executedAmount": "30000000000000000000", - }, - { - "kind": "fulfillment", - "order": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "executedAmount": "100000000000000000000", - }, - ], - "preInteractions": [], - "interactions": [ - { - "kind": "liquidity", - "internalize": false, - "id": "0", - "inputToken": "0x000000000000000000000000000000000000000b", - "outputToken": "0x000000000000000000000000000000000000000a", - "inputAmount": "70000000000000000000", - "outputAmount": "65237102608923246619" - }, - ], - "postInteractions": [], - "gas": 259417, - }] - }), - ); -} diff --git a/crates/solvers/src/tests/naive/mod.rs b/crates/solvers/src/tests/naive/mod.rs deleted file mode 100644 index df3005f3f3..0000000000 --- a/crates/solvers/src/tests/naive/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod extract_deepest_pool; -mod filters_out_of_price_orders; -mod limit_order_price; -mod matches_orders; -mod reserves_too_small; -mod rounds_prices_in_favour_of_traders; -mod swap_less_than_reserves; -mod without_pool; diff --git a/crates/solvers/src/tests/naive/reserves_too_small.rs b/crates/solvers/src/tests/naive/reserves_too_small.rs deleted file mode 100644 index a4c72d5132..0000000000 --- a/crates/solvers/src/tests/naive/reserves_too_small.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! This test verifies that it filters out large orders when pool reserves are -//! too small to be able to handle them. - -use {crate::tests, serde_json::json}; - -#[tokio::test] -async fn test() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - { - "uid": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "70145218378783248142575", - "fullSellAmount": "70145218378783248142575", - "buyAmount": "70123226323", - "fullBuyAmount": "70123226323", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - { - "uid": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "900000000000000", - "fullSellAmount": "900000000000000", - "buyAmount": "100", - "fullBuyAmount": "100", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0x000000000000000000000000000000000000000a": { - "balance": "25000075" - }, - "0x000000000000000000000000000000000000000b": { - "balance": "2500007500" - } - }, - "fee": "0.003", - "id": "0", - "address": "0xffffffffffffffffffffffffffffffffffffffff", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "110000" - }, - ], - "effectiveGasPrice": "15000000000", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [{ - "id": 0, - "prices": { - "0x000000000000000000000000000000000000000a": "2500007430", - "0x000000000000000000000000000000000000000b": "900000000000000", - }, - "trades": [ - { - "kind": "fulfillment", - "order": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "executedAmount": "900000000000000", - }, - ], - "preInteractions": [], - "interactions": [ - { - "kind": "liquidity", - "internalize": false, - "id": "0", - "inputToken": "0x000000000000000000000000000000000000000a", - "outputToken": "0x000000000000000000000000000000000000000b", - "inputAmount": "900000000000000", - "outputAmount": "2500007430" - }, - ], - "postInteractions": [], - "gas": 204391, - }] - }), - ); -} diff --git a/crates/solvers/src/tests/naive/rounds_prices_in_favour_of_traders.rs b/crates/solvers/src/tests/naive/rounds_prices_in_favour_of_traders.rs deleted file mode 100644 index 1be1dd491c..0000000000 --- a/crates/solvers/src/tests/naive/rounds_prices_in_favour_of_traders.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! This test verifies that rounding is done in favour of the traders. This is -//! a weird detail that stems from the fact the UniswapV2-like pool swaps are -//! encoded as **swapTokensForExactTokens** (i.e. a "buy" order). The reason for -//! this is to make settlements less likely to revert (because buy swaps have -//! order fees as guaranteed "buffers", while sell swaps only have buffers if -//! they are already in the contract). The rounding is needed because the -//! settlement contract will round executed amounts in favour or the trader, -//! meaning that the clearing prices can cause the total buy amount to be a few -//! wei larger than the exact output that is encoded. - -use {crate::tests, serde_json::json}; - -#[tokio::test] -async fn test() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - { - "uid": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "9000000", - "fullSellAmount": "9000000", - "buyAmount": "8500000", - "fullBuyAmount": "8500000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - { - "uid": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "sellToken": "0x000000000000000000000000000000000000000b", - "buyToken": "0x000000000000000000000000000000000000000a", - "sellAmount": "8500000", - "fullSellAmount": "8500000", - "buyAmount": "8000001", - "fullBuyAmount": "8000001", - "feePolicies": [], - "validTo": 0, - "kind": "buy", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0x000000000000000000000000000000000000000a": { - "balance": "1000000000000000000" - }, - "0x000000000000000000000000000000000000000b": { - "balance": "1000000000000000000" - } - }, - "fee": "0.003", - "id": "0", - "address": "0xffffffffffffffffffffffffffffffffffffffff", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "110000" - }, - ], - "effectiveGasPrice": "15000000000", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [{ - "id": 0, - "prices": { - "0x000000000000000000000000000000000000000a": "996999", - "0x000000000000000000000000000000000000000b": "999999", - }, - "trades": [ - { - "kind": "fulfillment", - "order": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "executedAmount": "9000000", - }, - { - "kind": "fulfillment", - "order": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "executedAmount": "8000001", - }, - ], - "preInteractions": [], - "interactions": [ - { - "kind": "liquidity", - "internalize": false, - "id": "0", - "inputToken": "0x000000000000000000000000000000000000000a", - "outputToken": "0x000000000000000000000000000000000000000b", - "inputAmount": "999999", - "outputAmount": "997000" - }, - ], - "postInteractions": [], - "gas": 259417, - }] - }), - ); -} diff --git a/crates/solvers/src/tests/naive/swap_less_than_reserves.rs b/crates/solvers/src/tests/naive/swap_less_than_reserves.rs deleted file mode 100644 index 3df8a591f0..0000000000 --- a/crates/solvers/src/tests/naive/swap_less_than_reserves.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! This test verifies that given a swap with multiple orders, including limit -//! orders, the settlement building does not cause the naive solver to generate -//! a solution that swaps more than the pools reserves. -//! -//! This test verifies a regression that was introduced with limit orders, where -//! the incorrect order amounts were used for computing the final pool swap -//! amounts, causing it buy more from the pool than it actual had. - -use {crate::tests, serde_json::json}; - -#[tokio::test] -async fn test() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - { - "uid": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "sellToken": "0xD533a949740bb3306d119CC777fa900bA034cd52", - "buyToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "sellAmount": "2161740107040163317224", - "fullSellAmount": "2161740107040163317224", - "buyAmount": "2146544862", - "fullBuyAmount": "2146544862", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "limit", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - { - "uid": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "sellToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "buyToken": "0xD533a949740bb3306d119CC777fa900bA034cd52", - "sellAmount": "495165988", - "fullSellAmount": "495165988", - "buyAmount": "1428571428571428571428", - "fullBuyAmount": "1428571428571428571428", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "limit", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { - "balance": "32275540" - }, - "0xD533a949740bb3306d119CC777fa900bA034cd52": { - "balance": "33308141034569852391" - } - }, - "fee": "0.003", - "id": "0", - "address": "0x210a97ba874a8e279c95b350ae8ba143a143c159", - "router": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d", - "gasEstimate": "110000" - }, - ], - "effectiveGasPrice": "15000000000", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [] - }), - ); -} diff --git a/crates/solvers/src/tests/naive/without_pool.rs b/crates/solvers/src/tests/naive/without_pool.rs deleted file mode 100644 index d6b4419092..0000000000 --- a/crates/solvers/src/tests/naive/without_pool.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! This test verifies that the naive solver doesn't use liquidity from the pool -//! when order amounts overlap. - -use {crate::tests, serde_json::json}; - -#[tokio::test] -async fn test() { - let engine = tests::SolverEngine::new("naive", tests::Config::None).await; - - let solution = engine - .solve(json!({ - "id": "1", - "tokens": {}, - "orders": [ - { - "uid": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "sellToken": "0x000000000000000000000000000000000000000a", - "buyToken": "0x000000000000000000000000000000000000000b", - "sellAmount": "1001000000000000000000", - "fullSellAmount": "1001000000000000000000", - "buyAmount": "1000000000000000000000", - "fullBuyAmount": "1000000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - { - "uid": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "sellToken": "0x000000000000000000000000000000000000000b", - "buyToken": "0x000000000000000000000000000000000000000a", - "sellAmount": "1001000000000000000000", - "fullSellAmount": "1001000000000000000000", - "buyAmount": "1000000000000000000000", - "fullBuyAmount": "1000000000000000000000", - "feePolicies": [], - "validTo": 0, - "kind": "sell", - "owner": "0x5b1e2c2762667331bc91648052f646d1b0d35984", - "partiallyFillable": false, - "preInteractions": [], - "postInteractions": [], - "sellTokenSource": "erc20", - "buyTokenDestination": "erc20", - "class": "market", - "appData": "0x6000000000000000000000000000000000000000000000000000000000000007", - "signingScheme": "presign", - "signature": "0x", - }, - ], - "liquidity": [ - { - "kind": "constantProduct", - "tokens": { - "0x000000000000000000000000000000000000000a": { - "balance": "1000001000000000000000000" - }, - "0x000000000000000000000000000000000000000b": { - "balance": "1000000000000000000000000" - } - }, - "fee": "0.003", - "id": "0", - "address": "0xffffffffffffffffffffffffffffffffffffffff", - "router": "0xffffffffffffffffffffffffffffffffffffffff", - "gasEstimate": "110000" - }, - ], - "effectiveGasPrice": "15000000000", - "deadline": "2106-01-01T00:00:00.000Z", - "surplusCapturingJitOrderOwners": [] - })) - .await; - - assert_eq!( - solution, - json!({ - "solutions": [{ - "id": 0, - "prices": { - "0x000000000000000000000000000000000000000a": "1000000000000000000000000", - "0x000000000000000000000000000000000000000b": "1000001000000000000000000", - }, - "trades": [ - { - "kind": "fulfillment", - "order": "0x0101010101010101010101010101010101010101010101010101010101010101\ - 0101010101010101010101010101010101010101\ - 01010101", - "executedAmount": "1001000000000000000000", - }, - { - "kind": "fulfillment", - "order": "0x0202020202020202020202020202020202020202020202020202020202020202\ - 0202020202020202020202020202020202020202\ - 02020202", - "executedAmount": "1001000000000000000000", - }, - ], - "preInteractions": [], - "interactions": [], - "postInteractions": [], - "gas": 259417, - }] - }), - ); -}