From 06e9c4ed58da488973299c14164c452c59505d72 Mon Sep 17 00:00:00 2001 From: Martin Beckmann Date: Wed, 14 Feb 2024 09:46:45 +0100 Subject: [PATCH] Clean up solver (#2405) # Description There was an external contribution working on a TODO comment inside the solver crate. This code actually was no longer used anywhere so they effectively wasted their time. We also regularly get comments asking about the `solver` crate and explaining the current state is always a bit weird. # Changes I tried to delete everything from the `solver` crate that was not used in the other crates. ## How to test CI --- crates/driver/src/boundary/mempool.rs | 5 +- crates/solver/Cargo.toml | 4 - crates/solver/src/analytics.rs | 153 -- crates/solver/src/arguments.rs | 554 ------- crates/solver/src/auction_preprocessing.rs | 30 - crates/solver/src/driver.rs | 449 ------ crates/solver/src/driver/gas.rs | 177 --- .../solver/src/driver/solver_settlements.rs | 86 -- crates/solver/src/driver_logger.rs | 318 ---- crates/solver/src/in_flight_orders.rs | 328 ----- crates/solver/src/interactions.rs | 5 +- crates/solver/src/interactions/balancer_v2.rs | 8 +- .../solver/src/interactions/block_coinbase.rs | 27 - crates/solver/src/lib.rs | 19 +- crates/solver/src/liquidity/slippage.rs | 227 +-- crates/solver/src/main.rs | 4 - crates/solver/src/metrics.rs | 419 ------ crates/solver/src/objective_value.rs | 168 --- crates/solver/src/orderbook.rs | 69 - crates/solver/src/run.rs | 597 -------- crates/solver/src/s3_instance_upload.rs | 149 -- .../src/s3_instance_upload_arguments.rs | 57 - .../src/settlement_post_processing/mod.rs | 165 --- .../optimize_buffer_usage.rs | 152 -- .../optimize_score.rs | 37 - .../optimize_unwrapping.rs | 213 --- crates/solver/src/settlement_ranker.rs | 315 ---- crates/solver/src/settlement_rater.rs | 291 +--- crates/solver/src/settlement_simulation.rs | 576 +------- crates/solver/src/settlement_submission.rs | 11 +- crates/solver/src/solver.rs | 736 +--------- .../solver/src/solver/balancer_sor_solver.rs | 608 -------- crates/solver/src/solver/baseline_solver.rs | 817 +---------- crates/solver/src/solver/http_solver.rs | 543 ------- .../solver/src/solver/http_solver/buffers.rs | 156 -- .../src/solver/http_solver/instance_cache.rs | 189 --- .../solver/http_solver/instance_creation.rs | 620 -------- .../src/solver/http_solver/settlement.rs | 1295 ----------------- crates/solver/src/solver/naive_solver.rs | 442 ------ crates/solver/src/solver/oneinch_solver.rs | 534 ------- crates/solver/src/solver/optimizing_solver.rs | 123 -- crates/solver/src/solver/paraswap_solver.rs | 602 -------- crates/solver/src/solver/risk_computation.rs | 176 --- .../solver/src/solver/single_order_solver.rs | 1256 ---------------- .../src/solver/single_order_solver/fills.rs | 185 --- .../src/solver/single_order_solver/merge.rs | 135 -- crates/solver/src/solver/zeroex_solver.rs | 520 ------- crates/solver/src/test.rs | 12 - docker/Dockerfile.binary | 1 - 49 files changed, 29 insertions(+), 14534 deletions(-) delete mode 100644 crates/solver/src/analytics.rs delete mode 100644 crates/solver/src/arguments.rs delete mode 100644 crates/solver/src/auction_preprocessing.rs delete mode 100644 crates/solver/src/driver.rs delete mode 100644 crates/solver/src/driver/gas.rs delete mode 100644 crates/solver/src/driver/solver_settlements.rs delete mode 100644 crates/solver/src/driver_logger.rs delete mode 100644 crates/solver/src/in_flight_orders.rs delete mode 100644 crates/solver/src/interactions/block_coinbase.rs delete mode 100644 crates/solver/src/main.rs delete mode 100644 crates/solver/src/objective_value.rs delete mode 100644 crates/solver/src/orderbook.rs delete mode 100644 crates/solver/src/run.rs delete mode 100644 crates/solver/src/s3_instance_upload.rs delete mode 100644 crates/solver/src/s3_instance_upload_arguments.rs delete mode 100644 crates/solver/src/settlement_post_processing/mod.rs delete mode 100644 crates/solver/src/settlement_post_processing/optimize_buffer_usage.rs delete mode 100644 crates/solver/src/settlement_post_processing/optimize_score.rs delete mode 100644 crates/solver/src/settlement_post_processing/optimize_unwrapping.rs delete mode 100644 crates/solver/src/settlement_ranker.rs delete mode 100644 crates/solver/src/solver/balancer_sor_solver.rs delete mode 100644 crates/solver/src/solver/http_solver.rs delete mode 100644 crates/solver/src/solver/http_solver/buffers.rs delete mode 100644 crates/solver/src/solver/http_solver/instance_cache.rs delete mode 100644 crates/solver/src/solver/http_solver/instance_creation.rs delete mode 100644 crates/solver/src/solver/http_solver/settlement.rs delete mode 100644 crates/solver/src/solver/oneinch_solver.rs delete mode 100644 crates/solver/src/solver/optimizing_solver.rs delete mode 100644 crates/solver/src/solver/paraswap_solver.rs delete mode 100644 crates/solver/src/solver/risk_computation.rs delete mode 100644 crates/solver/src/solver/single_order_solver.rs delete mode 100644 crates/solver/src/solver/single_order_solver/fills.rs delete mode 100644 crates/solver/src/solver/single_order_solver/merge.rs delete mode 100644 crates/solver/src/solver/zeroex_solver.rs delete mode 100644 crates/solver/src/test.rs diff --git a/crates/driver/src/boundary/mempool.rs b/crates/driver/src/boundary/mempool.rs index e2e498b3bd..ecbc92cd1c 100644 --- a/crates/driver/src/boundary/mempool.rs +++ b/crates/driver/src/boundary/mempool.rs @@ -12,7 +12,6 @@ use { submitter::{ flashbots_api::FlashbotsApi, public_mempool_api::{PublicMempoolApi, SubmissionNode, SubmissionNodeKind}, - Strategy, Submitter, SubmitterGasPriceEstimator, SubmitterParams, @@ -122,14 +121,14 @@ impl Mempool { )], matches!(revert_protection, RevertProtection::Enabled), )), - submitted_transactions: pool.add_sub_pool(Strategy::PublicMempool), + submitted_transactions: pool.add_sub_pool(), gas_price_estimator: eth.boundary_gas_estimator(), config, eth, }, Kind::MEVBlocker { url, .. } => Self { submit_api: Arc::new(FlashbotsApi::new(reqwest::Client::new(), url.to_owned())?), - submitted_transactions: pool.add_sub_pool(Strategy::Flashbots), + submitted_transactions: pool.add_sub_pool(), gas_price_estimator: eth.boundary_gas_estimator(), config, eth, diff --git a/crates/solver/Cargo.toml b/crates/solver/Cargo.toml index 657621123e..0cc315d070 100644 --- a/crates/solver/Cargo.toml +++ b/crates/solver/Cargo.toml @@ -10,10 +10,6 @@ name = "solver" path = "src/lib.rs" doctest = false -[[bin]] -name = "solver" -path = "src/main.rs" - [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } diff --git a/crates/solver/src/analytics.rs b/crates/solver/src/analytics.rs deleted file mode 100644 index 2dc39baef9..0000000000 --- a/crates/solver/src/analytics.rs +++ /dev/null @@ -1,153 +0,0 @@ -use { - crate::{ - driver::solver_settlements::RatedSettlement, - metrics::SolverMetrics, - settlement::Settlement, - solver::Solver, - }, - ethcontract::H160, - model::order::OrderUid, - num::{BigRational, ToPrimitive, Zero}, - shared::conversions::U256Ext, - std::{ - collections::{HashMap, HashSet}, - fmt::{Display, Formatter}, - sync::Arc, - }, -}; - -pub fn report_matched_but_not_settled( - metrics: &dyn SolverMetrics, - (_, winning_solution): &(Arc, RatedSettlement), - alternative_settlements: &[(Arc, RatedSettlement)], -) { - let submitted_orders: HashSet<_> = winning_solution - .settlement - .user_trades() - .map(|trade| trade.order.metadata.uid) - .collect(); - let other_matched_orders: HashSet<_> = alternative_settlements - .iter() - .flat_map(|(_, solution)| solution.settlement.user_trades()) - .map(|trade| trade.order.metadata.uid) - .collect(); - let matched_but_not_settled: HashSet<_> = other_matched_orders - .difference(&submitted_orders) - .copied() - .collect(); - - if !matched_but_not_settled.is_empty() { - tracing::debug!( - ?matched_but_not_settled, - "some orders were matched but not settled" - ); - } - - metrics.orders_matched_but_not_settled(matched_but_not_settled.len()); -} - -#[derive(Clone)] -struct SurplusInfo { - solver_name: String, - ratio: BigRational, -} - -impl SurplusInfo { - fn is_better_than(&self, other: &Self) -> bool { - self.ratio > other.ratio - } -} - -impl Display for SurplusInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Surplus {{solver: {}, ratio: {:.2e} }}", - self.solver_name, - self.ratio.to_f64().unwrap_or(f64::NAN) - ) - } -} - -fn get_prices(settlement: &Settlement) -> HashMap { - settlement - .clearing_prices() - .iter() - .map(|(token, price)| (*token, price.to_big_rational())) - .collect::>() -} - -/// Record metric with surplus achieved in winning settlement -/// vs that which was unrealized in other feasible solutions. -pub fn report_alternative_settlement_surplus( - metrics: &dyn SolverMetrics, - winning_settlement: &(Arc, RatedSettlement), - alternative_settlements: &[(Arc, RatedSettlement)], -) { - let (winning_solver, submitted) = winning_settlement; - let submitted_prices = get_prices(&submitted.settlement); - let submitted_surplus: HashMap<_, _> = submitted - .settlement - .user_trades() - .map(|trade| { - let sell_token_price = &submitted_prices[&trade.order.data.sell_token]; - let buy_token_price = &submitted_prices[&trade.order.data.buy_token]; - ( - trade.order.metadata.uid, - SurplusInfo { - solver_name: winning_solver.name().to_string(), - ratio: trade - .surplus_ratio(sell_token_price, buy_token_price) - .unwrap_or_else(BigRational::zero), - }, - ) - }) - .collect(); - - let best_alternative = best_surplus_by_order(alternative_settlements); - for (order_id, submitted) in submitted_surplus.iter() { - if let Some(alternative) = best_alternative.get(order_id) { - metrics.report_order_surplus( - (&submitted.ratio - &alternative.ratio) - .to_f64() - .unwrap_or_default(), - ); - if alternative.is_better_than(submitted) { - tracing::debug!( - ?order_id, %submitted, %alternative, - "submission surplus worse than lower ranked settlement", - ); - } - } - } -} - -fn best_surplus_by_order( - settlements: &[(Arc, RatedSettlement)], -) -> HashMap { - let mut best_surplus: HashMap = HashMap::new(); - for (solver, solution) in settlements.iter() { - let trades = solution.settlement.user_trades(); - let clearing_prices = get_prices(&solution.settlement); - for trade in trades { - let order_id = trade.order.metadata.uid; - let sell_token_price = &clearing_prices[&trade.order.data.sell_token]; - let buy_token_price = &clearing_prices[&trade.order.data.buy_token]; - let surplus = SurplusInfo { - solver_name: solver.name().to_string(), - ratio: trade - .surplus_ratio(sell_token_price, buy_token_price) - .unwrap_or_else(BigRational::zero), - }; - best_surplus - .entry(order_id) - .and_modify(|entry| { - if entry.ratio < surplus.ratio { - *entry = surplus.clone(); - } - }) - .or_insert(surplus); - } - } - best_surplus -} diff --git a/crates/solver/src/arguments.rs b/crates/solver/src/arguments.rs deleted file mode 100644 index 8b888be5d3..0000000000 --- a/crates/solver/src/arguments.rs +++ /dev/null @@ -1,554 +0,0 @@ -use { - crate::{ - liquidity::slippage, - s3_instance_upload_arguments::S3UploadArguments, - settlement_access_list::AccessListEstimatorType, - solver::{ - risk_computation, - single_order_solver, - ExternalSolverArg, - SolverAccountArg, - SolverType, - }, - }, - ethcontract::U256, - primitive_types::H160, - reqwest::Url, - shared::{ - arguments::{display_list, display_option}, - http_client, - }, - std::time::Duration, -}; - -#[derive(clap::Parser)] -pub struct Arguments { - #[clap(flatten)] - pub shared: shared::arguments::Arguments, - - #[clap(flatten)] - pub http_client: http_client::Arguments, - - #[clap(flatten)] - pub slippage: slippage::Arguments, - - #[clap(flatten)] - pub order_prioritization: single_order_solver::Arguments, - - /// The API endpoint to fetch the orderbook - #[clap(long, env, default_value = "http://localhost:8080")] - pub orderbook_url: Url, - - /// The API endpoint to call the Quasimodo solver - #[clap(long, env, default_value = "http://localhost:8000")] - pub quasimodo_solver_url: Url, - - /// The API endpoint for the Balancer SOR API for solving. - #[clap(long, env, default_value = "http://localhost:8000")] - pub balancer_sor_url: Url, - - /// The account used by the driver to sign transactions. This can be either - /// a 32-byte private key for offline signing, a 20-byte Ethereum address - /// for signing with a local node account, or a KMS key ID for signing with - /// AWS. - #[clap(long, env, hide_env_values = true)] - pub solver_account: Option, - - /// The target confirmation time in seconds for settlement transactions used - /// to estimate gas price. - #[clap( - long, - env, - default_value = "30s", - value_parser = humantime::parse_duration, - )] - pub target_confirm_time: Duration, - - /// Specify the interval between consecutive driver run loops. - /// - /// This is typically a low value to prevent busy looping in case of some - /// internal driver error, but can be set to a larger value for running - /// drivers in dry-run mode to prevent repeatedly settling the same - /// orders. - #[clap( - long, - env, - default_value = "10s", - value_parser = humantime::parse_duration, - )] - pub settle_interval: Duration, - - /// Which type of solver to use - #[clap( - long, - env, - default_values = &["Naive", "Baseline"], - value_enum, - ignore_case = true, - use_value_delimiter = true - )] - pub solvers: Vec, - - /// Individual accounts for each solver. See `--solver-account` for more - /// information about configuring accounts. - #[clap( - long, - env, - ignore_case = true, - use_value_delimiter = true, - hide_env_values = true - )] - pub solver_accounts: Option>, - - /// List of external solvers in the form of `name|url|account`. - #[clap(long, env, use_value_delimiter = true)] - pub external_solvers: Option>, - - /// The port at which we serve our metrics - #[clap(long, env, default_value = "9587")] - pub metrics_port: u16, - - /// The port at which we serve our metrics - #[clap(long, env, default_value = "5")] - pub max_merged_settlements: usize, - - /// The maximum amount of time in seconds a solver is allowed to take. - #[clap( - long, - env, - default_value = "30s", - value_parser = humantime::parse_duration, - )] - pub solver_time_limit: Duration, - - /// The URL of a list of tokens our settlement contract is willing to buy - /// when settling trades without external liquidity - #[clap(long, env)] - pub market_makable_token_list: Option, - - /// Like `market_makable_token_list` but hardcoded list of tokens. - #[clap(long, env, use_value_delimiter = true)] - pub market_makable_tokens: Option>, - - /// Time interval after which market makable list needs to be updated - #[clap( - long, - env, - default_value = "1h", - value_parser = humantime::parse_duration, - )] - pub market_makable_token_list_update_interval: Duration, - - /// The maximum gas price in Gwei the solver is willing to pay in a - /// settlement. - #[clap( - long, - env, - default_value = "1500", - value_parser = shared::arguments::wei_from_gwei - )] - pub gas_price_cap: f64, - - /// How to to submit settlement transactions. - /// Expected to contain either: - /// 1. One value equal to TransactionStrategyArg::DryRun or - /// 2. One or more values equal to any combination of enum variants except - /// TransactionStrategyArg::DryRun - #[clap( - long, - env, - default_value = "PublicMempool", - value_enum, - ignore_case = true, - use_value_delimiter = true - )] - pub transaction_strategy: Vec, - - /// Which access list estimators to use. Multiple estimators are used in - /// sequence if a previous one fails. Individual estimators might - /// support different networks. `Tenderly`: supports every network. - /// `Web3`: supports every network. - #[clap(long, env, value_enum, ignore_case = true, use_value_delimiter = true)] - pub access_list_estimators: Vec, - - /// The API endpoint of the Eden network for transaction submission. - #[clap(long, env, default_value = "https://api.edennetwork.io/v1/rpc")] - pub eden_api_url: Url, - - /// The API endpoint of the Flashbots network for transaction submission. - /// Multiple values could be defined for different Flashbots endpoints - /// (Flashbots Protect and Flashbots fast). - #[clap( - long, - env, - use_value_delimiter = true, - default_value = "https://rpc.flashbots.net" - )] - pub flashbots_api_url: Vec, - - /// Configures whether the submission logic is allowed to assume the - /// submission nodes implement soft cancellations. With soft cancellations a - /// cancellation transaction doesn't have to get mined to have an effect. On - /// arrival in the node all pending transactions with the same sender and - /// nonce will get discarded immediately. - #[clap(long, env, action = clap::ArgAction::Set, default_value = "false")] - pub use_soft_cancellations: bool, - - /// Maximum additional tip in gwei that we are willing to give to eden above - /// regular gas price estimation - #[clap( - long, - env, - default_value = "3", - value_parser = shared::arguments::wei_from_gwei - )] - pub max_additional_eden_tip: f64, - - /// The maximum time we spend trying to settle a transaction - /// through the ethereum network before going to back to solving. - #[clap( - long, - env, - default_value = "2m", - value_parser = humantime::parse_duration, - )] - pub max_submission_time: Duration, - - /// Maximum additional tip in gwei that we are willing to give to flashbots - /// above regular gas price estimation - #[clap( - long, - env, - default_value = "3", - value_parser = shared::arguments::wei_from_gwei - )] - pub max_additional_flashbot_tip: f64, - - /// Amount of time to wait before retrying to submit the tx to the ethereum - /// network - #[clap( - long, - env, - default_value = "2s", - value_parser = humantime::parse_duration, - )] - pub submission_retry_interval: Duration, - - /// Additional tip in percentage of max_fee_per_gas we are willing to give - /// to miners above regular gas price estimation - #[clap( - long, - env, - default_value = "0.05", - value_parser = shared::arguments::parse_percentage_factor - )] - pub additional_tip_percentage: f64, - - /// The RPC endpoints to use for submitting transaction to a custom set of - /// nodes. - #[clap(long, env, use_value_delimiter = true)] - pub transaction_submission_nodes: Vec, - - /// Additional RPC endpoints that we notify when we submit a transaction to - /// the network. These endpoints are usually third parties that seek to - /// be timely informed of a submission. These URLs are expected to - /// respond to valid RPC requests. however they are not expected to - /// be available nor we expect that transaction will eventually be mined. - #[clap(long, env, use_value_delimiter = true)] - pub transaction_notification_nodes: Vec, - - /// Don't submit high revert risk (i.e. transactions that interact with - /// on-chain AMMs) to the public mempool. This can be enabled to avoid - /// MEV when private transaction submission strategies are available. - #[clap(long, env)] - pub disable_high_risk_public_mempool_transactions: bool, - - /// The maximum number of settlements the driver considers per solver. - #[clap(long, env, default_value = "20")] - pub max_settlements_per_solver: usize, - - /// The smallest possible amount in Ether to consider for a partial order. - #[clap(long, env, default_value = "0.01", value_parser = shared::arguments::wei_from_ether)] - pub smallest_partial_fill: U256, - - /// Factor how much of the WETH buffer should be unwrapped if ETH buffer is - /// not big enough to settle ETH buy orders. - /// Unwrapping a bigger amount will cause fewer unwraps to happen and - /// thereby reduce the cost of unwraps per settled batch. - /// Only values in the range [0.0, 1.0] make sense. - #[clap(long, env, default_value = "0.6", value_parser = shared::arguments::parse_percentage_factor)] - pub weth_unwrap_factor: f64, - - /// Gas limit for simulations. This parameter is important to set correctly, - /// such that there are no simulation errors due to: err: insufficient - /// funds for gas * price + value, but at the same time we don't - /// restrict solutions sizes too much - #[clap(long, env, default_value = "15000000")] - pub simulation_gas_limit: u128, - - /// In order to protect against malicious solvers, the driver will check - /// that settlements prices do not exceed a max price deviation compared - /// to the external prices of the driver, if this optional value is set. - /// The max deviation value should be provided as a float percentage value. - /// E.g. for a max price deviation of 3%, one should set it to 0.03f64 - #[clap(long, env)] - pub max_settlement_price_deviation: Option, - - /// This variable allows to restrict the set of tokens for which a price - /// deviation check of settlement prices and external prices is - /// executed. If the value is not set, then all tokens included - /// in the settlement are checked for price deviation. - #[clap(long, env, use_value_delimiter = true)] - pub token_list_restriction_for_price_checks: Option>, - - #[clap(flatten)] - pub s3_upload: S3UploadArguments, - - /// Additional time to wait for a transaction to appear onchain before - /// considering the solution invalid and setting the reward to 0. - #[clap( - long, - env, - default_value = "1m", - value_parser = humantime::parse_duration, - )] - pub additional_mining_deadline: Duration, - - /// Parameters used to calculate the success/revert posibility of a - /// settlement. Currently used for gnosis solvers. - #[clap(flatten)] - pub risk_params: risk_computation::Arguments, - - /// Cap used for CIP20 score calculation. Defaults to 0.01 ETH. - #[clap(long, env, default_value = "0.01", value_parser = shared::arguments::wei_from_ether)] - pub score_cap: U256, - - /// Should we skip settlements with non-positive score for solver - /// competition? - #[clap(long, env, action = clap::ArgAction::Set, default_value = "true")] - pub skip_non_positive_score_settlements: bool, - - /// Flag to enable RFQ-T liquidity in the 0x solver. - #[clap(long, env, action = clap::ArgAction::Set, default_value = "false")] - pub zeroex_enable_rfqt: bool, - - /// Flag to enable slippage protection for the 0x solver. - #[clap(long, env, action = clap::ArgAction::Set, default_value = "false")] - pub zeroex_enable_slippage_protection: bool, - - #[clap(long, env, action = clap::ArgAction::Set, default_value = "true")] - pub process_partially_fillable_liquidity_orders: bool, - - #[clap(long, env, action = clap::ArgAction::Set, default_value = "true")] - pub process_partially_fillable_limit_orders: bool, - - /// Address of the ETH flow contract. If not specified, eth-flow orders are - /// disabled. - #[clap(long, env)] - pub ethflow_contract: Option, - - /// Controls whether we discard solutions without a fee for partially - /// filllable limit orders or set the fee to 0. This can make sense on - /// chains where we are not so concerned about the fee (e.g. gc, - /// goerli). - #[clap(long, env, action = clap::ArgAction::Set, default_value = "true")] - pub enforce_correct_fees_for_partially_fillable_limit_orders: bool, -} - -impl std::fmt::Display for Arguments { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { - shared, - http_client, - slippage, - order_prioritization, - orderbook_url, - quasimodo_solver_url, - balancer_sor_url, - solver_account, - target_confirm_time, - settle_interval, - solvers, - solver_accounts, - external_solvers, - metrics_port, - max_merged_settlements, - solver_time_limit, - market_makable_token_list, - market_makable_tokens, - gas_price_cap, - transaction_strategy, - access_list_estimators, - eden_api_url, - flashbots_api_url, - use_soft_cancellations, - max_additional_eden_tip, - max_submission_time, - max_additional_flashbot_tip, - submission_retry_interval, - additional_tip_percentage, - transaction_submission_nodes, - transaction_notification_nodes, - disable_high_risk_public_mempool_transactions, - max_settlements_per_solver, - weth_unwrap_factor, - simulation_gas_limit, - max_settlement_price_deviation, - token_list_restriction_for_price_checks, - s3_upload, - additional_mining_deadline, - risk_params, - score_cap, - skip_non_positive_score_settlements, - zeroex_enable_rfqt, - zeroex_enable_slippage_protection, - process_partially_fillable_liquidity_orders, - process_partially_fillable_limit_orders, - ethflow_contract, - enforce_correct_fees_for_partially_fillable_limit_orders, - market_makable_token_list_update_interval, - smallest_partial_fill, - } = self; - - write!(f, "{}", shared)?; - write!(f, "{}", http_client)?; - write!(f, "{}", slippage)?; - write!(f, "{}", order_prioritization)?; - writeln!(f, "orderbook_url: {}", orderbook_url)?; - writeln!(f, "quasimodo_solver_url: {}", quasimodo_solver_url)?; - writeln!(f, "balancer_sor_url: {}", balancer_sor_url)?; - display_option( - f, - "solver_account", - &solver_account - .as_ref() - .map(|account| format!("{account:?}")), - )?; - writeln!(f, "target_confirm_time: {:?}", target_confirm_time)?; - writeln!(f, "settle_interval: {:?}", settle_interval)?; - writeln!(f, "solvers: {:?}", solvers)?; - writeln!(f, "solver_accounts: {:?}", solver_accounts)?; - display_list( - f, - "external_solvers", - external_solvers - .iter() - .flatten() - .map(|solver| format!("{}|{}|{:?}", solver.name, solver.url, solver.account)), - )?; - writeln!(f, "metrics_port: {}", metrics_port)?; - writeln!(f, "max_merged_settlements: {}", max_merged_settlements)?; - writeln!(f, "solver_time_limit: {:?}", solver_time_limit)?; - display_option(f, "market_makable_token_list", market_makable_token_list)?; - display_option( - f, - "market_makable_tokens", - &market_makable_tokens - .as_ref() - .map(|list| format!("{list:?}")), - )?; - writeln!(f, "gas_price_cap: {}", gas_price_cap)?; - writeln!(f, "transaction_strategy: {:?}", transaction_strategy)?; - writeln!(f, "access_list_estimators: {:?}", &access_list_estimators)?; - writeln!(f, "eden_api_url: {}", eden_api_url)?; - display_list(f, "flashbots_api_url", flashbots_api_url)?; - writeln!(f, "use_soft_cancellations: {}", use_soft_cancellations)?; - writeln!(f, "max_additional_eden_tip: {}", max_additional_eden_tip)?; - writeln!(f, "max_submission_time: {:?}", max_submission_time)?; - writeln!( - f, - "max_additional_flashbots_tip: {}", - max_additional_flashbot_tip - )?; - writeln!( - f, - "submission_retry_interval: {:?}", - submission_retry_interval - )?; - writeln!( - f, - "additional_tip_percentage: {}%", - additional_tip_percentage - )?; - display_list( - f, - "transaction_submission_nodes", - transaction_submission_nodes, - )?; - display_list( - f, - "submission_notification_nodes", - transaction_notification_nodes, - )?; - writeln!( - f, - "disable_high_risk_public_mempool_transactions: {}", - disable_high_risk_public_mempool_transactions, - )?; - writeln!( - f, - "max_settlements_per_solver: {}", - max_settlements_per_solver - )?; - writeln!(f, "weth_unwrap_factor: {}", weth_unwrap_factor)?; - writeln!(f, "simulation_gas_limit: {}", simulation_gas_limit)?; - display_option( - f, - "max_settlement_price_deviation", - max_settlement_price_deviation, - )?; - writeln!( - f, - "token_list_restriction_for_price_checks: {:?}", - token_list_restriction_for_price_checks - )?; - writeln!(f, "{}", s3_upload)?; - writeln!( - f, - "additional_mining_deadline: {:?}", - additional_mining_deadline - )?; - writeln!(f, "{}", risk_params)?; - writeln!(f, "score_cap {}", score_cap)?; - writeln!(f, "{}", skip_non_positive_score_settlements)?; - writeln!(f, "zeroex_enable_rfqt: {}", zeroex_enable_rfqt)?; - writeln!( - f, - "zeroex_enable_slippage_protection: {}", - zeroex_enable_slippage_protection - )?; - writeln!( - f, - "process_partially_fillable_limit_orders: {:?}", - process_partially_fillable_limit_orders - )?; - writeln!( - f, - "process_partially_fillable_liquidity_orders: {:?}", - process_partially_fillable_liquidity_orders - )?; - display_option(f, "ethflow_contract", ethflow_contract)?; - writeln!( - f, - "enforce_correct_fees_for_partially_fillable_limit_orders: {:?}", - enforce_correct_fees_for_partially_fillable_limit_orders - )?; - writeln!( - f, - "market_makable_token_list_update_interval: {:?}", - market_makable_token_list_update_interval - )?; - writeln!(f, "smallest_partial_fill: {}", smallest_partial_fill)?; - - Ok(()) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, clap::ValueEnum)] -#[clap(rename_all = "verbatim")] -pub enum TransactionStrategyArg { - PublicMempool, - Eden, - Flashbots, - DryRun, -} diff --git a/crates/solver/src/auction_preprocessing.rs b/crates/solver/src/auction_preprocessing.rs deleted file mode 100644 index 6062cc0d0b..0000000000 --- a/crates/solver/src/auction_preprocessing.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Submodule containing helper methods to pre-process auction data before -//! passing it on to the solvers. - -use {model::order::Order, primitive_types::U256}; - -pub fn has_at_least_one_user_order(orders: &[Order]) -> bool { - orders - .iter() - .any(|order| !order.metadata.is_liquidity_order) -} - -/// Drops pre-interactions of all partially fillable orders that have -/// already been executed. We do this to ensure that each interaction gets only -/// executed once. -pub fn filter_executed_pre_interactions(orders: &mut [Order]) { - let zero = 0u32.into(); - - let was_partially_executed = |order: &Order| { - order.metadata.executed_buy_amount != zero - || order.metadata.executed_sell_amount != zero - || order.metadata.executed_sell_amount_before_fees != U256::zero() - || order.metadata.executed_fee_amount != U256::zero() - }; - - for order in orders { - if was_partially_executed(order) { - order.interactions.pre.clear(); - } - } -} diff --git a/crates/solver/src/driver.rs b/crates/solver/src/driver.rs deleted file mode 100644 index 150111b408..0000000000 --- a/crates/solver/src/driver.rs +++ /dev/null @@ -1,449 +0,0 @@ -use { - crate::{ - auction_preprocessing, - driver_logger::DriverLogger, - in_flight_orders::InFlightOrders, - liquidity_collector::{LiquidityCollecting, LiquidityCollector}, - metrics::SolverMetrics, - order_balance_filter, - orderbook::OrderBookApi, - settlement::{PriceCheckTokens, Settlement}, - settlement_ranker::SettlementRanker, - settlement_rater::SettlementRating, - settlement_submission::{SolutionSubmitter, SubmissionError}, - solver::{Auction, Solver, Solvers}, - }, - anyhow::{Context, Result}, - contracts::GPv2Settlement, - ethcontract::Account, - ethrpc::{current_block::CurrentBlockStream, Web3}, - futures::future::join_all, - gas_estimation::GasPriceEstimating, - model::{ - auction::{AuctionId, AuctionWithId}, - order::{Order, OrderUid}, - TokenPair, - }, - num::{rational::Ratio, BigInt}, - primitive_types::{H160, U256}, - shared::{ - account_balances::BalanceFetching, - external_prices::ExternalPrices, - http_solver::model::{AuctionResult, SolverRunError, SubmissionResult}, - recent_block_cache::Block, - tenderly_api::TenderlyApi, - }, - std::{ - collections::HashSet, - fmt::Write, - sync::Arc, - time::{Duration, Instant}, - }, - tracing::{info_span, Instrument as _}, - web3::types::TransactionReceipt, -}; - -pub mod gas; -pub mod solver_settlements; - -pub struct Driver { - liquidity_collector: LiquidityCollector, - solvers: Solvers, - gas_price_estimator: gas::Estimator, - settle_interval: Duration, - native_token: H160, - metrics: Arc, - solver_time_limit: Duration, - block_stream: CurrentBlockStream, - solution_submitter: SolutionSubmitter, - run_id: u64, - api: OrderBookApi, - in_flight_orders: InFlightOrders, - settlement_ranker: SettlementRanker, - logger: DriverLogger, - web3: Web3, - last_attempted_settlement: Option, - process_partially_fillable_liquidity_orders: bool, - process_partially_fillable_limit_orders: bool, - balance_fetcher: Arc, - previous_auction_orders: HashSet, -} -impl Driver { - #[allow(clippy::too_many_arguments)] - pub fn new( - settlement_contract: GPv2Settlement, - liquidity_collector: LiquidityCollector, - solvers: Solvers, - gas_price_estimator: Arc, - gas_price_cap: f64, - settle_interval: Duration, - native_token: H160, - metrics: Arc, - web3: Web3, - network_id: String, - solver_time_limit: Duration, - skip_non_positive_score_settlements: bool, - block_stream: CurrentBlockStream, - solution_submitter: SolutionSubmitter, - api: OrderBookApi, - simulation_gas_limit: u128, - max_settlement_price_deviation: Option>, - token_list_restriction_for_price_checks: PriceCheckTokens, - tenderly: Option>, - process_partially_fillable_liquidity_orders: bool, - process_partially_fillable_limit_orders: bool, - settlement_rater: Arc, - balance_fetcher: Arc, - ) -> Self { - let gas_price_estimator = - gas::Estimator::new(gas_price_estimator).with_gas_price_cap(gas_price_cap); - - let settlement_ranker = SettlementRanker { - max_settlement_price_deviation, - token_list_restriction_for_price_checks, - metrics: metrics.clone(), - settlement_rater, - skip_non_positive_score_settlements, - }; - - let logger = DriverLogger { - metrics: metrics.clone(), - web3: web3.clone(), - tenderly, - network_id, - settlement_contract, - simulation_gas_limit, - }; - - Self { - liquidity_collector, - solvers, - gas_price_estimator, - settle_interval, - native_token, - metrics, - solver_time_limit, - block_stream, - solution_submitter, - run_id: 0, - api, - in_flight_orders: InFlightOrders::default(), - settlement_ranker, - logger, - web3, - last_attempted_settlement: None, - process_partially_fillable_liquidity_orders, - process_partially_fillable_limit_orders, - balance_fetcher, - previous_auction_orders: Default::default(), - } - } - - pub async fn run_forever(&mut self) -> ! { - loop { - let start = Instant::now(); - match self.single_run().await { - Ok(()) => tracing::debug!("single run finished ok"), - Err(err) => tracing::error!("single run errored: {:?}", err), - } - self.metrics.runloop_completed(); - tokio::time::sleep_until((start + self.settle_interval).into()).await; - } - } - - // Returns solver name and result. - async fn run_solvers( - &self, - auction: Auction, - ) -> Vec<(Arc, Result, SolverRunError>)> { - join_all(self.solvers.iter().map(|solver| { - let auction = auction.clone(); - let metrics = &self.metrics; - async move { - let start_time = Instant::now(); - let span = info_span!("solver", solver = solver.name()); - let result = - match tokio::time::timeout_at(auction.deadline.into(), solver.solve(auction)) - .instrument(span) - .await - { - Ok(inner) => { - inner.map_err(|err| SolverRunError::Solving(format!("{err:?}"))) - } - Err(_timeout) => Err(SolverRunError::Timeout), - }; - let response = match &result { - Err(SolverRunError::Timeout) => "timeout", - Err(_) => "error", - Ok(solutions) if solutions.is_empty() => "none", - Ok(_) => "solution", - }; - metrics.settlement_computed(solver.name(), response, start_time); - (solver.clone(), result) - } - })) - .await - } - - pub async fn single_run(&mut self) -> Result<()> { - let auction = self - .api - .get_auction() - .await - .context("error retrieving current auction")?; - - // It doesn't make sense to solve the same auction again because we wouldn't be - // able to store competition info etc. - if self.last_attempted_settlement == Some(auction.id) { - tracing::debug!("skipping run because auction hasn't changed {}", auction.id); - return Ok(()); - } - - let id = auction.id; - let run = self.next_run_id(); - - // extra function so that we can add span information - let settlement_attempted = self - .single_auction(auction, run) - .instrument(tracing::info_span!("auction", id, run)) - .await?; - - if settlement_attempted { - self.last_attempted_settlement = Some(id); - } - - Ok(()) - } - - fn observe_auction_orders(&mut self, orders: &[Order]) { - let orders: HashSet = orders.iter().map(|order| order.metadata.uid).collect(); - let mut msg = String::new(); - for order in orders.difference(&self.previous_auction_orders) { - writeln!(&mut msg, "{order}").unwrap(); - } - tracing::debug!("orders that started showing up in this auction:\n{msg}"); - msg.clear(); - for order in self.previous_auction_orders.difference(&orders) { - writeln!(&mut msg, "{order}").unwrap(); - } - tracing::debug!("orders that stopped showing up in this auction:\n{msg}"); - self.previous_auction_orders = orders; - } - - /// Returns whether a settlement transaction was attempted. - async fn single_auction(&mut self, auction: AuctionWithId, run_id: u64) -> Result { - let start = Instant::now(); - tracing::debug!("starting single run"); - - let auction_id = auction.id; - let mut auction = auction.auction; - - self.observe_auction_orders(&auction.orders); - - let current_block_during_liquidity_fetch = self.block_stream.borrow().number; - - self.in_flight_orders.update_and_filter(&mut auction); - - auction.orders.retain(|order| { - match ( - order.data.partially_fillable, - order.metadata.is_liquidity_order, - ) { - (false, _) => true, - (true, true) => self.process_partially_fillable_liquidity_orders, - (true, false) => self.process_partially_fillable_limit_orders, - } - }); - - let balance_start = Instant::now(); - let balances = - order_balance_filter::fetch_balances(self.balance_fetcher.as_ref(), &auction.orders) - .await; - tracing::debug!("fetching order balances took {:?}", balance_start.elapsed()); - - tracing::info!(count =% auction.orders.len(), "got orders"); - self.metrics.orders_fetched(&auction.orders); - - let external_prices = - ExternalPrices::try_from_auction_prices(self.native_token, auction.prices) - .context("malformed auction prices")?; - tracing::debug!(?external_prices, "estimated prices"); - - if !auction_preprocessing::has_at_least_one_user_order(&auction.orders) { - return Ok(false); - } - - auction_preprocessing::filter_executed_pre_interactions(&mut auction.orders); - - let gas_price = self - .gas_price_estimator - .estimate() - .await - .context("failed to estimate gas price")?; - tracing::debug!(%gas_price, "solving with gas price"); - - let pairs: HashSet<_> = auction - .orders - .iter() - .filter(|o| !o.metadata.is_liquidity_order) - .flat_map(|o| TokenPair::new(o.data.buy_token, o.data.sell_token)) - .collect(); - let liquidity_start = Instant::now(); - let liquidity = self - .liquidity_collector - .get_liquidity(pairs, Block::Number(current_block_during_liquidity_fetch)) - .await?; - tracing::debug!("collecting liquidity took {:?}", liquidity_start.elapsed()); - self.metrics.liquidity_fetched(&liquidity); - - let auction = Auction { - id: auction_id, - run: run_id, - orders: auction.orders, - liquidity, - liquidity_fetch_block: current_block_during_liquidity_fetch, - gas_price: gas_price.effective_gas_price(), - deadline: Instant::now() + self.solver_time_limit, - external_prices: external_prices.clone(), - balances, - }; - - tracing::debug!(deadline =? auction.deadline, "solving auction"); - let run_solver_results = self.run_solvers(auction).await; - let (mut rated_settlements, errors) = self - .settlement_ranker - .rank_legal_settlements(run_solver_results, &external_prices, gas_price, auction_id) - .await?; - - DriverLogger::print_settlements(&rated_settlements); - - let mut settlement_transaction_attempted = false; - if let Some((winning_solver, winning_settlement)) = rated_settlements.pop() { - tracing::info!( - "winning settlement id {} by solver {}: {:?}", - winning_settlement.id, - winning_solver.name(), - winning_settlement - ); - - let account = winning_solver.account(); - let address = account.address(); - let nonce = self - .web3 - .eth() - .transaction_count(address, None) - .await - .context("transaction_count")?; - - self.metrics - .complete_runloop_until_transaction(start.elapsed()); - tracing::debug!(?address, ?nonce, "submitting settlement"); - settlement_transaction_attempted = true; - let hash = match submit_settlement( - &self.solution_submitter, - &self.logger, - account.clone(), - nonce, - winning_solver.name(), - winning_settlement.settlement.clone(), - winning_settlement.gas_estimate, - gas_price.max_fee_per_gas, - Some(winning_settlement.id as u64), - ) - .await - { - Ok(receipt) => { - self.update_in_flight_orders(&receipt, &winning_settlement.settlement); - winning_solver.notify_auction_result( - auction_id, - AuctionResult::SubmittedOnchain(SubmissionResult::Success( - receipt.transaction_hash, - )), - ); - Some(receipt.transaction_hash) - } - Err(SubmissionError::Revert(hash)) => { - winning_solver.notify_auction_result( - auction_id, - AuctionResult::SubmittedOnchain(SubmissionResult::Revert(hash)), - ); - Some(hash) - } - Err(err) => { - winning_solver.notify_auction_result( - auction_id, - AuctionResult::SubmittedOnchain(SubmissionResult::Fail), - ); - tracing::warn!(?err, "settlement submission error"); - None - } - }; - if let Some(hash) = hash { - tracing::debug!(?hash, "settled transaction"); - } - - self.logger - .report_on_batch(&(winning_solver, winning_settlement), rated_settlements); - } - // Happens after settlement submission so that we do not delay it. - self.logger.report_simulation_errors( - errors, - current_block_during_liquidity_fetch, - gas_price, - ); - Ok(settlement_transaction_attempted) - } - - /// Marks all orders in the winning settlement as "in flight". - fn update_in_flight_orders(&mut self, receipt: &TransactionReceipt, settlement: &Settlement) { - let block = match receipt.block_number { - Some(block) => block.as_u64(), - None => { - tracing::error!("tx receipt does not contain block number"); - 0 - } - }; - self.in_flight_orders.mark_settled_orders(block, settlement); - } - - fn next_run_id(&mut self) -> u64 { - let id = self.run_id; - self.run_id += 1; - id - } -} - -/// Submits the winning solution and handles the related logging and metrics. -#[allow(clippy::too_many_arguments)] -pub async fn submit_settlement( - solution_submitter: &SolutionSubmitter, - logger: &DriverLogger, - account: Account, - nonce: U256, - solver_name: &str, - settlement: Settlement, - gas_estimate: U256, - max_fee_per_gas: f64, - settlement_id: Option, -) -> Result { - let start = Instant::now(); - let result = solution_submitter - .settle( - settlement.clone(), - gas_estimate, - max_fee_per_gas, - account, - nonce, - ) - .await; - logger - .log_submission_info( - &result, - &settlement, - settlement_id, - solver_name, - start.elapsed(), - ) - .await; - result.map(Into::into) -} diff --git a/crates/solver/src/driver/gas.rs b/crates/solver/src/driver/gas.rs deleted file mode 100644 index 68b1a42641..0000000000 --- a/crates/solver/src/driver/gas.rs +++ /dev/null @@ -1,177 +0,0 @@ -//! Gas price estimation used for settlement submission. - -use { - anyhow::{ensure, Result}, - gas_estimation::{GasPrice1559, GasPriceEstimating}, - std::{sync::Arc, time::Duration}, -}; - -pub struct Estimator { - inner: Arc, - gas_price_cap: f64, -} - -impl Estimator { - /// Creates a new gas price estimator for the driver. - /// - /// This estimator computes a EIP-1159 gas price with an upfront maximum - /// `max_fee_per_gas` that is allowed for any given run. This allows - /// settlement simulation to know upfront the actual minimum balance that - /// would be required by a solver account (since, as per EIP-1559, an - /// account needs at least `max_fee_per_gas * gas_limit` for a transaction - /// to be valid, regardless of the `effective_gas_price` the transaction is - /// executed with). This way, we: - /// - Don't need to over-estimate a `max_fee_per_gas` value to account for - /// gas spikes, meaning we won't disregard solvers with lower, but - /// sufficient balances - /// - Will only ever chose a solver IFF it will have enough balance to - /// execute a settlement up until `max_fee_per_gas`, preventing settlement - /// submissions being stopped part-way through because of insufficient - /// balance for executing a transaction - pub fn new(inner: Arc) -> Self { - Self { - inner, - gas_price_cap: f64::INFINITY, - } - } - - /// Sets the gas price cap for the estimator. - pub fn with_gas_price_cap(mut self, gas_price_cap: f64) -> Self { - self.gas_price_cap = gas_price_cap; - self - } -} - -#[async_trait::async_trait] -impl GasPriceEstimating for Estimator { - async fn estimate_with_limits( - &self, - gas_limit: f64, - time_limit: Duration, - ) -> Result { - let mut estimate = self - .inner - .estimate_with_limits(gas_limit, time_limit) - .await?; - - estimate.max_fee_per_gas = (estimate.base_fee_per_gas * MAX_FEE_FACTOR) - .max(estimate.base_fee_per_gas + estimate.max_priority_fee_per_gas) - .min(self.gas_price_cap); - estimate.max_priority_fee_per_gas = estimate - .max_priority_fee_per_gas - .min(estimate.max_fee_per_gas); - estimate = estimate.ceil(); - - ensure!(estimate.is_valid(), "invalid gas estimate {estimate}"); - Ok(estimate) - } -} - -/// The factor of `base_fee_per_gas` to use for the `max_fee_per_gas` for gas -/// price estimates. This is chosen to be the maximum increase in the -/// `base_fee_per_gas` possible over a period of 12 blocks (which roughly -/// corresponds to the deadline a solver has for mining a transaction on -/// Mainnet + solvers solving time). -const MAX_FEE_FACTOR: f64 = 4.2; - -#[cfg(test)] -mod tests { - use {super::*, shared::gas_price_estimation::FakeGasPriceEstimator}; - - #[tokio::test] - async fn scales_max_gas_price() { - let estimator = Estimator::new(Arc::new(FakeGasPriceEstimator::new(GasPrice1559 { - base_fee_per_gas: 10., - max_fee_per_gas: 20., - max_priority_fee_per_gas: 1., - }))); - - assert_eq!( - estimator - .estimate_with_limits(Default::default(), Default::default()) - .await - .unwrap(), - GasPrice1559 { - base_fee_per_gas: 10., - max_fee_per_gas: 42., - max_priority_fee_per_gas: 1., - } - ); - } - - #[tokio::test] - async fn respects_max_priority_fee() { - let estimator = Estimator::new(Arc::new(FakeGasPriceEstimator::new(GasPrice1559 { - base_fee_per_gas: 1., - max_fee_per_gas: 200., - max_priority_fee_per_gas: 99., - }))); - - assert_eq!( - estimator - .estimate_with_limits(Default::default(), Default::default()) - .await - .unwrap(), - GasPrice1559 { - base_fee_per_gas: 1., - max_fee_per_gas: 100., - max_priority_fee_per_gas: 99., - } - ); - - let estimator = estimator.with_gas_price_cap(50.); - - assert_eq!( - estimator - .estimate_with_limits(Default::default(), Default::default()) - .await - .unwrap(), - GasPrice1559 { - base_fee_per_gas: 1., - max_fee_per_gas: 50., - max_priority_fee_per_gas: 50., - } - ); - } - - #[tokio::test] - async fn capped_gas_price() { - let estimator = Estimator::new(Arc::new(FakeGasPriceEstimator::new(GasPrice1559 { - base_fee_per_gas: 100., - max_fee_per_gas: 200., - max_priority_fee_per_gas: 10., - }))) - .with_gas_price_cap(250.); - - assert_eq!( - estimator - .estimate_with_limits(Default::default(), Default::default()) - .await - .unwrap(), - GasPrice1559 { - base_fee_per_gas: 100., - max_fee_per_gas: 250., - max_priority_fee_per_gas: 10., - } - ); - - let estimator = estimator.with_gas_price_cap(150.); - assert_eq!( - estimator - .estimate_with_limits(Default::default(), Default::default()) - .await - .unwrap(), - GasPrice1559 { - base_fee_per_gas: 100., - max_fee_per_gas: 150., - max_priority_fee_per_gas: 10., - } - ); - - let estimator = estimator.with_gas_price_cap(99.); - assert!(estimator - .estimate_with_limits(Default::default(), Default::default()) - .await - .is_err()); - } -} diff --git a/crates/solver/src/driver/solver_settlements.rs b/crates/solver/src/driver/solver_settlements.rs deleted file mode 100644 index 6ef5f5d4e6..0000000000 --- a/crates/solver/src/driver/solver_settlements.rs +++ /dev/null @@ -1,86 +0,0 @@ -use { - crate::settlement::Settlement, - model::solver_competition::Score, - num::BigRational, - primitive_types::U256, -}; - -pub fn has_user_order(settlement: &Settlement) -> bool { - settlement.user_trades().next().is_some() -} - -// Each individual settlement has an objective value. -#[derive(Debug, Default, Clone)] -pub struct RatedSettlement { - // Identifies a settlement during a run loop. - pub id: usize, - pub settlement: Settlement, - pub surplus: BigRational, // In wei. - pub earned_fees: BigRational, // In wei. - pub solver_fees: BigRational, // In wei. - pub gas_estimate: U256, // In gas units. - pub gas_price: BigRational, // In wei per gas unit. - pub objective_value: BigRational, - pub score: Score, // auction based score. - pub ranking: usize, // auction based ranking. -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::settlement::Trade, - chrono::{offset::Utc, DateTime}, - model::order::{Order, OrderClass, OrderData, OrderMetadata, OrderUid}, - }; - - fn trade(created_at: DateTime, uid: u8, class: OrderClass) -> Trade { - Trade { - order: Order { - data: OrderData { - sell_amount: 1.into(), - buy_amount: 1.into(), - ..Default::default() - }, - metadata: OrderMetadata { - creation_date: created_at, - uid: OrderUid([uid; 56]), - class, - ..Default::default() - }, - ..Default::default() - }, - executed_amount: 1.into(), - ..Default::default() - } - } - - #[test] - fn has_user_order_() { - let order = |class| trade(Default::default(), 0, class); - - let settlement = Settlement::with_default_prices(vec![]); - assert!(!has_user_order(&settlement)); - - let settlement = Settlement::with_default_prices(vec![order(OrderClass::Limit)]); - assert!(has_user_order(&settlement)); - - let settlement = Settlement::with_default_prices(vec![order(OrderClass::Liquidity)]); - assert!(!has_user_order(&settlement)); - - let settlement = Settlement::with_default_prices(vec![order(OrderClass::Market)]); - assert!(has_user_order(&settlement)); - - let settlement = Settlement::with_default_prices(vec![ - order(OrderClass::Market), - order(OrderClass::Liquidity), - ]); - assert!(has_user_order(&settlement)); - - let settlement = Settlement::with_default_prices(vec![ - order(OrderClass::Liquidity), - order(OrderClass::Limit), - ]); - assert!(has_user_order(&settlement)); - } -} diff --git a/crates/solver/src/driver_logger.rs b/crates/solver/src/driver_logger.rs deleted file mode 100644 index 36c6666b5c..0000000000 --- a/crates/solver/src/driver_logger.rs +++ /dev/null @@ -1,318 +0,0 @@ -use { - crate::{ - analytics, - driver::solver_settlements::RatedSettlement, - metrics::{SolverMetrics, SolverSimulationOutcome}, - settlement::Settlement, - settlement_simulation::{ - simulate_and_error_with_tenderly_link, - simulate_before_after_access_list, - }, - settlement_submission::{SubmissionError, SubmissionReceipt}, - solver::{Simulation, SimulationWithError, Solver}, - }, - anyhow::{Context, Result}, - contracts::GPv2Settlement, - gas_estimation::GasPrice1559, - itertools::Itertools, - model::order::{Order, OrderKind}, - num::ToPrimitive, - primitive_types::H256, - shared::{ethrpc::Web3, tenderly_api::TenderlyApi}, - std::{sync::Arc, time::Duration}, - tracing::{Instrument as _, Span}, -}; - -pub struct DriverLogger { - pub metrics: Arc, - pub web3: Web3, - pub tenderly: Option>, - pub network_id: String, - pub settlement_contract: GPv2Settlement, - pub simulation_gas_limit: u128, -} - -impl DriverLogger { - pub async fn metric_access_list_gas_saved(&self, transaction_hash: H256) -> Result<()> { - let gas_saved = simulate_before_after_access_list( - &self.web3, - self.tenderly.as_deref().context("tenderly disabled")?, - self.network_id.clone(), - transaction_hash, - ) - .await?; - tracing::debug!(?gas_saved, "access list gas saved"); - if gas_saved.is_sign_positive() { - self.metrics - .settlement_access_list_saved_gas(gas_saved, "positive"); - } else { - self.metrics - .settlement_access_list_saved_gas(-gas_saved, "negative"); - } - - Ok(()) - } - - /// Collects all orders which got traded in the settlement. Tapping into - /// partially fillable orders multiple times will not result in - /// duplicates. Partially fillable orders get considered as traded only - /// the first time we tap into their liquidity. - fn get_traded_orders(settlement: &Settlement) -> Vec { - let mut traded_orders = Vec::new(); - for (_, group) in &settlement - .trades() - .group_by(|trade| trade.order.metadata.uid) - { - let mut group = group.into_iter().peekable(); - let order = &group.peek().unwrap().order; - let was_already_filled = match order.data.kind { - OrderKind::Buy => &order.metadata.executed_buy_amount, - OrderKind::Sell => &order.metadata.executed_sell_amount, - } > &0u8.into(); - let is_getting_filled = group.any(|trade| !trade.executed_amount.is_zero()); - if !was_already_filled && is_getting_filled { - traded_orders.push(order.clone()); - } - } - traded_orders - } - - pub async fn log_submission_info( - &self, - submission: &Result, - settlement: &Settlement, - settlement_id: Option, - solver_name: &str, - elapsed_time: Duration, - ) { - self.metrics - .settlement_revertable_status(settlement.revertable(), solver_name); - self.metrics.transaction_submission( - elapsed_time, - submission - .as_ref() - .map(|x| x.strategy) - .unwrap_or("all_failed"), - ); - match submission { - Ok(receipt) => { - tracing::info!( - settlement_id, - transaction_hash =? receipt.tx.transaction_hash, - "Successfully submitted settlement", - ); - Self::get_traded_orders(settlement) - .iter() - .for_each(|order| self.metrics.order_settled(order, solver_name)); - self.metrics.settlement_submitted( - crate::metrics::SettlementSubmissionOutcome::Success, - solver_name, - ); - if let Err(err) = self - .metric_access_list_gas_saved(receipt.tx.transaction_hash) - .await - { - tracing::debug!(?err, "access list metric not saved"); - } - match receipt.tx.effective_gas_price { - Some(price) => { - self.metrics.transaction_gas_price(price); - } - None => { - tracing::error!("node did not return effective gas price in tx receipt"); - } - } - } - Err(err) => { - // Since we simulate and only submit solutions when they used to pass before, - // there is no point in logging transaction failures in the form - // of race conditions as hard errors. - tracing::warn!(settlement_id, ?err, "Failed to submit settlement",); - self.metrics - .settlement_submitted(err.as_outcome(), solver_name); - if let Some(transaction_hash) = err.revert_transaction_hash() { - if let Err(err) = self.metric_access_list_gas_saved(transaction_hash).await { - tracing::debug!(?err, "access list metric not saved"); - } - } - } - } - } - - // Log simulation errors only if the simulation also fails in the block at which - // on chain liquidity was queried. If the simulation succeeds at the - // previous block then the solver worked correctly and the error doesn't - // have to be reported. Note that we could still report a false positive - // because the earlier block might be off by if the block has changed just - // as were were querying the node. - pub fn report_simulation_errors( - &self, - errors: Vec, - current_block_during_liquidity_fetch: u64, - gas_price: GasPrice1559, - ) { - let contract = self.settlement_contract.clone(); - let web3 = self.web3.clone(); - let network_id = self.network_id.clone(); - let metrics = self.metrics.clone(); - let simulation_gas_limit = self.simulation_gas_limit; - let task = async move { - let simulations = simulate_and_error_with_tenderly_link( - errors.iter().map(|simulation_with_error| { - let simulation = &simulation_with_error.simulation; - let settlement = simulation - .settlement - .clone() - .encode(simulation.transaction.internalization); - ( - simulation.solver.account.clone(), - settlement, - simulation.transaction.access_list.clone(), - ) - }), - &contract, - &web3, - gas_price, - &network_id, - current_block_during_liquidity_fetch, - simulation_gas_limit, - ) - .await; - - for ( - SimulationWithError { - simulation: - Simulation { - solver, settlement, .. - }, - error: error_at_latest_block, - }, - result, - ) in errors.iter().zip(simulations) - { - metrics - .settlement_simulation(&solver.name, SolverSimulationOutcome::FailureOnLatest); - if let Err(error_at_earlier_block) = result { - tracing::warn!( - "{} settlement simulation failed at submission and block {}:\n{:?}", - solver.name, - current_block_during_liquidity_fetch, - error_at_earlier_block, - ); - // split warning into separate logs so that the messages aren't too long. - tracing::warn!( - "{} settlement failure for: \n{:#?}", - solver.name, - settlement, - ); - - metrics.settlement_simulation(&solver.name, SolverSimulationOutcome::Failure); - } else { - tracing::debug!( - name = solver.name, - ?error_at_latest_block, - "simulation only failed on the latest block but not on the block the \ - auction started", - ); - } - } - }; - tokio::task::spawn(task.instrument(Span::current())); - } - - pub fn print_settlements(rated_settlements: &[(Arc, RatedSettlement)]) { - let mut text = String::new(); - for (solver, settlement) in rated_settlements { - use std::fmt::Write; - write!( - text, - "\nid={} solver={} objective={:.2e} score={:.2e} surplus={:.2e} \ - gas_estimate={:.2e} gas_price={:.2e} solver_fees={:.2e} earned_fees={:.2e}", - settlement.id, - solver.name(), - settlement.objective_value.to_f64().unwrap_or(f64::NAN), - settlement.score.score().to_f64_lossy(), - settlement.surplus.to_f64().unwrap_or(f64::NAN), - settlement.gas_estimate.to_f64_lossy(), - settlement.gas_price.to_f64().unwrap_or(f64::NAN), - &settlement.solver_fees.to_f64().unwrap_or(f64::NAN), - settlement.earned_fees.to_f64().unwrap_or(f64::NAN), - ) - .unwrap(); - } - tracing::info!("Rated Settlements: {}", text); - } - - /// Record metrics on the matched orders from a single batch. Specifically - /// we report on the number of orders that were; - /// - surplus in winning settlement vs unrealized surplus from other - /// feasible solutions. - /// - matched but not settled in this runloop (effectively queued for the - /// next one) - /// Should help us to identify how much we can save by parallelizing - /// execution. - pub fn report_on_batch( - &self, - submitted: &(Arc, RatedSettlement), - other_settlements: Vec<(Arc, RatedSettlement)>, - ) { - // Report surplus - analytics::report_alternative_settlement_surplus( - &*self.metrics, - submitted, - &other_settlements, - ); - // Report matched but not settled - analytics::report_matched_but_not_settled(&*self.metrics, submitted, &other_settlements); - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::solver::dummy_arc_solver, - model::solver_competition::Score, - num::BigRational, - }; - - #[test] - #[ignore] - fn print_settlements() { - let a = [ - ( - dummy_arc_solver(), - RatedSettlement { - id: 0, - settlement: Default::default(), - surplus: BigRational::new(1u8.into(), 1u8.into()), - earned_fees: BigRational::new(2u8.into(), 1u8.into()), - solver_fees: BigRational::new(3u8.into(), 1u8.into()), - gas_estimate: 4.into(), - gas_price: BigRational::new(5u8.into(), 1u8.into()), - objective_value: BigRational::new(6u8.into(), 1u8.into()), - score: Score::Solver(6.into()), - ranking: 1, - }, - ), - ( - dummy_arc_solver(), - RatedSettlement { - id: 6, - settlement: Default::default(), - surplus: BigRational::new(7u8.into(), 1u8.into()), - earned_fees: BigRational::new(8u8.into(), 1u8.into()), - solver_fees: BigRational::new(9u8.into(), 1u8.into()), - gas_estimate: 10.into(), - gas_price: BigRational::new(11u8.into(), 1u8.into()), - objective_value: BigRational::new(12u8.into(), 1u8.into()), - score: Score::Solver(12.into()), - ranking: 2, - }, - ), - ]; - - observe::tracing::initialize_reentrant("INFO"); - DriverLogger::print_settlements(&a); - } -} diff --git a/crates/solver/src/in_flight_orders.rs b/crates/solver/src/in_flight_orders.rs deleted file mode 100644 index 94ee8aed31..0000000000 --- a/crates/solver/src/in_flight_orders.rs +++ /dev/null @@ -1,328 +0,0 @@ -use { - crate::settlement::{Settlement, TradeExecution}, - itertools::Itertools, - model::{ - auction::Auction, - order::{Order, OrderKind, OrderUid}, - }, - number::conversions::u256_to_big_uint, - std::collections::{BTreeMap, HashMap, HashSet}, -}; - -#[derive(Debug, Clone)] -struct PartiallyFilledOrder { - order: Order, - in_flight_trades: Vec, -} - -impl PartiallyFilledOrder { - pub fn order_with_remaining_amounts(&self) -> Order { - let mut updated_order = self.order.clone(); - - for trade in &self.in_flight_trades { - updated_order.metadata.executed_buy_amount += u256_to_big_uint(&trade.buy_amount); - updated_order.metadata.executed_sell_amount += - u256_to_big_uint(&(trade.sell_amount + trade.fee_amount)); - updated_order.metadata.executed_sell_amount_before_fees += trade.sell_amount; - updated_order.metadata.executed_fee_amount += trade.fee_amount; - } - - updated_order - } -} - -/// After a settlement transaction we need to keep track of in flight orders -/// until the api has seen the tx. Otherwise we would attempt to solve already -/// matched orders again leading to failures. -#[derive(Default)] -pub struct InFlightOrders { - /// Maps block to orders settled in that block. - in_flight: BTreeMap>, - /// Tracks in flight trades which use liquidity from partially fillable - /// orders. - in_flight_trades: HashMap, -} - -impl InFlightOrders { - /// Takes note of the new set of solvable orders and returns the ones that - /// aren't in flight and scales down partially fillable orders if there - /// are currently orders in-flight tapping into their executable - /// amounts. Returns the set of order uids that are considered in - /// flight. - pub fn update_and_filter(&mut self, auction: &mut Auction) -> HashSet { - let uids = |in_flight: &BTreeMap>| { - in_flight - .values() - .flatten() - .copied() - .collect::>() - }; - let inflight_before = uids(&self.in_flight); - let orders_before = auction.orders.len(); - - // If api has seen block X then trades starting at X + 1 are still in flight. - self.in_flight = self - .in_flight - .split_off(&(auction.latest_settlement_block + 1)); - - let in_flight = uids(&self.in_flight); - self.in_flight_trades - .retain(|uid, _| in_flight.contains(uid)); - - auction.orders.iter_mut().for_each(|order| { - let uid = &order.metadata.uid; - - if order.data.partially_fillable { - if let Some(trades) = self.in_flight_trades.get(uid) { - *order = trades.order_with_remaining_amounts(); - } - } else if in_flight.contains(uid) { - // fill-or-kill orders can only be used once and there is already a trade in - // flight for this one => Modify it such that it gets filtered - // out in the next step. - order.metadata.executed_buy_amount = u256_to_big_uint(&order.data.buy_amount); - order.metadata.executed_sell_amount_before_fees = order.data.sell_amount; - } - }); - auction.orders.retain(|order| match order.data.kind { - OrderKind::Sell => { - u256_to_big_uint(&order.data.sell_amount) - > u256_to_big_uint(&order.metadata.executed_sell_amount_before_fees) - } - OrderKind::Buy => { - u256_to_big_uint(&order.data.buy_amount) > order.metadata.executed_buy_amount - } - }); - - tracing::trace!( - auction_block = %auction.block, - latest_settlement_block = %auction.latest_settlement_block, - inflight_before_count = %inflight_before.len(), - inflight_after_count = %in_flight.len(), - orders_before_count = %orders_before, - orders_after_count = %auction.orders.len(), - inflight_before = ?inflight_before, - inflight_after = ?in_flight, - "inflight stats" - ); - - in_flight - } - - /// Tracks all in_flight orders and how much of the executable amount of - /// partially fillable orders is currently used in in-flight trades. - pub fn mark_settled_orders(&mut self, block: u64, settlement: &Settlement) { - let uids = settlement.traded_orders().map(|order| order.metadata.uid); - self.in_flight.entry(block).or_default().extend(uids); - - settlement - .trades() - .zip(settlement.trade_executions()) - .filter(|(trade, _)| trade.order.data.partially_fillable) - .into_group_map_by(|(trade, _)| trade.order.metadata.uid) - .into_iter() - .for_each(|(uid, trades)| { - let most_recent_data = PartiallyFilledOrder { - order: trades[0].0.order.clone(), - in_flight_trades: trades.into_iter().map(|(_, execution)| execution).collect(), - }; - // always overwrite existing data with the most recent data - self.in_flight_trades.insert(uid, most_recent_data); - }); - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::settlement::{SettlementEncoder, Trade}, - maplit::hashmap, - model::order::{Order, OrderData, OrderKind, OrderMetadata}, - primitive_types::H160, - }; - - #[test] - fn test() { - let token0 = H160::from_low_u64_be(0); - let token1 = H160::from_low_u64_be(1); - - let fill_or_kill = Order { - data: OrderData { - sell_token: token0, - buy_token: token1, - sell_amount: 100u8.into(), - buy_amount: 100u8.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - metadata: OrderMetadata { - uid: OrderUid::from_integer(1), - ..Default::default() - }, - ..Default::default() - }; - - // partially fillable order 30% filled - let mut partially_fillable_1 = fill_or_kill.clone(); - partially_fillable_1.data.partially_fillable = true; - partially_fillable_1.metadata.uid = OrderUid::from_integer(2); - partially_fillable_1.metadata.executed_buy_amount = 30u8.into(); - partially_fillable_1.metadata.executed_sell_amount = 30u8.into(); - partially_fillable_1 - .metadata - .executed_sell_amount_before_fees = 30u8.into(); - - // a different partially fillable order 30% filled - let mut partially_fillable_2 = partially_fillable_1.clone(); - partially_fillable_2.metadata.uid = OrderUid::from_integer(3); - - let trades = vec![ - Trade { - order: fill_or_kill.clone(), - executed_amount: 100u8.into(), - ..Default::default() - }, - // This order uses some of the remaining executable amount of partially_fillable_1 - Trade { - order: partially_fillable_2.clone(), - executed_amount: 20u8.into(), - ..Default::default() - }, - // Following orders use remaining executable amount of partially_fillable_2 - Trade { - order: partially_fillable_1.clone(), - executed_amount: 50u8.into(), - ..Default::default() - }, - Trade { - order: partially_fillable_1.clone(), - executed_amount: 20u8.into(), - ..Default::default() - }, - ]; - - let prices = hashmap! {token0 => 1u8.into(), token1 => 1u8.into()}; - let settlement = Settlement { - encoder: SettlementEncoder::with_trades(prices, trades), - ..Default::default() - }; - - let mut inflight = InFlightOrders::default(); - inflight.mark_settled_orders(1, &settlement); - let mut order0 = fill_or_kill.clone(); - order0.metadata.uid = OrderUid::from_integer(0); - let mut auction = Auction { - block: 0, - orders: vec![ - order0, - fill_or_kill, - partially_fillable_1, - partially_fillable_2, - ], - ..Default::default() - }; - - let mut update_and_get_filtered_orders = |auction: &Auction| { - let mut auction = auction.clone(); - inflight.update_and_filter(&mut auction); - auction.orders - }; - - let filtered = update_and_get_filtered_orders(&auction); - assert_eq!(filtered.len(), 2); - // keep order 0 because there are no trades for it in flight - assert_eq!(filtered[0].metadata.uid, OrderUid::from_integer(0)); - // drop order 1 because it's fill-or-kill and there is already one trade in - // flight keep order 2 and reduce remaning executable amount by trade - // amounts currently in flight - assert_eq!(filtered[1].metadata.uid, OrderUid::from_integer(3)); - assert_eq!(filtered[1].metadata.executed_buy_amount, 50u8.into()); - assert_eq!(filtered[1].metadata.executed_sell_amount, 50u8.into()); - assert_eq!( - filtered[1].metadata.executed_sell_amount_before_fees, - 50u8.into() - ); - // drop order 3 because in flight orders filled the remaining executable amount - - auction.block = 1; - let filtered = update_and_get_filtered_orders(&auction); - // same behaviour as above - assert_eq!(filtered.len(), 2); - assert_eq!(filtered[0].metadata.uid, OrderUid::from_integer(0)); - assert_eq!(filtered[1].metadata.uid, OrderUid::from_integer(3)); - assert_eq!(filtered[1].metadata.executed_buy_amount, 50u8.into()); - assert_eq!( - filtered[1].metadata.executed_sell_amount_before_fees, - 50u8.into() - ); - - auction.latest_settlement_block = 1; - let filtered = update_and_get_filtered_orders(&auction); - // Because we drop all in-flight trades from blocks older than the settlement - // block there is nothing left to filter solvable orders by => keep all - // orders unaltered - assert_eq!(filtered.len(), 4); - } - - #[test] - fn test_order_is_not_excluded_when_min_buy_amount_is_reached() { - let order = Order { - data: OrderData { - sell_token: H160::from_low_u64_be(0), - buy_token: H160::from_low_u64_be(1), - sell_amount: 100u8.into(), - buy_amount: 100u8.into(), - kind: OrderKind::Sell, - partially_fillable: true, - ..Default::default() - }, - metadata: OrderMetadata { - uid: OrderUid::from_integer(1), - // Only half filled but min buy amount already reached - executed_sell_amount: 50u8.into(), - executed_buy_amount: 100u8.into(), - ..Default::default() - }, - ..Default::default() - }; - let mut auction = Auction { - block: 0, - orders: vec![order], - ..Default::default() - }; - let mut inflight = InFlightOrders::default(); - inflight.update_and_filter(&mut auction); - assert_eq!(auction.orders.len(), 1); - } - - #[test] - fn test_filled_buy_order_gets_filtered() { - let order = Order { - data: OrderData { - sell_token: H160::from_low_u64_be(0), - buy_token: H160::from_low_u64_be(1), - sell_amount: 100u8.into(), - buy_amount: 100u8.into(), - kind: OrderKind::Buy, - ..Default::default() - }, - metadata: OrderMetadata { - uid: OrderUid::from_integer(1), - // Filled with a lot of surplus (only needed to sell half of maxSellAmount) - executed_sell_amount: 50u8.into(), - executed_buy_amount: 100u8.into(), - ..Default::default() - }, - ..Default::default() - }; - let mut auction = Auction { - block: 0, - orders: vec![order], - ..Default::default() - }; - let mut inflight = InFlightOrders::default(); - inflight.update_and_filter(&mut auction); - assert_eq!(auction.orders.len(), 0); - } -} diff --git a/crates/solver/src/interactions.rs b/crates/solver/src/interactions.rs index 9097108e6e..9d7c887ccd 100644 --- a/crates/solver/src/interactions.rs +++ b/crates/solver/src/interactions.rs @@ -1,11 +1,10 @@ pub mod allowances; -pub mod balancer_v2; -pub mod block_coinbase; +mod balancer_v2; mod erc20; mod uniswap_v2; mod uniswap_v3; mod weth; -pub mod zeroex; +mod zeroex; pub use { balancer_v2::BalancerSwapGivenOutInteraction, diff --git a/crates/solver/src/interactions/balancer_v2.rs b/crates/solver/src/interactions/balancer_v2.rs index 32cc61050d..c97a746d86 100644 --- a/crates/solver/src/interactions/balancer_v2.rs +++ b/crates/solver/src/interactions/balancer_v2.rs @@ -18,12 +18,6 @@ pub struct BalancerSwapGivenOutInteraction { pub user_data: Bytes>, } -#[repr(u8)] -pub enum SwapKind { - GivenIn = 0, - GivenOut = 1, -} - lazy_static::lazy_static! { /// An impossibly distant future timestamp. Note that we use `0x80000...00` /// as the value so that it is mostly 0's to save small amounts of gas on @@ -36,7 +30,7 @@ impl BalancerSwapGivenOutInteraction { let method = self.vault.swap( ( Bytes(self.pool_id.0), - SwapKind::GivenOut as _, + 1, // GivenOut, self.asset_in_max.token, self.asset_out.token, self.asset_out.amount, diff --git a/crates/solver/src/interactions/block_coinbase.rs b/crates/solver/src/interactions/block_coinbase.rs deleted file mode 100644 index 621f19d3e8..0000000000 --- a/crates/solver/src/interactions/block_coinbase.rs +++ /dev/null @@ -1,27 +0,0 @@ -use { - hex_literal::hex, - primitive_types::{H160, U256}, - shared::interaction::{EncodedInteraction, Interaction}, -}; - -// vk: A simple contract I made with verified code on etherscan: -// https://etherscan.io/address/0x5c2cD95CF750B8f8A4881d96F04bf571A07042B1 -// Gas use for a full transaction when amount is 0 is 23849 and nonzero 30549. -const MAINNET_ADDRESS: H160 = H160(hex!("5c2cd95cf750b8f8a4881d96f04bf571a07042b1")); -const METHOD_ID: [u8; 4] = hex!("2755cd2d"); - -#[derive(Clone, Debug)] -pub struct PayBlockCoinbase { - // ether wei - pub amount: U256, -} - -impl Interaction for PayBlockCoinbase { - fn encode(&self) -> Vec { - vec![( - MAINNET_ADDRESS, - self.amount, - ethcontract::Bytes(METHOD_ID.to_vec()), - )] - } -} diff --git a/crates/solver/src/lib.rs b/crates/solver/src/lib.rs index 04e75bb0d5..0fe9531bf8 100644 --- a/crates/solver/src/lib.rs +++ b/crates/solver/src/lib.rs @@ -1,28 +1,11 @@ -mod analytics; -pub mod arguments; -mod auction_preprocessing; -pub mod driver; -pub mod driver_logger; -pub mod in_flight_orders; pub mod interactions; pub mod liquidity; pub mod liquidity_collector; -pub mod metrics; -pub mod objective_value; +mod metrics; pub mod order_balance_filter; -pub mod orderbook; -pub mod run; -pub mod s3_instance_upload; -pub mod s3_instance_upload_arguments; pub mod settlement; pub mod settlement_access_list; -pub mod settlement_post_processing; -pub mod settlement_ranker; pub mod settlement_rater; pub mod settlement_simulation; pub mod settlement_submission; pub mod solver; -#[cfg(test)] -mod test; - -pub use self::run::{run, start}; diff --git a/crates/solver/src/liquidity/slippage.rs b/crates/solver/src/liquidity/slippage.rs index 7ec14c5473..cae0517367 100644 --- a/crates/solver/src/liquidity/slippage.rs +++ b/crates/solver/src/liquidity/slippage.rs @@ -1,169 +1,17 @@ //! Module defining slippage computation for AMM liquidity. use { - super::{AmmOrderExecution, LimitOrder}, - crate::solver::SolverType, - anyhow::{anyhow, Context as _, Result}, - clap::{Parser, ValueEnum as _}, + super::AmmOrderExecution, + anyhow::{Context as _, Result}, ethcontract::{H160, U256}, - model::order::OrderKind, num::{BigInt, BigRational, CheckedDiv, Integer as _, ToPrimitive as _}, once_cell::sync::OnceCell, shared::{external_prices::ExternalPrices, http_solver::model::TokenAmount}, - std::{ - borrow::Cow, - cmp, - collections::HashMap, - fmt::{self, Display, Formatter}, - str::FromStr, - }, + std::{borrow::Cow, cmp}, }; -/// Slippage configuration command line arguments. -#[derive(Debug, Parser)] -#[group(skip)] -pub struct Arguments { - /// The relative slippage tolerance to apply to on-chain swaps. This flag - /// expects a comma-separated list of relative slippage values in basis - /// points per solver. If a solver is not included, it will use the default - /// global value. For example, "10,oneinch=20,zeroex=5" will configure all - /// solvers to have 10 BPS of relative slippage tolerance, with 1Inch and - /// 0x solvers configured for 20 and 5 BPS respectively. The global value - /// can be specified as `~` to keep it its default. For example, - /// "~,paraswap=42" will configure all solvers to use the default - /// configuration, while overriding the ParaSwap solver to use 42 BPS. - #[clap(long, env, default_value = "10")] - pub relative_slippage_bps: SlippageArgumentValues, - - /// The absolute slippage tolerance in native token units to cap relative - /// slippage at. This makes it so very large trades use a potentially - /// tighter slippage tolerance to reduce absolute losses. This parameter - /// uses the same format as `--relative-slippage-bps`. For example, - /// "~,oneinch=0.001,zeroex=0.042" will disable absolute slippage tolerance - /// globally for all solvers, while overriding 1Inch and 0x solvers to cap - /// absolute slippage at 0.001Ξ and 0.042Ξ respectively. - #[clap(long, env, default_value = "~")] - pub absolute_slippage_in_native_token: SlippageArgumentValues, -} - -impl Arguments { - /// Returns the slippage calculator for the specified solver. - pub fn get_calculator(&self, solver: SolverType) -> SlippageCalculator { - let bps = self - .relative_slippage_bps - .get(solver) - .copied() - .unwrap_or(DEFAULT_MAX_SLIPPAGE_BPS); - let absolute = self - .absolute_slippage_in_native_token - .get(solver) - .map(|value| U256::from_f64_lossy(value * 1e18)); - - SlippageCalculator::from_bps(bps, absolute) - } - - /// Returns the slippage calculator for the specified solver. - pub fn get_global_calculator(&self) -> SlippageCalculator { - let bps = self - .relative_slippage_bps - .get_global() - .copied() - .unwrap_or(DEFAULT_MAX_SLIPPAGE_BPS); - let absolute = self - .absolute_slippage_in_native_token - .get_global() - .map(|value| U256::from_f64_lossy(value * 1e18)); - - SlippageCalculator::from_bps(bps, absolute) - } -} - -impl Display for Arguments { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let Self { - relative_slippage_bps, - absolute_slippage_in_native_token, - } = self; - - writeln!(f, "relative_slippage_bps: {}", relative_slippage_bps)?; - writeln!( - f, - "absolute_slippage_in_native_token: {}", - absolute_slippage_in_native_token, - )?; - - Ok(()) - } -} - -/// A comma separated slippage value per solver. -#[derive(Clone, Debug)] -pub struct SlippageArgumentValues(Option, HashMap); - -impl SlippageArgumentValues { - /// Gets the slippage configuration value for the specified solver. - pub fn get(&self, solver: SolverType) -> Option<&T> { - self.1.get(&solver).or(self.0.as_ref()) - } - - /// Gets the global slippage configuration value. - pub fn get_global(&self) -> Option<&T> { - self.0.as_ref() - } -} - -impl Display for SlippageArgumentValues -where - T: Display, -{ - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match &self.0 { - Some(global) => write!(f, "{global}")?, - None => f.write_str("~")?, - } - for (solver, value) in &self.1 { - write!(f, ",{solver:?}={value}")?; - } - Ok(()) - } -} - -impl FromStr for SlippageArgumentValues -where - T: FromStr, - anyhow::Error: From, -{ - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let mut values = s.split(','); - - let global_value = values - .next() - .map(|value| match value { - "~" => Ok(None), - _ => Ok(Some(value.parse()?)), - }) - .transpose()? - .flatten(); - let solver_values = values - .map(|part| { - let (solver, value) = part - .split_once('=') - .context("malformed solver slippage value")?; - Ok(( - SolverType::from_str(solver, true).map_err(|message| anyhow!(message))?, - value.parse()?, - )) - }) - .collect::>>()?; - - Ok(Self(global_value, solver_values)) - } -} - /// Constant maximum slippage of 10 BPS (0.1%) to use for on-chain liquidity. -pub const DEFAULT_MAX_SLIPPAGE_BPS: u32 = 10; +const DEFAULT_MAX_SLIPPAGE_BPS: u32 = 10; /// Basis points in 100%. const BPS_BASE: u32 = 10000; @@ -270,25 +118,6 @@ impl<'a> SlippageContext<'a> { pub fn apply_to_amount_out(&self, token: H160, amount: U256) -> Result { Ok(self.slippage(token, amount)?.sub_from_amount(amount)) } - - /// Computes the relative slippage for a limit order. - pub fn relative_for_order(&self, order: &LimitOrder) -> Result { - // We use the fixed token and amount for computing relative slippage. - // This is because the variable token amount may not be representative - // of the actual trade value. For example, a "pure" market sell order - // would have almost 0 limit buy amount, which would cause a potentially - // large order to not get capped on the absolute slippage value. - let (token, amount) = match order.kind { - OrderKind::Sell => (order.sell_token, order.sell_amount), - OrderKind::Buy => (order.buy_token, order.buy_amount), - }; - self.relative(token, amount) - } - - /// Computes the relative slippage for a token and amount. - pub fn relative(&self, token: H160, amount: U256) -> Result { - Ok(self.slippage(token, amount)?.relative()) - } } impl Default for SlippageContext<'static> { @@ -391,31 +220,6 @@ impl SlippageAmount { pub fn add_to_amount(&self, amount: U256) -> U256 { amount.saturating_add(self.absolute) } - - /// Returns the relative slippage value. - pub fn relative(&self) -> RelativeSlippage { - RelativeSlippage(self.relative) - } -} - -/// A relative slippage value. -pub struct RelativeSlippage(f64); - -impl RelativeSlippage { - /// Returns the relative slippage as a factor. - pub fn as_factor(&self) -> f64 { - self.0 - } - - /// Returns the relative slippage as a percentage. - pub fn as_percentage(&self) -> f64 { - self.0 * 100. - } - - /// Returns the relative slippage as basis points rounded down. - pub fn as_bps(&self) -> u32 { - (self.0 * 10000.) as _ - } } fn absolute_slippage_amount(relative: &BigRational, amount: &BigInt) -> BigInt { @@ -428,31 +232,10 @@ fn absolute_slippage_amount(relative: &BigRational, amount: &BigInt) -> BigInt { mod tests { use { super::*, - shared::{conversions::U256Ext as _, externalprices}, + shared::externalprices, testlib::tokens::{GNO, USDC, WETH}, }; - #[test] - fn limits_max_slippage() { - let calculator = SlippageCalculator::from_bps(10, Some(U256::exp10(17))); - let prices = externalprices! { - native_token: WETH, - GNO => U256::exp10(9).to_big_rational(), - USDC => BigRational::new(2.into(), 1000.into()), - }; - - let slippage = calculator.context(&prices); - for (token, amount, expected_slippage) in [ - (GNO, U256::exp10(12), 1), - (USDC, U256::exp10(23), 5), - (GNO, U256::exp10(8), 10), - (GNO, U256::exp10(17), 0), - ] { - let relative = slippage.relative(token, amount).unwrap(); - assert_eq!(relative.as_bps(), expected_slippage); - } - } - #[test] fn errors_on_missing_token_price() { let calculator = SlippageCalculator::from_bps(10, Some(1_000.into())); diff --git a/crates/solver/src/main.rs b/crates/solver/src/main.rs deleted file mode 100644 index e049ac8f7c..0000000000 --- a/crates/solver/src/main.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[tokio::main] -async fn main() { - solver::start(std::env::args()).await; -} diff --git a/crates/solver/src/metrics.rs b/crates/solver/src/metrics.rs index c77ff966d5..f8b0db4264 100644 --- a/crates/solver/src/metrics.rs +++ b/crates/solver/src/metrics.rs @@ -1,47 +1,3 @@ -use { - crate::{liquidity::Liquidity, settlement::Revertable}, - anyhow::Result, - ethcontract::U256, - model::order::{Order, OrderClass}, - prometheus::{Gauge, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGaugeVec}, - shared::metrics::LivenessChecking, - std::{ - convert::TryInto, - sync::Mutex, - time::{Duration, Instant}, - }, - strum::{IntoEnumIterator, VariantNames}, -}; - -/// The maximum time between the completion of two run loops. If exceeded the -/// service will be considered unhealthy. -const MAX_RUNLOOP_DURATION: Duration = Duration::from_secs(7 * 60); - -/// The outcome of a solver run. -#[derive(strum::EnumIter)] -pub enum SolverRunOutcome { - /// Computed a non-trivial settlement. - Success, - /// Run succeeded (i.e. did not error), but solver produced no settlement or - /// only trivial settlements. - Empty, - /// The solver timed out. - Timeout, - /// The solver returned an error. - Failure, -} - -impl SolverRunOutcome { - fn label(&self) -> &'static str { - match self { - SolverRunOutcome::Success => "success", - SolverRunOutcome::Empty => "empty", - SolverRunOutcome::Timeout => "timeout", - SolverRunOutcome::Failure => "failure", - } - } -} - /// The outcome of settlement submission. #[derive(strum::EnumIter)] pub enum SettlementSubmissionOutcome { @@ -76,378 +32,3 @@ impl SettlementSubmissionOutcome { } } } - -#[derive(strum::EnumIter)] -pub enum SolverSimulationOutcome { - Success, - Failure, - FailureOnLatest, -} - -impl SolverSimulationOutcome { - fn label(&self) -> &'static str { - match self { - SolverSimulationOutcome::Success => "success", - SolverSimulationOutcome::Failure => "failure", - SolverSimulationOutcome::FailureOnLatest => "failure_on_latest", - } - } -} - -pub trait SolverMetrics: Send + Sync { - fn orders_fetched(&self, orders: &[Order]); - fn liquidity_fetched(&self, liquidity: &[Liquidity]); - fn settlement_computed(&self, solver_type: &str, response: &str, start: Instant); - fn order_settled(&self, order: &Order, solver: &str); - fn settlement_simulation(&self, solver: &str, outcome: SolverSimulationOutcome); - fn settlement_non_positive_score(&self, solver: &str); - fn solver_run(&self, outcome: SolverRunOutcome, solver: &str); - fn single_order_solver_succeeded(&self, solver: &str); - fn single_order_solver_failed(&self, solver: &str); - fn settlement_submitted(&self, outcome: SettlementSubmissionOutcome, solver: &str); - fn settlement_access_list_saved_gas(&self, gas_saved: f64, sign: &str); - fn settlement_revertable_status(&self, status: Revertable, solver: &str); - fn orders_matched_but_not_settled(&self, count: usize); - fn report_order_surplus(&self, surplus_diff: f64); - fn runloop_completed(&self); - fn complete_runloop_until_transaction(&self, duration: Duration); - fn transaction_submission(&self, duration: Duration, strategy: &str); - fn transaction_gas_price(&self, gas_price: U256); -} - -// TODO add labeled interaction counter once we support more than one -// interaction -#[derive(prometheus_metric_storage::MetricStorage)] -struct Storage { - /// Number of trades settled - #[metric(name = "trade_counter", labels("solver_type", "trade_type"))] - trade_counter: IntCounterVec, - /// Counter for the number of seconds between creation and settlement of an - /// order - #[metric(name = "order_settlement_time_seconds", labels("order_type"))] - order_settlement_time: IntCounterVec, - /// Ms each solver takes to compute their solution - #[metric(name = "computation_time_ms", labels("solver_type", "solution_type"))] - solver_computation_time: IntCounterVec, - /// Like `computation_time_ms` but the total count - #[metric(name = "computation_count", labels("solver_type", "solution_type"))] - solver_computation_count: IntCounterVec, - /// Amount of orders labeled by liquidity type currently available to the - /// solvers - #[metric(name = "liquidity_gauge", labels("liquidity_type"))] - liquidity: IntGaugeVec, - /// Settlement simulation counts - #[metric(labels("result", "solver_type"))] - settlement_simulations: IntCounterVec, - /// Settlement non-positive score counts - #[metric(labels("solver_type"))] - settlement_non_positive_scores: IntCounterVec, - /// Settlement submission counts - #[metric(labels("result", "solver_type"))] - settlement_submissions: IntCounterVec, - /// Settlement revertable status counts - #[metric(labels("result", "solver_type"))] - settlement_revertable_status: IntCounterVec, - /// Saved gas by using access list for transaction submission - #[metric(labels("sign"))] - settlement_access_list_saved_gas: HistogramVec, - /// Success/Failure counts - #[metric(name = "solver_run", labels("result", "solver_type"))] - solver_runs: IntCounterVec, - /// Success/Failure counts - #[metric(name = "single_order_solver", labels("result", "solver_type"))] - single_order_solver_runs: IntCounterVec, - /// Counter for the number of orders for which at least one solver computed - /// an execution which was not chosen in this run-loop - #[metric(name = "orders_matched_not_settled")] - matched_but_unsettled_orders: IntCounter, - /// Surplus ratio differences between winning and best settlement per order - #[metric(name = "settlement_surplus_report", buckets(-1.0, -0.1, -0.01, -0.005, 0., 0.005, 0.01, 0.1, 1.0))] - order_surplus_report: Histogram, - /// Time a runloop that wants to submit a solution takes until the - /// transaction submission starts. - #[metric(name = "complete_runloop_until_transaction_seconds", buckets())] - complete_runloop_until_transaction: Histogram, - /// "Time it takes to submit a settlement transaction. - #[metric(name = "transaction_submission_seconds", labels("strategy"), buckets())] - transaction_submission: HistogramVec, - /// Actual gas price used by settlement transaction. - transaction_gas_price_gwei: Gauge, -} - -pub struct Metrics { - last_runloop_completed: Mutex, - metrics: &'static Storage, -} - -impl Metrics { - pub fn new() -> Result { - Ok(Self { - metrics: Storage::instance(observe::metrics::get_storage_registry()).unwrap(), - last_runloop_completed: Mutex::new(Instant::now()), - }) - } - - /// Initialize known to exist labels on solver related metrics to 0. - /// - /// Useful to make sure the prometheus metric exists for example for - /// alerting. - pub fn initialize_solver_metrics(&self, solver_names: &[&str]) { - for solver in solver_names { - for outcome in SolverSimulationOutcome::iter() { - self.metrics - .settlement_simulations - .with_label_values(&[outcome.label(), solver]) - .reset(); - } - for outcome in SettlementSubmissionOutcome::iter() { - self.metrics - .settlement_submissions - .with_label_values(&[outcome.label(), solver]) - .reset(); - } - for outcome in SolverRunOutcome::iter() { - self.metrics - .solver_runs - .with_label_values(&[outcome.label(), solver]) - .reset(); - } - } - } -} - -impl SolverMetrics for Metrics { - fn orders_fetched(&self, orders: &[Order]) { - let user_orders = orders - .iter() - .filter(|order| !order.metadata.is_liquidity_order) - .count(); - let liquidity_orders = orders.len() - user_orders; - - self.metrics - .liquidity - .with_label_values(&["UserOrder"]) - .set(user_orders as _); - self.metrics - .liquidity - .with_label_values(&["LiquidityOrder"]) - .set(liquidity_orders as _); - } - - fn liquidity_fetched(&self, liquidity: &[Liquidity]) { - // Reset all gauges and start from scratch - Liquidity::VARIANTS.iter().for_each(|label| { - self.metrics.liquidity.with_label_values(&[label]).set(0); - }); - liquidity.iter().for_each(|liquidity| { - let label: &str = liquidity.into(); - self.metrics.liquidity.with_label_values(&[label]).inc(); - }) - } - - fn settlement_computed(&self, solver_type: &str, response: &str, start: Instant) { - self.metrics - .solver_computation_time - .with_label_values(&[solver_type, response]) - .inc_by( - Instant::now() - .duration_since(start) - .as_millis() - .try_into() - .unwrap_or(u64::MAX), - ); - self.metrics - .solver_computation_count - .with_label_values(&[solver_type, response]) - .inc(); - } - - fn order_settled(&self, order: &Order, solver: &str) { - let time_to_settlement = - chrono::offset::Utc::now().signed_duration_since(order.metadata.creation_date); - let order_type = match order.metadata.class { - OrderClass::Market => "user_order", - OrderClass::Liquidity => "liquidity_order", - OrderClass::Limit => "limit_order", - }; - self.metrics - .trade_counter - .with_label_values(&[solver, order_type]) - .inc(); - self.metrics - .order_settlement_time - .with_label_values(&[order_type]) - .inc_by( - time_to_settlement - .num_seconds() - .try_into() - .unwrap_or_default(), - ) - } - - fn settlement_simulation(&self, solver: &str, outcome: SolverSimulationOutcome) { - self.metrics - .settlement_simulations - .with_label_values(&[outcome.label(), solver]) - .inc() - } - - fn settlement_non_positive_score(&self, solver: &str) { - self.metrics - .settlement_non_positive_scores - .with_label_values(&[solver]) - .inc() - } - - fn solver_run(&self, outcome: SolverRunOutcome, solver: &str) { - self.metrics - .solver_runs - .with_label_values(&[outcome.label(), solver]) - .inc() - } - - fn single_order_solver_succeeded(&self, solver: &str) { - self.metrics - .single_order_solver_runs - .with_label_values(&["success", solver]) - .inc() - } - - fn single_order_solver_failed(&self, solver: &str) { - self.metrics - .single_order_solver_runs - .with_label_values(&["failure", solver]) - .inc() - } - - fn settlement_submitted(&self, outcome: SettlementSubmissionOutcome, solver: &str) { - self.metrics - .settlement_submissions - .with_label_values(&[outcome.label(), solver]) - .inc() - } - - fn settlement_access_list_saved_gas(&self, gas_saved: f64, label: &str) { - self.metrics - .settlement_access_list_saved_gas - .with_label_values(&[label]) - .observe(gas_saved); - } - - fn orders_matched_but_not_settled(&self, count: usize) { - self.metrics - .matched_but_unsettled_orders - .inc_by(count as u64); - } - - fn report_order_surplus(&self, surplus_diff: f64) { - self.metrics.order_surplus_report.observe(surplus_diff) - } - - fn runloop_completed(&self) { - *self - .last_runloop_completed - .lock() - .expect("thread holding mutex panicked") = Instant::now(); - } - - fn complete_runloop_until_transaction(&self, duration: Duration) { - self.metrics - .complete_runloop_until_transaction - .observe(duration.as_secs_f64()); - } - - fn transaction_submission(&self, duration: Duration, strategy: &str) { - self.metrics - .transaction_submission - .with_label_values(&[strategy]) - .observe(duration.as_secs_f64()); - } - - fn transaction_gas_price(&self, gas_price: U256) { - self.metrics - .transaction_gas_price_gwei - .set(gas_price.to_f64_lossy() / 1e9) - } - - fn settlement_revertable_status(&self, status: Revertable, solver: &str) { - let result = match status { - Revertable::NoRisk => "no_risk", - Revertable::HighRisk => "high_risk", - }; - self.metrics - .settlement_revertable_status - .with_label_values(&[result, solver]) - .inc() - } -} - -#[async_trait::async_trait] -impl LivenessChecking for Metrics { - async fn is_alive(&self) -> bool { - Instant::now().duration_since( - *self - .last_runloop_completed - .lock() - .expect("thread holding mutex panicked"), - ) <= MAX_RUNLOOP_DURATION - } -} - -#[derive(Default)] -pub struct NoopMetrics {} - -impl SolverMetrics for NoopMetrics { - fn orders_fetched(&self, _liquidity: &[Order]) {} - - fn liquidity_fetched(&self, _liquidity: &[Liquidity]) {} - - fn settlement_computed(&self, _solver_type: &str, _response: &str, _start: Instant) {} - - fn order_settled(&self, _: &Order, _: &str) {} - - fn solver_run(&self, _: SolverRunOutcome, _: &str) {} - - fn single_order_solver_succeeded(&self, _: &str) {} - - fn single_order_solver_failed(&self, _: &str) {} - - fn settlement_submitted(&self, _: SettlementSubmissionOutcome, _: &str) {} - - fn settlement_revertable_status(&self, _: Revertable, _: &str) {} - - fn settlement_access_list_saved_gas(&self, _: f64, _: &str) {} - - fn orders_matched_but_not_settled(&self, _: usize) {} - - fn report_order_surplus(&self, _: f64) {} - - fn runloop_completed(&self) {} - - fn complete_runloop_until_transaction(&self, _: Duration) {} - - fn transaction_submission(&self, _: Duration, _: &str) {} - - fn transaction_gas_price(&self, _: U256) {} - - fn settlement_simulation(&self, _: &str, _: SolverSimulationOutcome) {} - - fn settlement_non_positive_score(&self, _: &str) {} -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn metrics_work() { - let metrics = Metrics::new().unwrap(); - metrics.settlement_computed("asdf", "none", Instant::now()); - metrics.order_settled(&Default::default(), "test"); - metrics.settlement_simulation("test", SolverSimulationOutcome::Success); - metrics.settlement_simulation("test", SolverSimulationOutcome::Failure); - metrics.settlement_submitted(SettlementSubmissionOutcome::Success, "test"); - metrics.orders_matched_but_not_settled(20); - metrics.initialize_solver_metrics(&["", "a"]); - } -} diff --git a/crates/solver/src/objective_value.rs b/crates/solver/src/objective_value.rs deleted file mode 100644 index 263490948e..0000000000 --- a/crates/solver/src/objective_value.rs +++ /dev/null @@ -1,168 +0,0 @@ -use { - crate::settlement::Settlement, - num::BigRational, - number::conversions::u256_to_big_rational, - primitive_types::U256, - shared::external_prices::ExternalPrices, -}; - -#[derive(Debug, Clone)] -pub struct Inputs { - pub surplus_given: BigRational, - pub solver_fees: BigRational, - pub gas_price: BigRational, - pub gas_amount: BigRational, -} - -impl Inputs { - pub fn from_settlement( - settlement: &Settlement, - prices: &ExternalPrices, - gas_price: BigRational, - gas_amount: &U256, - ) -> Self { - let gas_amount = u256_to_big_rational(gas_amount); - - Self { - surplus_given: settlement.total_surplus(prices), - solver_fees: settlement.total_scoring_fees(prices), - gas_price, - gas_amount, - } - } - - pub fn objective_value(&self) -> BigRational { - &self.surplus_given + &self.solver_fees - &self.gas_price * &self.gas_amount - } - - pub fn gas_cost(&self) -> BigRational { - &self.gas_price * &self.gas_amount - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn compute_objective_value() { - // Surplus1 is 1.003 ETH - let surplus1 = BigRational::from_integer(1_003_000_000_000_000_000_u128.into()); - - // Surplus2 is 1.009 ETH - let surplus2 = BigRational::from_integer(1_009_000_000_000_000_000_u128.into()); - - // Fees is 0.001 ETH - let solver_fees = BigRational::from_integer(1_000_000_000_000_000_u128.into()); - - let gas_estimate1 = BigRational::from_integer(300_000.into()); - let gas_estimate2 = BigRational::from_integer(500_000.into()); - - // Three cases when using three different gas prices: - - // Case 1: objective value 1 < objective value 2 - - // Gas price is 10 gwei - let gas_price = BigRational::from_integer(10_000_000_000_u128.into()); - - // Objective value 1 is 1.004 - 3e5 * 10e-9 = 1.001 ETH - let obj_value1 = Inputs { - surplus_given: surplus1.clone(), - solver_fees: solver_fees.clone(), - gas_amount: gas_estimate1.clone(), - gas_price: gas_price.clone(), - } - .objective_value(); - - assert_eq!( - obj_value1, - BigRational::from_integer(1_001_000_000_000_000_000_u128.into()) - ); - - // Objective value 2 is 1.01 - 5e5 * 10e-9 = 1.005 ETH - let obj_value2 = Inputs { - surplus_given: surplus2.clone(), - solver_fees: solver_fees.clone(), - gas_amount: gas_estimate2.clone(), - gas_price: gas_price.clone(), - } - .objective_value(); - - assert_eq!( - obj_value2, - BigRational::from_integer(1_005_000_000_000_000_000_u128.into()) - ); - - assert!(obj_value1 < obj_value2); - - // Case 2: objective value 1 = objective value 2 - - // Gas price is 30 gwei - let gas_price = BigRational::from_integer(30_000_000_000_u128.into()); - - // Objective value 1 is 1.004 - 3e5 * 30e-9 = 0.995 ETH - let obj_value1 = Inputs { - surplus_given: surplus1.clone(), - solver_fees: solver_fees.clone(), - gas_amount: gas_estimate1.clone(), - gas_price: gas_price.clone(), - } - .objective_value(); - - assert_eq!( - obj_value1, - BigRational::from_integer(995_000_000_000_000_000_u128.into()) - ); - - // Objective value 2 is 1.01 - 5e5 * 30e-9 = 0.995 ETH - let obj_value2 = Inputs { - surplus_given: surplus2.clone(), - solver_fees: solver_fees.clone(), - gas_amount: gas_estimate2.clone(), - gas_price: gas_price.clone(), - } - .objective_value(); - - assert_eq!( - obj_value2, - BigRational::from_integer(995_000_000_000_000_000_u128.into()) - ); - - assert!(obj_value1 == obj_value2); - - // Case 3: objective value 1 > objective value 2 - - // Gas price is 50 gwei - let gas_price = BigRational::from_integer(50_000_000_000_u128.into()); - - // Objective value 1 is 1.004 - 3e5 * 50e-9 = 0.989 ETH - let obj_value1 = Inputs { - surplus_given: surplus1.clone(), - solver_fees: solver_fees.clone(), - gas_amount: gas_estimate1.clone(), - gas_price: gas_price.clone(), - } - .objective_value(); - - assert_eq!( - obj_value1, - BigRational::from_integer(989_000_000_000_000_000_u128.into()) - ); - - // Objective value 2 is 1.01 - 5e5 * 50e-9 = 0.985 ETH - let obj_value2 = Inputs { - surplus_given: surplus2.clone(), - solver_fees: solver_fees.clone(), - gas_amount: gas_estimate2.clone(), - gas_price: gas_price.clone(), - } - .objective_value(); - - assert_eq!( - obj_value2, - BigRational::from_integer(985_000_000_000_000_000_u128.into()) - ); - - assert!(obj_value1 > obj_value2); - } -} diff --git a/crates/solver/src/orderbook.rs b/crates/solver/src/orderbook.rs deleted file mode 100644 index 72d2016b84..0000000000 --- a/crates/solver/src/orderbook.rs +++ /dev/null @@ -1,69 +0,0 @@ -use { - anyhow::Result, - model::auction::AuctionWithId, - reqwest::{Client, Url}, -}; - -pub struct OrderBookApi { - base: Url, - client: Client, - competition_auth: Option, -} - -impl OrderBookApi { - /// base: protocol and host of the url. example: `https://example.com` - pub fn new(base: Url, client: Client, competition_auth: Option) -> Self { - Self { - base, - client, - competition_auth, - } - } - - pub async fn get_auction(&self) -> Result { - let url = shared::url::join(&self.base, "api/v1/auction"); - let response = self.client.get(url).send().await?; - if let Err(err) = response.error_for_status_ref() { - let body = response.text().await; - return Err(anyhow::Error::new(err).context(format!("body: {body:?}"))); - } - let auction = response.json().await?; - Ok(auction) - } - - /// If this is false then sending solver competition most likely fails. - pub fn is_authenticated(&self) -> bool { - self.competition_auth.is_some() - } -} - -#[cfg(test)] -pub mod test_util { - use super::*; - - // cargo test local_orderbook -- --ignored --nocapture - #[tokio::test] - #[ignore] - async fn local_orderbook() { - let api = OrderBookApi::new( - Url::parse("http://localhost:8080").unwrap(), - Client::new(), - None, - ); - let auction = api.get_auction().await.unwrap(); - println!("{auction:#?}"); - } - - // cargo test real_orderbook -- --ignored --nocapture - #[tokio::test] - #[ignore] - async fn real_orderbook() { - let api = OrderBookApi::new( - Url::parse("https://barn.api.cow.fi/mainnet/").unwrap(), - Client::new(), - None, - ); - let auction = api.get_auction().await.unwrap(); - println!("{auction:#?}"); - } -} diff --git a/crates/solver/src/run.rs b/crates/solver/src/run.rs deleted file mode 100644 index bad5258512..0000000000 --- a/crates/solver/src/run.rs +++ /dev/null @@ -1,597 +0,0 @@ -use { - crate::{ - arguments::{Arguments, TransactionStrategyArg}, - driver::Driver, - liquidity::{ - balancer_v2::BalancerV2Liquidity, - order_converter::OrderConverter, - uniswap_v2::UniswapLikeLiquidity, - uniswap_v3::UniswapV3Liquidity, - zeroex::ZeroExLiquidity, - }, - liquidity_collector::{LiquidityCollecting, LiquidityCollector}, - metrics::Metrics, - orderbook::OrderBookApi, - s3_instance_upload::S3InstanceUploader, - settlement_post_processing::PostProcessingPipeline, - settlement_rater::{ScoreCalculator, SettlementRater}, - settlement_submission::{ - submitter::{ - eden_api::EdenApi, - flashbots_api::FlashbotsApi, - public_mempool_api::{ - validate_submission_node, - PublicMempoolApi, - SubmissionNode, - SubmissionNodeKind, - }, - Strategy, - }, - GlobalTxPool, - SolutionSubmitter, - StrategyArgs, - TransactionStrategy, - }, - }, - clap::Parser, - contracts::{BalancerV2Vault, IUniswapLikeRouter, UniswapV3SwapRouter, WETH9}, - ethcontract::errors::DeployError, - futures::{future, future::join_all, StreamExt}, - model::DomainSeparator, - num::rational::Ratio, - number::conversions::u256_to_big_rational, - shared::{ - account_balances, - baseline_solver::BaseTokens, - code_fetching::CachedCodeFetcher, - ethrpc, - http_client::HttpClientFactory, - maintenance::{Maintaining, ServiceMaintenance}, - metrics::serve_metrics, - network::network_name, - recent_block_cache::CacheConfig, - sources::{ - self, - balancer_v2::{ - pool_fetching::BalancerContracts, - BalancerFactoryKind, - BalancerPoolFetcher, - }, - uniswap_v2::{pool_cache::PoolCache, UniV2BaselineSourceParameters}, - uniswap_v3::pool_fetching::UniswapV3PoolFetcher, - BaselineSource, - }, - token_info::{CachedTokenInfoFetcher, TokenInfoFetcher}, - token_list::{AutoUpdatingTokenList, TokenListConfiguration}, - zeroex_api::DefaultZeroExApi, - }, - std::sync::Arc, -}; - -pub async fn start(args: impl Iterator) { - let args = Arguments::parse_from(args); - observe::tracing::initialize( - args.shared.logging.log_filter.as_str(), - args.shared.logging.log_stderr_threshold, - ); - observe::panic_hook::install(); - tracing::info!("running solver with validated arguments:\n{}", args); - observe::metrics::setup_registry(Some("gp_v2_solver".into()), None); - run(args).await; -} - -pub async fn run(args: Arguments) { - let metrics = Arc::new(Metrics::new().expect("Couldn't register metrics")); - - let http_factory = HttpClientFactory::new(&args.http_client); - - let web3 = ethrpc::web3( - &args.shared.ethrpc, - &http_factory, - &args.shared.node_url, - "base", - ); - - let chain_id = web3 - .eth() - .chain_id() - .await - .expect("Could not get chainId") - .as_u64(); - if let Some(expected_chain_id) = args.shared.chain_id { - assert_eq!( - chain_id, expected_chain_id, - "connected to node with incorrect chain ID", - ); - } - - let network_id = web3 - .net() - .version() - .await - .expect("failed to get network id"); - let network_name = network_name(&network_id, chain_id); - let settlement_contract = match args.shared.settlement_contract_address { - Some(address) => contracts::GPv2Settlement::with_deployment_info(&web3, address, None), - None => contracts::GPv2Settlement::deployed(&web3) - .await - .expect("load settlement contract"), - }; - let vault_relayer = settlement_contract - .vault_relayer() - .call() - .await - .expect("Couldn't get vault relayer address"); - let vault_contract = match args.shared.balancer_v2_vault_address { - Some(address) => Some(contracts::BalancerV2Vault::with_deployment_info( - &web3, address, None, - )), - None => match BalancerV2Vault::deployed(&web3).await { - Ok(contract) => Some(contract), - Err(DeployError::NotFound(_)) => None, - Err(err) => panic!("failed to get balancer vault contract: {err}"), - }, - }; - let native_token = match args.shared.native_token_address { - Some(address) => contracts::WETH9::with_deployment_info(&web3, address, None), - None => WETH9::deployed(&web3) - .await - .expect("load native token contract"), - }; - let base_tokens = Arc::new(BaseTokens::new( - native_token.address(), - &args.shared.base_tokens, - )); - - let block_retriever = args.shared.current_block.retriever(web3.clone()); - let token_info_fetcher = Arc::new(CachedTokenInfoFetcher::new(Arc::new(TokenInfoFetcher { - web3: web3.clone(), - }))); - let gas_price_estimator = Arc::new( - shared::gas_price_estimation::create_priority_estimator( - &http_factory, - &web3, - args.shared.gas_estimators.as_slice(), - args.shared.blocknative_api_key, - ) - .await - .expect("failed to create gas price estimator"), - ); - - let current_block_stream = args - .shared - .current_block - .stream(web3.clone()) - .await - .unwrap(); - - let cache_config = CacheConfig { - number_of_blocks_to_cache: args.shared.pool_cache_blocks, - maximum_recent_block_age: args.shared.pool_cache_maximum_recent_block_age, - max_retries: args.shared.pool_cache_maximum_retries, - delay_between_retries: args.shared.pool_cache_delay_between_retries, - ..Default::default() - }; - let baseline_sources = args.shared.baseline_sources.unwrap_or_else(|| { - sources::defaults_for_chain(chain_id).expect("failed to get default baseline sources") - }); - - let mut liquidity_sources: Vec> = vec![]; - let mut maintainers: Vec> = vec![]; - - tracing::info!(?baseline_sources, "using baseline sources"); - let univ2_sources = baseline_sources - .iter() - .filter_map(|source: &BaselineSource| { - UniV2BaselineSourceParameters::from_baseline_source(*source, &network_id) - }) - .chain(args.shared.custom_univ2_baseline_sources.iter().copied()); - let univ2_sources: Vec<(IUniswapLikeRouter, Arc)> = - futures::stream::iter(univ2_sources) - .then(|source: UniV2BaselineSourceParameters| { - let web3 = &web3; - let block_stream = ¤t_block_stream; - async move { - let source = source.into_source(web3).await.unwrap(); - let cache = Arc::new( - PoolCache::new(cache_config, source.pool_fetching, block_stream.clone()) - .unwrap(), - ); - (source.router, cache) - } - }) - .collect() - .await; - - if baseline_sources.contains(&BaselineSource::BalancerV2) { - let factories = args - .shared - .balancer_factories - .unwrap_or_else(|| BalancerFactoryKind::for_chain(chain_id)); - let contracts = BalancerContracts::new(&web3, factories).await.unwrap(); - match BalancerPoolFetcher::new( - &args.shared.graph_api_base_url, - chain_id, - block_retriever.clone(), - token_info_fetcher.clone(), - cache_config, - current_block_stream.clone(), - http_factory.create(), - web3.clone(), - &contracts, - args.shared.balancer_pool_deny_list, - ) - .await - { - Ok(balancer_pool_fetcher) => { - let balancer_pool_fetcher = Arc::new(balancer_pool_fetcher); - liquidity_sources.push(Box::new(BalancerV2Liquidity::new( - web3.clone(), - balancer_pool_fetcher, - settlement_contract.clone(), - contracts.vault, - ))); - } - Err(err) => { - tracing::error!( - "failed to create BalancerV2 pool fetcher, this is most likely due to \ - temporary issues with the graph (in that case consider manually restarting \ - services once the graph is back online)): {err}" - ); - } - } - } - - let uniswap_like_liquidity: Vec> = univ2_sources - .into_iter() - .map(|(router, cache)| -> Box { - Box::new(UniswapLikeLiquidity::new( - router, - settlement_contract.clone(), - web3.clone(), - cache, - )) - }) - .collect(); - liquidity_sources.extend(uniswap_like_liquidity); - - let solvers = { - if let Some(solver_accounts) = args.solver_accounts { - assert!( - solver_accounts.len() == args.solvers.len(), - "number of solvers ({}) does not match the number of accounts ({})", - args.solvers.len(), - solver_accounts.len() - ); - - join_all( - solver_accounts - .into_iter() - .map(|account_arg| account_arg.into_account(chain_id)), - ) - .await - .into_iter() - .zip(args.solvers) - .collect() - } else if let Some(account_arg) = args.solver_account { - join_all( - std::iter::repeat(account_arg) - .take(args.solvers.len()) - .map(|account| account.into_account(chain_id)), - ) - .await - .into_iter() - .zip(args.solvers) - .collect() - } else { - panic!("either SOLVER_ACCOUNTS or SOLVER_ACCOUNT must be set") - } - }; - - let zeroex_api = Arc::new( - DefaultZeroExApi::new( - http_factory.builder(), - args.shared - .zeroex_url - .as_deref() - .unwrap_or(DefaultZeroExApi::DEFAULT_URL), - args.shared.zeroex_api_key, - current_block_stream.clone(), - ) - .unwrap(), - ); - - let order_converter = OrderConverter { - native_token: native_token.clone(), - }; - - let market_makable_token_list_configuration = TokenListConfiguration { - url: args.market_makable_token_list, - update_interval: args.market_makable_token_list_update_interval, - chain_id, - client: http_factory.create(), - hardcoded: args.market_makable_tokens.unwrap_or_default(), - }; - // updated in background task - let market_makable_token_list = - AutoUpdatingTokenList::from_configuration(market_makable_token_list_configuration).await; - - let post_processing_pipeline = Arc::new(PostProcessingPipeline::new( - native_token.address(), - web3.clone(), - args.weth_unwrap_factor, - settlement_contract.clone(), - market_makable_token_list.clone(), - )); - - let domain = DomainSeparator::new(chain_id, settlement_contract.address()); - - let s3_instance_uploader = match args.s3_upload.into_config().unwrap() { - Some(config) => Some(Arc::new(S3InstanceUploader::new(config).await)), - None => None, - }; - - let code_fetcher = Arc::new(CachedCodeFetcher::new(Arc::new(web3.clone()))); - let access_list_estimator = Arc::new( - crate::settlement_access_list::create_priority_estimator( - &web3, - args.access_list_estimators.as_slice(), - args.shared - .tenderly - .get_api_instance(&http_factory, "access_lists".to_owned()) - .expect("failed to create Tenderly API"), - network_id.clone(), - ) - .expect("failed to create access list estimator"), - ); - let settlement_rater = Arc::new(SettlementRater { - access_list_estimator: access_list_estimator.clone(), - settlement_contract: settlement_contract.clone(), - web3: web3.clone(), - code_fetcher: code_fetcher.clone(), - score_calculator: ScoreCalculator::new(u256_to_big_rational(&args.score_cap)), - consider_cost_failure: args.transaction_strategy.iter().any(|s| { - matches!(s, TransactionStrategyArg::PublicMempool) - && !args.disable_high_risk_public_mempool_transactions - }), - }); - - let solver = crate::solver::create( - web3.clone(), - solvers, - base_tokens.clone(), - native_token.clone(), - args.quasimodo_solver_url, - args.balancer_sor_url, - &settlement_contract, - vault_contract.as_ref(), - token_info_fetcher, - network_name.to_string(), - chain_id, - args.shared.disabled_one_inch_protocols, - args.shared.disabled_paraswap_dexs, - args.shared.paraswap_partner, - args.shared.paraswap_api_url, - &http_factory, - metrics.clone(), - zeroex_api.clone(), - args.shared.disabled_zeroex_sources, - args.zeroex_enable_rfqt, - args.zeroex_enable_slippage_protection, - args.shared.use_internal_buffers, - url::Url::parse(shared::price_estimation::oneinch::BASE_URL).unwrap(), - args.shared.one_inch_referrer_address, - args.external_solvers.unwrap_or_default(), - order_converter.clone(), - args.max_settlements_per_solver, - args.max_merged_settlements, - args.smallest_partial_fill, - &args.slippage, - market_makable_token_list, - &args.order_prioritization, - post_processing_pipeline, - &domain, - s3_instance_uploader, - &args.risk_params, - settlement_rater.clone(), - args.enforce_correct_fees_for_partially_fillable_limit_orders, - args.ethflow_contract, - current_block_stream.clone(), - ) - .await - .expect("failure creating solvers"); - - metrics.initialize_solver_metrics( - &solver - .iter() - .map(|solver| solver.name()) - .collect::>(), - ); - - if baseline_sources.contains(&BaselineSource::ZeroEx) { - liquidity_sources.push(Box::new(ZeroExLiquidity::new( - web3.clone(), - zeroex_api, - contracts::IZeroEx::deployed(&web3).await.unwrap(), - settlement_contract.clone(), - ))); - } - - if baseline_sources.contains(&BaselineSource::UniswapV3) { - match UniswapV3PoolFetcher::new( - &args.shared.graph_api_base_url, - chain_id, - web3.clone(), - http_factory.create(), - block_retriever, - args.shared.max_pools_to_initialize_cache, - ) - .await - { - Ok(uniswap_v3_pool_fetcher) => { - let uniswap_v3_pool_fetcher = Arc::new(uniswap_v3_pool_fetcher); - maintainers.push(uniswap_v3_pool_fetcher.clone()); - liquidity_sources.push(Box::new(UniswapV3Liquidity::new( - UniswapV3SwapRouter::deployed(&web3).await.unwrap(), - settlement_contract.clone(), - web3.clone(), - uniswap_v3_pool_fetcher, - ))); - } - Err(err) => { - tracing::error!( - "failed to create UniswapV3 pool fetcher, this is most likely due to \ - temporary issues with the graph (in that case consider manually restarting \ - services once the graph is back online)): {err}" - ); - } - } - } - - let liquidity_collector = LiquidityCollector { - liquidity_sources, - base_tokens, - }; - let submission_nodes = future::join_all( - args.transaction_submission_nodes - .into_iter() - .enumerate() - .map(|(index, url)| { - let name = format!("broadcast {index}"); - (name, url, SubmissionNodeKind::Broadcast) - }) - .chain( - args.transaction_notification_nodes - .into_iter() - .enumerate() - .map(|(index, url)| { - let name = format!("notify {index}"); - (name, url, SubmissionNodeKind::Notification) - }), - ) - .map(|(name, url, kind)| { - let web3 = ethrpc::web3(&args.shared.ethrpc, &http_factory, &url, name); - let expected_network_id = &network_id; - async move { - if let Err(err) = validate_submission_node(&web3, expected_network_id).await { - tracing::error!("Error validating {kind:?} submission node {url}: {err}"); - assert!(kind == SubmissionNodeKind::Notification); - } - SubmissionNode::new(kind, web3) - } - }), - ) - .await; - let submitted_transactions = GlobalTxPool::default(); - let mut transaction_strategies = vec![]; - for strategy in args.transaction_strategy { - match strategy { - TransactionStrategyArg::Eden => { - transaction_strategies.push(TransactionStrategy::Eden(StrategyArgs { - submit_api: Box::new( - EdenApi::new(http_factory.create(), args.eden_api_url.clone()).unwrap(), - ), - max_additional_tip: args.max_additional_eden_tip, - additional_tip_percentage_of_max_fee: args.additional_tip_percentage, - sub_tx_pool: submitted_transactions.add_sub_pool(Strategy::Eden), - use_soft_cancellations: false, - })) - } - TransactionStrategyArg::Flashbots => { - for flashbots_url in args.flashbots_api_url.clone() { - transaction_strategies.push(TransactionStrategy::Flashbots(StrategyArgs { - submit_api: Box::new( - FlashbotsApi::new(http_factory.create(), flashbots_url).unwrap(), - ), - max_additional_tip: args.max_additional_flashbot_tip, - additional_tip_percentage_of_max_fee: args.additional_tip_percentage, - sub_tx_pool: submitted_transactions.add_sub_pool(Strategy::Flashbots), - use_soft_cancellations: args.use_soft_cancellations, - })) - } - } - TransactionStrategyArg::PublicMempool => { - assert!( - submission_nodes.iter().any(|node| node.can_broadcast()), - "At least one submission node that can broadcast transactions must be \ - available" - ); - transaction_strategies.push(TransactionStrategy::PublicMempool(StrategyArgs { - submit_api: Box::new(PublicMempoolApi::new( - submission_nodes.clone(), - args.disable_high_risk_public_mempool_transactions, - )), - max_additional_tip: 0., - additional_tip_percentage_of_max_fee: 0., - sub_tx_pool: submitted_transactions.add_sub_pool(Strategy::PublicMempool), - use_soft_cancellations: false, - })) - } - TransactionStrategyArg::DryRun => { - transaction_strategies.push(TransactionStrategy::DryRun) - } - } - } - let solution_submitter = SolutionSubmitter { - web3: web3.clone(), - contract: settlement_contract.clone(), - gas_price_estimator: gas_price_estimator.clone(), - target_confirm_time: args.target_confirm_time, - max_confirm_time: args.max_submission_time, - retry_interval: args.submission_retry_interval, - transaction_strategies, - access_list_estimator, - code_fetcher, - }; - let api = OrderBookApi::new( - args.orderbook_url, - http_factory.create(), - args.shared.solver_competition_auth.clone(), - ); - - let balance_fetcher = account_balances::fetcher( - &web3, - account_balances::Contracts { - chain_id, - settlement: settlement_contract.address(), - vault_relayer, - vault: vault_contract.as_ref().map(|contract| contract.address()), - }, - ); - - let mut driver = Driver::new( - settlement_contract, - liquidity_collector, - solver, - gas_price_estimator, - args.gas_price_cap, - args.settle_interval, - native_token.address(), - metrics.clone(), - web3, - network_id, - args.solver_time_limit, - args.skip_non_positive_score_settlements, - current_block_stream.clone(), - solution_submitter, - api, - args.simulation_gas_limit, - args.max_settlement_price_deviation - .map(|max_price_deviation| Ratio::from_float(max_price_deviation).unwrap()), - args.token_list_restriction_for_price_checks.into(), - args.shared - .tenderly - .get_api_instance(&http_factory, "driver".to_owned()) - .expect("failed to create Tenderly API"), - args.process_partially_fillable_liquidity_orders, - args.process_partially_fillable_limit_orders, - settlement_rater, - balance_fetcher, - ); - - let maintainer = ServiceMaintenance::new(maintainers); - tokio::task::spawn(maintainer.run_maintenance_on_new_block(current_block_stream)); - - serve_metrics(metrics, ([0, 0, 0, 0], args.metrics_port).into()); - driver.run_forever().await -} diff --git a/crates/solver/src/s3_instance_upload.rs b/crates/solver/src/s3_instance_upload.rs deleted file mode 100644 index c40bd99db4..0000000000 --- a/crates/solver/src/s3_instance_upload.rs +++ /dev/null @@ -1,149 +0,0 @@ -use { - anyhow::{Context, Result}, - aws_sdk_s3::{primitives::ByteStream, Client}, - flate2::{bufread::GzEncoder, Compression}, - model::auction::AuctionId, - std::io::Read, -}; - -#[derive(Default)] -pub struct Config { - pub bucket: String, - /// Prepended to the auction id to form the final filename. Something like - /// "staging/mainnet/quasimodo/". Should end with `/` if intended to be a - /// folder. - pub filename_prefix: String, -} - -pub struct S3InstanceUploader { - bucket: String, - filename_prefix: String, - client: Client, -} - -impl S3InstanceUploader { - pub async fn new(config: Config) -> Self { - let uploader = Self { - bucket: config.bucket, - filename_prefix: config.filename_prefix, - client: Client::new(&aws_config::load_from_env().await), - }; - uploader.assert_credentials_are_usable().await; - uploader - } - - /// Upload the bytes (expected to represent a json encoded solver instance) - /// to the configured S3 bucket. - /// - /// The final filename is the configured prefix followed by - /// `{auction_id}.json.gzip`. - pub async fn upload_instance(&self, auction: AuctionId, value: &[u8]) -> Result<()> { - self.upload(self.filename(auction), value).await - } - - /// Uploads a small test file to verify that the credentials loaded from the - /// environment allow uploads to S3. - async fn assert_credentials_are_usable(&self) { - const DOCS_URL: &str = "https://docs.rs/aws-config/latest/aws_config/default_provider/credentials/struct.DefaultCredentialsChain.html"; - self.upload( - "test".into(), - "test file to verify uploading capabilities".as_bytes(), - ) - .await - .unwrap_or_else(|err| { - panic!( - "Could not upload test file to S3.\n Either disable uploads to S3 by removing the \ - s3_instance_upload_* arguments.\n Or make sure your environment variables are \ - set up to contain the correct AWS credentials.\n See {DOCS_URL} for more details \ - on that. \n{err:?}" - ) - }) - } - - /// Compresses the input bytes using Gzip. - fn gzip(&self, bytes: &[u8]) -> Result> { - let mut encoder = GzEncoder::new(bytes, Compression::best()); - let mut encoded: Vec = Vec::with_capacity(bytes.len()); - encoder.read_to_end(&mut encoded).context("gzip encoding")?; - Ok(encoded) - } - - fn filename(&self, auction: AuctionId) -> String { - format!("{}{auction}.json", self.filename_prefix) - } - - async fn upload(&self, key: String, bytes: &[u8]) -> Result<()> { - let encoded = self.gzip(bytes)?; - self.client - .put_object() - .bucket(self.bucket.clone()) - .key(key) - .body(ByteStream::new(encoded.into())) - .content_encoding("gzip") - .content_type("application/json") - .send() - .await?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use {super::*, flate2::read::GzDecoder, serde_json::json}; - - #[tokio::test] - #[ignore] - async fn print_filename() { - let uploader = S3InstanceUploader::new(Config { - filename_prefix: "test/".to_string(), - ..Default::default() - }) - .await; - let key = uploader.filename(11); - println!("{key}"); - } - - // This test requires AWS credentials to be set via env variables. - // See https://docs.rs/aws-config/latest/aws_config/default_provider/credentials/struct.DefaultCredentialsChain.html - // to know which arguments are expected and in what precedence they - // get loaded. - #[tokio::test] - #[ignore] - async fn real_upload() { - let config = Config { - bucket: std::env::var("bucket").unwrap(), - filename_prefix: "".to_string(), - }; - - let key = "test.json".to_string(); - // Upload a reasonable amount of data. This helps see the benefits of - // compression. - let value = serde_json::to_string(&json!({ - "content": include_str!("../../../README.md"), - "timestamp": chrono::Utc::now(), - })) - .unwrap(); - - let uploader = S3InstanceUploader::new(config).await; - uploader - .upload(key.clone(), value.as_bytes()) - .await - .unwrap(); - - let get_object = uploader - .client - .get_object() - .bucket(uploader.bucket) - .key(key) - .send() - .await - .unwrap(); - let body = get_object.body.collect().await.unwrap().to_vec(); - - let mut decoder = GzDecoder::new(body.as_slice()); - let mut decoded = String::new(); - decoder.read_to_string(&mut decoded).unwrap(); - - assert_eq!(value, decoded); - } -} diff --git a/crates/solver/src/s3_instance_upload_arguments.rs b/crates/solver/src/s3_instance_upload_arguments.rs deleted file mode 100644 index 26bf00e8b7..0000000000 --- a/crates/solver/src/s3_instance_upload_arguments.rs +++ /dev/null @@ -1,57 +0,0 @@ -use {crate::s3_instance_upload::Config, shared::arguments::display_option}; - -#[derive(clap::Parser)] -pub struct S3UploadArguments { - #[clap(long, env)] - /// The s3_instance_upload_* arguments configure how quasimodo instances - /// should be uploaded to AWS S3. They must either all be set or all not - /// set. If they are set then every instance sent to Quasimodo as part - /// of auction solving is also uploaded to S3. - pub s3_instance_upload_bucket: Option, - - /// Prepended to the auction id to form the final instance filename on S3. - /// Something like "staging/mainnet/quasimodo/". Should end with `/` if - /// intended to be a folder. - #[clap(long, env)] - pub s3_instance_upload_filename_prefix: Option, -} - -impl std::fmt::Display for S3UploadArguments { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { - s3_instance_upload_bucket, - s3_instance_upload_filename_prefix, - } = self; - - display_option(f, "s3_instance_upload_bucket", s3_instance_upload_bucket)?; - display_option( - f, - "s3_instance_upload_filename_prefix", - s3_instance_upload_filename_prefix, - )?; - Ok(()) - } -} - -impl S3UploadArguments { - pub fn into_config(self) -> anyhow::Result> { - let s3_args = &[ - &self.s3_instance_upload_bucket, - &self.s3_instance_upload_filename_prefix, - ]; - let all_some = s3_args.iter().all(|arg| arg.is_some()); - let all_none = s3_args.iter().all(|arg| arg.is_none()); - anyhow::ensure!( - all_some || all_none, - "either set all s3_instance_upload bucket arguments or none" - ); - Ok(if all_some { - Some(Config { - bucket: self.s3_instance_upload_bucket.unwrap(), - filename_prefix: self.s3_instance_upload_filename_prefix.unwrap(), - }) - } else { - None - }) - } -} diff --git a/crates/solver/src/settlement_post_processing/mod.rs b/crates/solver/src/settlement_post_processing/mod.rs deleted file mode 100644 index 33471ff549..0000000000 --- a/crates/solver/src/settlement_post_processing/mod.rs +++ /dev/null @@ -1,165 +0,0 @@ -mod optimize_buffer_usage; -mod optimize_score; -mod optimize_unwrapping; - -use { - crate::{ - settlement::Settlement, - settlement_simulation::simulate_and_estimate_gas_at_current_block, - solver::{http_solver::buffers::BufferRetriever, risk_computation::RiskCalculator}, - }, - anyhow::{Context, Result}, - contracts::{GPv2Settlement, WETH9}, - ethcontract::{Account, U256}, - gas_estimation::GasPrice1559, - optimize_buffer_usage::optimize_buffer_usage, - optimize_score::compute_success_probability, - optimize_unwrapping::optimize_unwrapping, - primitive_types::H160, - shared::{ - ethrpc::Web3, - http_solver::{self, model::InternalizationStrategy}, - token_list::AutoUpdatingTokenList, - }, -}; - -/// Determines whether a settlement would be executed successfully. -/// If the settlement would succeed, the gas estimate is returned. -#[cfg_attr(test, mockall::automock)] -#[async_trait::async_trait] -pub trait SettlementSimulating: Send + Sync { - async fn estimate_gas(&self, settlement: Settlement) -> Result; -} - -pub struct SettlementSimulator { - settlement_contract: GPv2Settlement, - gas_price: GasPrice1559, - solver_account: Account, - internalization: InternalizationStrategy, -} - -#[async_trait::async_trait] -impl SettlementSimulating for SettlementSimulator { - async fn estimate_gas(&self, settlement: Settlement) -> Result { - let settlement = settlement.encode(self.internalization); - simulate_and_estimate_gas_at_current_block( - std::iter::once((self.solver_account.clone(), settlement, None)), - &self.settlement_contract, - self.gas_price, - ) - .await? - .pop() - .context("empty result")? - .map_err(Into::into) - } -} - -#[async_trait::async_trait] -#[mockall::automock] -pub trait PostProcessing: Send + Sync + 'static { - /// Tries to apply optimizations to a given settlement. If all optimizations - /// fail the original settlement gets returned. - async fn optimize_settlement( - &self, - settlement: Settlement, - solver_account: Account, - gas_price: GasPrice1559, - risk_calculator: Option<&RiskCalculator>, - ) -> Settlement; -} - -pub struct PostProcessingPipeline { - settlement_contract: GPv2Settlement, - unwrap_factor: f64, - weth: WETH9, - buffer_retriever: BufferRetriever, - market_makable_token_list: AutoUpdatingTokenList, -} - -impl PostProcessingPipeline { - pub fn new( - native_token: H160, - web3: Web3, - unwrap_factor: f64, - settlement_contract: GPv2Settlement, - market_makable_token_list: AutoUpdatingTokenList, - ) -> Self { - let weth = WETH9::at(&web3, native_token); - let buffer_retriever = BufferRetriever::new(web3, settlement_contract.address()); - - Self { - settlement_contract, - unwrap_factor, - weth, - buffer_retriever, - market_makable_token_list, - } - } -} - -#[async_trait::async_trait] -impl PostProcessing for PostProcessingPipeline { - async fn optimize_settlement( - &self, - settlement: Settlement, - solver_account: Account, - gas_price: GasPrice1559, - risk_calculator: Option<&RiskCalculator>, - ) -> Settlement { - let simulator = SettlementSimulator { - settlement_contract: self.settlement_contract.clone(), - gas_price, - solver_account: solver_account.clone(), - internalization: InternalizationStrategy::SkipInternalizableInteraction, - }; - - let optimized_solution = optimize_buffer_usage( - settlement, - self.market_makable_token_list.clone(), - &simulator, - ) - .await; - - // an error will leave the settlement unmodified - let optimized_solution = optimize_unwrapping( - optimized_solution, - &simulator, - &self.buffer_retriever, - &self.weth, - self.unwrap_factor, - ) - .await; - - // although some solvers provided success probability, protocol will - // override the success probability if it has risk parameters for the solver. - // this is currently done for naive, baseline, gnosis solvers - // TODO: once we eliminate naive and baseline this logic should be moved to - // SingleOrderSettlement::into_settlement - match (optimized_solution.score, risk_calculator) { - (http_solver::model::Score::RiskAdjusted { gas_amount, .. }, Some(risk_calculator)) => { - match compute_success_probability( - &optimized_solution, - &simulator, - risk_calculator, - gas_price, - &solver_account.address(), - ) - .await - { - Ok(success_probability) => Settlement { - score: http_solver::model::Score::RiskAdjusted { - success_probability, - gas_amount, - }, - ..optimized_solution - }, - Err(err) => { - tracing::warn!(?err, "failed to compute success probability"); - optimized_solution - } - } - } - _ => optimized_solution, - } - } -} diff --git a/crates/solver/src/settlement_post_processing/optimize_buffer_usage.rs b/crates/solver/src/settlement_post_processing/optimize_buffer_usage.rs deleted file mode 100644 index 3fb9336504..0000000000 --- a/crates/solver/src/settlement_post_processing/optimize_buffer_usage.rs +++ /dev/null @@ -1,152 +0,0 @@ -use { - super::SettlementSimulating, - crate::settlement::Settlement, - primitive_types::H160, - shared::token_list::AutoUpdatingTokenList, -}; - -/// If a settlement only trades trusted tokens try to optimize it by trading -/// with internal buffers. -pub async fn optimize_buffer_usage( - settlement: Settlement, - market_makable_token_list: AutoUpdatingTokenList, - settlement_simulator: &impl SettlementSimulating, -) -> Settlement { - // We don't want to buy tokens that we don't trust. If no list is set, we settle - // with external liquidity. - if !is_only_selling_trusted_tokens(&settlement, &market_makable_token_list) { - return settlement; - } - - // Sometimes solvers propose stable to stable trades that produce good prices - // but require enormous gas overhead. Normally these would be discarded but - // due to naive buffer usage rules it's technically allowed to internalize - // these trades which gets rid of their high gas cost. That's why we disable - // internalization of any settlement that contains a stable to stable - // trade until we have better rules for buffer usage. This code only affects - // Gnosis solvers. - if some_stable_to_stable_trade(&settlement) { - return settlement; - } - - let optimized_settlement = settlement.clone().without_onchain_liquidity(); - - if settlement_simulator - .estimate_gas(optimized_settlement.clone()) - .await - .is_ok() - { - tracing::debug!("settlement without onchain liquidity"); - return optimized_settlement; - } - - settlement -} - -fn is_only_selling_trusted_tokens( - settlement: &Settlement, - market_makable_token_list: &AutoUpdatingTokenList, -) -> bool { - let market_makable_token_list = market_makable_token_list.all(); - !settlement - .traded_orders() - .any(|order| !market_makable_token_list.contains(&order.data.sell_token)) -} - -fn some_stable_to_stable_trade(settlement: &Settlement) -> bool { - let stable_coins = [ - H160(hex_literal::hex!( - "A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // USDC - )), - H160(hex_literal::hex!( - "6B175474E89094C44Da98b954EedeAC495271d0F" // DAI - )), - H160(hex_literal::hex!( - "dAC17F958D2ee523a2206206994597C13D831ec7" // USDT - )), - ]; - settlement.traded_orders().any(|o| { - stable_coins.contains(&o.data.sell_token) && stable_coins.contains(&o.data.buy_token) - }) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::settlement::Trade, - model::order::{Order, OrderData}, - primitive_types::H160, - }; - - fn trade(sell_token: H160, buy_token: H160) -> Trade { - Trade { - order: Order { - data: OrderData { - sell_token, - buy_token, - sell_amount: 1.into(), - buy_amount: 1.into(), - ..Default::default() - }, - ..Default::default() - }, - executed_amount: 1.into(), - ..Default::default() - } - } - - #[test] - fn test_is_only_selling_trusted_tokens() { - let good_token = H160::from_low_u64_be(1); - let another_good_token = H160::from_low_u64_be(2); - let bad_token = H160::from_low_u64_be(3); - - let token_list = - AutoUpdatingTokenList::new([good_token, another_good_token].into_iter().collect()); - - let settlement = Settlement::with_default_prices(vec![ - trade(good_token, bad_token), - trade(another_good_token, bad_token), - ]); - assert!(is_only_selling_trusted_tokens(&settlement, &token_list)); - - let settlement = Settlement::with_default_prices(vec![ - trade(good_token, bad_token), - trade(another_good_token, bad_token), - trade(bad_token, good_token), - ]); - assert!(!is_only_selling_trusted_tokens(&settlement, &token_list)); - } - - #[test] - fn prevent_stable_to_stable_trade_internalization() { - let usdc = H160(hex_literal::hex!( - "A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - )); - let dai = H160(hex_literal::hex!( - "6B175474E89094C44Da98b954EedeAC495271d0F" - )); - let usdt = H160(hex_literal::hex!( - "dAC17F958D2ee523a2206206994597C13D831ec7" - )); - let non_stable_a = H160([1; 20]); - let non_stable_b = H160([2; 20]); - - let settlement = Settlement::with_default_prices(vec![trade(usdc, dai)]); - assert!(some_stable_to_stable_trade(&settlement)); - let settlement = Settlement::with_default_prices(vec![trade(usdc, usdt)]); - assert!(some_stable_to_stable_trade(&settlement)); - let settlement = Settlement::with_default_prices(vec![ - trade(usdc, usdt), - trade(non_stable_a, non_stable_b), - ]); - assert!(some_stable_to_stable_trade(&settlement)); - let settlement = Settlement::with_default_prices(vec![trade(usdc, non_stable_a)]); - assert!(!some_stable_to_stable_trade(&settlement)); - let settlement = Settlement::with_default_prices(vec![trade(non_stable_a, usdc)]); - assert!(!some_stable_to_stable_trade(&settlement)); - let settlement = Settlement::with_default_prices(vec![trade(non_stable_a, non_stable_b)]); - assert!(!some_stable_to_stable_trade(&settlement)); - } -} diff --git a/crates/solver/src/settlement_post_processing/optimize_score.rs b/crates/solver/src/settlement_post_processing/optimize_score.rs deleted file mode 100644 index 28b9c09d86..0000000000 --- a/crates/solver/src/settlement_post_processing/optimize_score.rs +++ /dev/null @@ -1,37 +0,0 @@ -use { - super::SettlementSimulating, - crate::{settlement::Settlement, solver::risk_computation::RiskCalculator}, - anyhow::Result, - ethcontract::Address, - gas_estimation::GasPrice1559, -}; - -pub async fn compute_success_probability( - settlement: &Settlement, - settlement_simulator: &impl SettlementSimulating, - risk_calculator: &RiskCalculator, - gas_price: GasPrice1559, - solver: &Address, -) -> Result { - let gas_amount = settlement_simulator - .estimate_gas(settlement.clone()) - .await - .map(|gas_amount| { - // Multiply by 0.9 to get more realistic gas amount. - // This is because the gas estimation is not accurate enough and does not take - // the EVM gas refund into account. - gas_amount.to_f64_lossy() * 0.9 - })?; - let gas_price = gas_price.effective_gas_price(); - let nmb_orders = settlement.trades().count(); - - let success_probability = risk_calculator.calculate(gas_amount, gas_price, nmb_orders)?; - - tracing::debug!( - ?solver, - ?success_probability, - "computed success_probability", - ); - - Ok(success_probability) -} diff --git a/crates/solver/src/settlement_post_processing/optimize_unwrapping.rs b/crates/solver/src/settlement_post_processing/optimize_unwrapping.rs deleted file mode 100644 index 53f6640faf..0000000000 --- a/crates/solver/src/settlement_post_processing/optimize_unwrapping.rs +++ /dev/null @@ -1,213 +0,0 @@ -use { - super::SettlementSimulating, - crate::{settlement::Settlement, solver::http_solver::buffers::BufferRetrieving}, - contracts::WETH9, - primitive_types::U256, -}; - -/// Tries to do one of 2 optimizations. -/// 1) Drop WETH unwraps and instead pay ETH with the settlement contract's -/// buffer. 2) Top up settlement contract's ETH buffer by unwrapping way more -/// WETH than this settlement needs. This will cause the next few settlements -/// to use optimization 1. -pub async fn optimize_unwrapping( - settlement: Settlement, - settlement_simulator: &impl SettlementSimulating, - buffer_retriever: &impl BufferRetrieving, - weth: &WETH9, - unwrap_factor: f64, -) -> Settlement { - let required_eth_payout = settlement.encoder.amount_to_unwrap(weth.address()); - if required_eth_payout.is_zero() { - return settlement; - } - - // We can't determine how much of the WETH and ETH buffers solvers are using for - // their solution. Dropping the unwrap could alter the buffers such that the - // proposed solution is no longer possible. That's why a simulation is - // necessary. - let mut optimized_settlement = settlement.clone(); - optimized_settlement.encoder.drop_unwrap(weth.address()); - - if settlement_simulator - .estimate_gas(optimized_settlement.clone()) - .await - .is_ok() - { - tracing::debug!("use internal buffer for unwraps"); - return optimized_settlement; - } - - let buffers = buffer_retriever.get_buffers(&[weth.address()]).await; - let weth_balance = match buffers.get(&weth.address()) { - Some(Ok(balance)) => *balance, - _ => return settlement, - }; - let amount_to_unwrap = U256::from_f64_lossy(weth_balance.to_f64_lossy() * unwrap_factor); - - if amount_to_unwrap <= required_eth_payout { - // if we wouldn't unwrap more than required we can leave the settlement as it is - return settlement; - } - - // simulate settlement with way bigger unwrap - optimized_settlement - .encoder - .add_unwrap(crate::interactions::UnwrapWethInteraction { - weth: weth.clone(), - amount: amount_to_unwrap, - }); - - // We can't determine how much of the WETH and ETH buffers solvers are using for - // their solution. Increasing the unwrap could alter the buffers such that - // the proposed solution is no longer possible. That's why a simulation is - // necessary. - if settlement_simulator - .estimate_gas(optimized_settlement.clone()) - .await - .is_ok() - { - tracing::debug!( - ?amount_to_unwrap, - "unwrap parts of the settlement contract's WETH buffer" - ); - return optimized_settlement; - } - - settlement -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - interactions::UnwrapWethInteraction, - settlement_post_processing::MockSettlementSimulating, - solver::http_solver::buffers::MockBufferRetrieving, - }, - contracts::dummy_contract, - maplit::hashmap, - std::collections::HashMap, - }; - - fn to_wei(base: u128) -> U256 { - U256::from(base) * U256::from(10).pow(18.into()) - } - - fn settlement_with_unwrap(weth: &WETH9, amount: U256) -> Settlement { - let mut settlement = Settlement::with_trades(HashMap::default(), Vec::default()); - if !amount.is_zero() { - settlement.encoder.add_unwrap(UnwrapWethInteraction { - weth: weth.clone(), - amount, - }); - } - assert_eq!(amount, settlement.encoder.amount_to_unwrap(weth.address())); - settlement - } - - #[tokio::test] - async fn drop_unwrap_if_eth_buffer_is_big_enough() { - let weth = dummy_contract!(WETH9, [0x42; 20]); - let mut buffer_retriever = MockBufferRetrieving::new(); - let weth_address = weth.address(); - buffer_retriever - .expect_get_buffers() - .returning(move |_| hashmap! {weth_address => Ok(U256::zero())}); - - let mut settlement_simulator = MockSettlementSimulating::new(); - settlement_simulator - .expect_estimate_gas() - .times(1) - .returning(|_| Ok(Default::default())); - - let settlement = optimize_unwrapping( - settlement_with_unwrap(&weth, to_wei(1)), - &settlement_simulator, - &buffer_retriever, - &weth, - 0.6, - ) - .await; - - // no unwraps left because we pay 1 ETH from our buffer - assert_eq!( - U256::zero(), - settlement.encoder.amount_to_unwrap(weth.address()) - ); - } - - #[tokio::test] - async fn bulk_convert_if_weth_buffer_is_big_enough() { - let weth = dummy_contract!(WETH9, [0x42; 20]); - let weth_address = weth.address(); - let mut buffer_retriever = MockBufferRetrieving::new(); - buffer_retriever - .expect_get_buffers() - .returning(move |_| hashmap! {weth_address => Ok(to_wei(100))}); - - let mut settlement_simulator = MockSettlementSimulating::new(); - settlement_simulator - .expect_estimate_gas() - .times(1) - .returning(|_| Err(anyhow::anyhow!("simulation failed"))); - settlement_simulator - .expect_estimate_gas() - .times(1) - .returning(|_| Ok(Default::default())); - - let settlement = optimize_unwrapping( - settlement_with_unwrap(&weth, to_wei(10)), - &settlement_simulator, - &buffer_retriever, - &weth, - 0.6, - ) - .await; - - // we unwrap way more than needed to hopefully drop unwraps on the next few - // settlements - assert_eq!( - to_wei(60), - settlement.encoder.amount_to_unwrap(weth.address()) - ); - } - - #[tokio::test] - async fn leave_settlement_unchanged_if_buffers_are_too_small_for_optimizations() { - // Although we would have enough WETH to cover the ETH payout, we pretend the - // bulk unwrap would fail anyway. This can happen if the execution_plan - // of the settlement also tries to use the WETH buffer (In this case - // more than 10 WETH). - let mut settlement_simulator = MockSettlementSimulating::new(); - settlement_simulator - .expect_estimate_gas() - .times(2) - .returning(|_| Err(anyhow::anyhow!("simulation failed"))); - - let weth = dummy_contract!(WETH9, [0x42; 20]); - let weth_address = weth.address(); - let mut buffer_retriever = MockBufferRetrieving::new(); - buffer_retriever - .expect_get_buffers() - .returning(move |_| hashmap! {weth_address => Ok(to_wei(100))}); - - let eth_to_unwrap = to_wei(50); - - let settlement = optimize_unwrapping( - settlement_with_unwrap(&weth, eth_to_unwrap), - &settlement_simulator, - &buffer_retriever, - &weth, - 0.6, - ) - .await; - - // the settlement has been left unchanged - assert_eq!( - eth_to_unwrap, - settlement.encoder.amount_to_unwrap(weth.address()) - ); - } -} diff --git a/crates/solver/src/settlement_ranker.rs b/crates/solver/src/settlement_ranker.rs deleted file mode 100644 index 4b95922aa2..0000000000 --- a/crates/solver/src/settlement_ranker.rs +++ /dev/null @@ -1,315 +0,0 @@ -use { - crate::{ - driver::solver_settlements::{ - RatedSettlement, - {self}, - }, - metrics::{SolverMetrics, SolverRunOutcome, SolverSimulationOutcome}, - settlement::{PriceCheckTokens, Settlement}, - settlement_rater::{Rating, RatingError, ScoringError, SettlementRating}, - settlement_simulation::call_data, - solver::{SimulationWithError, Solver, SolverInfo}, - }, - anyhow::Result, - ethcontract::U256, - futures::future::join_all, - gas_estimation::GasPrice1559, - itertools::Itertools, - model::auction::AuctionId, - num::{rational::Ratio, BigInt}, - number::conversions::big_rational_to_u256, - rand::prelude::SliceRandom, - shared::{ - external_prices::ExternalPrices, - http_solver::model::{ - AuctionResult, - InternalizationStrategy, - SolverRejectionReason, - SolverRunError, - TransactionWithError, - }, - }, - std::sync::Arc, -}; - -type SolverResult = (Arc, Result, SolverRunError>); -pub type RatedSolverSettlement = (Arc, RatedSettlement); - -pub struct SettlementRanker { - pub metrics: Arc, - pub settlement_rater: Arc, - // TODO: these should probably come from the autopilot to make the test parameters identical - // for everyone. - pub max_settlement_price_deviation: Option>, - pub token_list_restriction_for_price_checks: PriceCheckTokens, - pub skip_non_positive_score_settlements: bool, -} - -impl SettlementRanker { - /// Discards settlements without user orders and settlements which violate - /// price checks. Logs info and updates metrics about the out come of - /// this run loop for each solver. - fn discard_illegal_settlements( - &self, - solver: &Arc, - settlements: Result, SolverRunError>, - external_prices: &ExternalPrices, - auction_id: AuctionId, - ) -> Vec { - let name = solver.name(); - match settlements { - Ok(settlements) => { - if settlements.is_empty() { - solver.notify_auction_result( - auction_id, - AuctionResult::Rejected(SolverRejectionReason::EmptySolution), - ); - } - let settlements: Vec<_> = settlements.into_iter().filter_map(|settlement| { - tracing::debug!(solver_name = %name, ?settlement, "found solution"); - - // Do not continue with settlements that are empty or only liquidity orders. - if !solver_settlements::has_user_order(&settlement) { - tracing::trace!( - solver_name = %name, - "settlement(s) filtered containing only liquidity orders", - ); - solver.notify_auction_result(auction_id, AuctionResult::Rejected(SolverRejectionReason::NoUserOrders)); - return None; - } - - // Do not continue with settlements that contain prices too different from external prices. - if let Some(max_settlement_price_deviation) = &self.max_settlement_price_deviation { - if ! - settlement.satisfies_price_checks( - solver.name(), - external_prices, - max_settlement_price_deviation, - &self.token_list_restriction_for_price_checks, - ) { - - tracing::debug!( - solver_name = %name, - "settlement(s) filtered for violating maximum external price deviation", - ); - - solver.notify_auction_result(auction_id, AuctionResult::Rejected(SolverRejectionReason::PriceViolation)); - return None; - } - } - - Some(settlement) - }).collect(); - - let outcome = match settlements.is_empty() { - true => SolverRunOutcome::Empty, - false => SolverRunOutcome::Success, - }; - self.metrics.solver_run(outcome, name); - settlements - } - Err(err) => { - let outcome = match err { - SolverRunError::Timeout => SolverRunOutcome::Timeout, - SolverRunError::Solving(_) => SolverRunOutcome::Failure, - }; - self.metrics.solver_run(outcome, name); - tracing::warn!(solver_name = %name, ?err, "solver error"); - solver.notify_auction_result( - auction_id, - AuctionResult::Rejected(SolverRejectionReason::RunError(err)), - ); - vec![] - } - } - } - - /// Computes a list of settlements which pass all pre-simulation sanity - /// checks. - fn get_legal_settlements( - &self, - settlements: Vec, - prices: &ExternalPrices, - auction_id: AuctionId, - ) -> Vec<(Arc, Settlement)> { - let mut solver_settlements = vec![]; - for (solver, settlements) in settlements { - let settlements = - self.discard_illegal_settlements(&solver, settlements, prices, auction_id); - for settlement in settlements { - solver_settlements.push((solver.clone(), settlement)); - } - } - solver_settlements - } - - /// Determines legal settlements and ranks them by simulating them. - /// Settlements get partitioned into simulation errors and a list - /// of `RatedSettlement`s sorted by ascending order of score. - pub async fn rank_legal_settlements( - &self, - settlements: Vec, - external_prices: &ExternalPrices, - gas_price: GasPrice1559, - auction_id: AuctionId, - ) -> Result<(Vec, Vec)> { - let solver_settlements = - self.get_legal_settlements(settlements, external_prices, auction_id); - - // log considered settlements. While we already log all found settlements, this - // additonal statement allows us to figure out which settlements were - // filtered out and which ones are going to be simulated and considered - // for competition. - for (solver, settlement) in &solver_settlements { - let uninternalized_calldata = format!( - "0x{}", - hex::encode(call_data( - settlement - .clone() - .encode(InternalizationStrategy::EncodeAllInteractions) - )), - ); - - tracing::debug!( - solver_name = %solver.name(), ?settlement, %uninternalized_calldata, - "considering solution for solver competition", - ); - } - - let (mut rated_settlements, failed_simulations): (Vec<_>, Vec<_>) = join_all( - solver_settlements.into_iter().enumerate().map( - |(i, (solver, settlement))| async move { - let simulation = self - .settlement_rater - .rate_settlement( - &SolverInfo { - account: solver.account().clone(), - name: solver.name().to_owned(), - }, - settlement, - external_prices, - gas_price, - i, - ) - .await; - (solver, simulation) - }, - ), - ) - .await - .into_iter() - .filter_map(|(solver, result)| match result { - Ok(res) => Some((solver, Rating::Ok(res))), - Err(err) => match err { - RatingError::FailedSimulation(error) => { - solver.notify_auction_result( - auction_id, - AuctionResult::Rejected(SolverRejectionReason::SimulationFailure( - TransactionWithError { - transaction: error.simulation.transaction.clone(), - error: error.error.to_string(), - }, - false, - )), - ); - Some((solver, Rating::Err(error))) - } - RatingError::FailedScoring(error) => { - tracing::debug!( - solver_name = %solver.name(), ?error, - "settlement filtered", - ); - let reason = match error { - ScoringError::ObjectiveValueNonPositive(_) => { - Some(SolverRejectionReason::ObjectiveValueNonPositiveLegacy) - } - ScoringError::SuccessProbabilityOutOfRange(_) => { - Some(SolverRejectionReason::SuccessProbabilityOutOfRange) - } - ScoringError::InternalError(_) => None, - }; - if let Some(reason) = reason { - solver.notify_auction_result(auction_id, AuctionResult::Rejected(reason)); - } - None - } - RatingError::Internal(error) => { - tracing::warn!(?error, "error in settlement rating logic"); - None - } - }, - }) - .partition_map(|(solver, result)| match result { - Rating::Ok(r) => itertools::Either::Left((solver, r)), - Rating::Err(err) => itertools::Either::Right((solver, err)), - }); - - tracing::info!( - "{} settlements passed simulation and {} failed", - rated_settlements.len(), - failed_simulations.len(), - ); - - // Filter out settlements with non-positive score. - if self.skip_non_positive_score_settlements { - rated_settlements.retain(|(solver, settlement)| { - let positive_score = settlement.score.score() > 0.into(); - if !positive_score { - tracing::debug!( - solver_name = %solver.name(), - "settlement filtered for having non-positive score", - ); - solver.notify_auction_result( - auction_id, - AuctionResult::Rejected(SolverRejectionReason::NonPositiveScore), - ); - self.metrics.settlement_non_positive_score(solver.name()); - } - positive_score - }); - } - - // Filter out settlements with too high score. - rated_settlements.retain(|(solver, settlement)| { - let surplus = big_rational_to_u256(&settlement.surplus).unwrap_or(U256::MAX); - let fees = big_rational_to_u256(&settlement.solver_fees).unwrap_or(U256::MAX); - let quality = surplus.saturating_add(fees); - let valid_score = settlement.score.score() < quality; - if !valid_score { - tracing::debug!( - solver_name = %solver.name(), - "settlement filtered for having too high score", - ); - solver.notify_auction_result( - auction_id, - AuctionResult::Rejected(SolverRejectionReason::ScoreHigherThanQuality { - score: settlement.score.score(), - quality, - }), - ); - } - valid_score - }); - - // Before sorting, make sure to shuffle the settlements. This is to make sure we - // don't give preference to any specific solver when there is a score tie. - rated_settlements.shuffle(&mut rand::thread_rng()); - rated_settlements.sort_by_key(|(_, settlement)| settlement.score.score()); - - rated_settlements - .iter_mut() - .rev() - .enumerate() - .for_each(|(i, (solver, settlement))| { - self.metrics - .settlement_simulation(solver.name(), SolverSimulationOutcome::Success); - settlement.ranking = i + 1; - solver.notify_auction_result(auction_id, AuctionResult::Ranked(i + 1)); - }); - let errors = failed_simulations - .into_iter() - .map(|(_, error)| error) - .collect(); - Ok((rated_settlements, errors)) - } -} diff --git a/crates/solver/src/settlement_rater.rs b/crates/solver/src/settlement_rater.rs index bf22586687..0c3b081c34 100644 --- a/crates/solver/src/settlement_rater.rs +++ b/crates/solver/src/settlement_rater.rs @@ -1,300 +1,11 @@ use { - crate::{ - driver::solver_settlements::RatedSettlement, - settlement::Settlement, - settlement_access_list::{estimate_settlement_access_list, AccessListEstimating}, - settlement_simulation::{ - call_data, - settle_method, - simulate_and_estimate_gas_at_current_block, - }, - settlement_submission::gas_limit_for_estimate, - solver::{Simulation, SimulationError, SimulationWithError, SolverInfo}, - }, anyhow::{anyhow, Context, Result}, - contracts::GPv2Settlement, - ethcontract::Account, - gas_estimation::GasPrice1559, - model::solver_competition::Score, num::{zero, BigRational, CheckedDiv, One}, number::conversions::big_rational_to_u256, primitive_types::U256, - shared::{ - code_fetching::CodeFetching, - ethrpc::Web3, - external_prices::ExternalPrices, - http_solver::{ - self, - model::{InternalizationStrategy, SimulatedTransaction}, - }, - }, - std::{borrow::Borrow, cmp::min, sync::Arc}, - web3::types::AccessList, + std::cmp::min, }; -type GasEstimate = U256; - -pub enum SimulateError { - FailedSimulation(SimulationWithError), - Internal(anyhow::Error), -} - -impl From for SimulateError { - fn from(error: anyhow::Error) -> Self { - Self::Internal(error) - } -} - -#[derive(Debug)] -pub enum RatingError { - FailedSimulation(SimulationWithError), - FailedScoring(ScoringError), - Internal(anyhow::Error), -} - -impl From for RatingError { - fn from(error: SimulateError) -> Self { - match error { - SimulateError::FailedSimulation(failure) => Self::FailedSimulation(failure), - SimulateError::Internal(error) => Self::Internal(error), - } - } -} - -impl From for RatingError { - fn from(error: ScoringError) -> Self { - Self::FailedScoring(error) - } -} -impl From for RatingError { - fn from(error: anyhow::Error) -> Self { - Self::Internal(error) - } -} - -pub enum Rating { - Ok(RatedSettlement), - Err(SimulationWithError), -} - -#[mockall::automock] -#[async_trait::async_trait] -pub trait SettlementRating: Send + Sync { - async fn rate_settlement( - &self, - solver: &SolverInfo, - settlement: Settlement, - prices: &ExternalPrices, - gas_price: GasPrice1559, - id: usize, - ) -> Result; -} - -pub struct SettlementRater { - pub access_list_estimator: Arc, - pub code_fetcher: Arc, - pub settlement_contract: GPv2Settlement, - pub web3: Web3, - pub score_calculator: ScoreCalculator, - pub consider_cost_failure: bool, -} - -impl SettlementRater { - async fn generate_access_list( - &self, - account: &Account, - settlement: &Settlement, - gas_price: GasPrice1559, - internalization: InternalizationStrategy, - ) -> Option { - let tx = settle_method( - gas_price, - &self.settlement_contract, - settlement.clone().encode(internalization), - account.clone(), - ) - .tx; - estimate_settlement_access_list( - self.access_list_estimator.borrow(), - self.code_fetcher.borrow(), - self.web3.clone(), - account.clone(), - settlement, - &tx, - ) - .await - .ok() - } - - /// Simulates the settlement and returns the gas used or the reason for a - /// revert. - async fn simulate_settlement( - &self, - solver: &SolverInfo, - settlement: &Settlement, - gas_price: GasPrice1559, - internalization: InternalizationStrategy, - ) -> Result<(Simulation, GasEstimate), SimulateError> { - let access_list = self - .generate_access_list(&solver.account, settlement, gas_price, internalization) - .await; - let block_number = self - .web3 - .eth() - .block_number() - .await - .context("failed to get block number")? - .as_u64(); - let simulation_result = simulate_and_estimate_gas_at_current_block( - std::iter::once(( - solver.account.clone(), - settlement.clone().encode(internalization), - access_list.clone(), - )), - &self.settlement_contract, - gas_price, - ) - .await - .context("failed to simulate settlements")? - .pop() - .expect("yields exactly 1 item"); - - let simulation = Simulation { - transaction: SimulatedTransaction { - internalization, - access_list, - // simulating on block X and tx index A is equal to simulating on block - // X+1 and tx index 0. - block_number: block_number + 1, - tx_index: 0, - to: self.settlement_contract.address(), - from: solver.account.address(), - data: call_data(settlement.clone().encode(internalization)), - max_fee_per_gas: U256::from_f64_lossy(gas_price.max_fee_per_gas), - max_priority_fee_per_gas: U256::from_f64_lossy(gas_price.max_priority_fee_per_gas), - }, - settlement: settlement.clone(), - solver: solver.clone(), - }; - - match simulation_result { - Ok(gas_estimate) => Ok((simulation, gas_estimate)), - Err(error) => Err(SimulateError::FailedSimulation(SimulationWithError { - simulation, - error: error.into(), - })), - } - } -} - -#[async_trait::async_trait] -impl SettlementRating for SettlementRater { - async fn rate_settlement( - &self, - solver: &SolverInfo, - settlement: Settlement, - prices: &ExternalPrices, - gas_price: GasPrice1559, - id: usize, - ) -> Result { - // first simulate settlements without internalizations to make sure they pass - let _ = self - .simulate_settlement( - solver, - &settlement, - gas_price, - InternalizationStrategy::EncodeAllInteractions, - ) - .await?; - - // since rating is done with internalizations, repeat the simulations for - // previously succeeded simulations - let (simulation, gas_estimate) = self - .simulate_settlement( - solver, - &settlement, - gas_price, - InternalizationStrategy::SkipInternalizableInteraction, - ) - .await?; - - let effective_gas_price = - BigRational::from_float(gas_price.effective_gas_price()).expect("Invalid gas price."); - - let solver_balance = self - .web3 - .eth() - .balance(solver.account.address(), None) - .await - .unwrap_or_default(); - - let gas_limit = gas_limit_for_estimate(gas_estimate); - let required_balance = - gas_limit.saturating_mul(U256::from_f64_lossy(gas_price.max_fee_per_gas)); - - if solver_balance < required_balance { - return Err(RatingError::FailedSimulation(SimulationWithError { - simulation, - error: SimulationError::InsufficientBalance { - needs: required_balance, - has: solver_balance, - }, - })); - } - - let earned_fees = settlement.total_earned_fees(prices); - let inputs = { - let gas_amount = match settlement.score { - http_solver::model::Score::RiskAdjusted { gas_amount, .. } => { - gas_amount.unwrap_or(gas_estimate) - } - _ => gas_estimate, - }; - crate::objective_value::Inputs::from_settlement( - &settlement, - prices, - effective_gas_price.clone(), - &gas_amount, - ) - }; - - let objective_value = inputs.objective_value(); - let score = match settlement.score { - http_solver::model::Score::Solver { score } => Score::Solver(score), - http_solver::model::Score::RiskAdjusted { - success_probability, - .. - } => { - let cost_fail = self - .consider_cost_failure - .then(|| inputs.gas_cost()) - .unwrap_or_else(zero); - Score::ProtocolWithSolverRisk(self.score_calculator.compute_score( - &objective_value, - cost_fail, - success_probability, - )?) - } - }; - - let rated_settlement = RatedSettlement { - id, - settlement, - surplus: inputs.surplus_given, - earned_fees, - solver_fees: inputs.solver_fees, - // save simulation gas estimate even if the solver provided gas amount - // it's safer and more accurate since simulation gas estimate includes pre/post hooks - gas_estimate, - gas_price: effective_gas_price, - objective_value, - score, - ranking: Default::default(), - }; - Ok(rated_settlement) - } -} - #[derive(Debug)] pub enum ScoringError { ObjectiveValueNonPositive(BigRational), diff --git a/crates/solver/src/settlement_simulation.rs b/crates/solver/src/settlement_simulation.rs index 78d111719a..88a3358a64 100644 --- a/crates/solver/src/settlement_simulation.rs +++ b/crates/solver/src/settlement_simulation.rs @@ -1,25 +1,18 @@ use { - anyhow::{anyhow, Context, Error, Result}, + anyhow::Result, contracts::GPv2Settlement, ethcontract::{ - batch::CallBatch, contract::MethodBuilder, dyns::{DynMethodBuilder, DynTransport}, errors::ExecutionError, transaction::TransactionBuilder, Account, }, - ethrpc::Web3, - futures::FutureExt, gas_estimation::GasPrice1559, itertools::Itertools, - primitive_types::{H160, H256, U256}, - shared::{ - conversions::into_gas_price, - encoded_settlement::EncodedSettlement, - tenderly_api::{SimulationRequest, TenderlyApi}, - }, - web3::types::{AccessList, BlockId}, + primitive_types::U256, + shared::{conversions::into_gas_price, encoded_settlement::EncodedSettlement}, + web3::types::AccessList, }; const SIMULATE_BATCH_SIZE: usize = 10; @@ -55,108 +48,7 @@ pub async fn simulate_and_estimate_gas_at_current_block( Ok(results) } -pub async fn simulate_and_error_with_tenderly_link( - settlements: impl Iterator)>, - contract: &GPv2Settlement, - web3: &Web3, - gas_price: GasPrice1559, - network_id: &str, - block: u64, - simulation_gas_limit: u128, -) -> Vec> { - let mut batch = CallBatch::new(web3.transport()); - let futures = settlements - .map(|(account, settlement, access_list)| { - let method = settle_method(gas_price, contract, settlement, account); - let method = match access_list { - Some(access_list) => method.access_list(access_list), - None => method, - }; - let transaction_builder = method.tx.clone(); - let view = method - .view() - .block(BlockId::Number(block.into())) - // Since we now supply the gas price for the simulation, make sure to also - // set a gas limit so we don't get failed simulations because of insufficient - // solver balance. The limit should be below the current block gas - // limit of 30M gas - .gas(simulation_gas_limit.into()); - (view.batch_call(&mut batch), transaction_builder) - }) - .collect::>(); - batch.execute_all(SIMULATE_BATCH_SIZE).await; - - futures - .into_iter() - .map(|(future, transaction_builder)| { - future.now_or_never().unwrap().map(|_| ()).map_err(|err| { - Error::new(err).context(tenderly_link( - block, - network_id, - transaction_builder, - Some(gas_price), - None, - )) - }) - }) - .collect() -} - -pub async fn simulate_before_after_access_list( - web3: &Web3, - tenderly: &dyn TenderlyApi, - network_id: String, - transaction_hash: H256, -) -> Result { - let transaction = web3 - .eth() - .transaction(transaction_hash.into()) - .await? - .context("no transaction found")?; - - if transaction.access_list.is_none() { - return Err(anyhow!( - "no need to analyze access list since no access list was found in mined transaction" - )); - } - - let (block_number, from, to, transaction_index) = ( - transaction - .block_number - .context("no block number field exist")? - .as_u64(), - transaction.from.context("no from field exist")?, - transaction.to.context("no to field exist")?, - transaction - .transaction_index - .context("no transaction_index field exist")? - .as_u64(), - ); - - let request = SimulationRequest { - network_id, - block_number: Some(block_number), - transaction_index: Some(transaction_index), - from, - input: transaction.input.0, - to, - gas: Some(transaction.gas.as_u64()), - ..Default::default() - }; - - let gas_used_without_access_list = tenderly.simulate(request).await?.transaction.gas_used; - let gas_used_with_access_list = web3 - .eth() - .transaction_receipt(transaction_hash) - .await? - .context("no transaction receipt")? - .gas_used - .context("no gas used field")?; - - Ok(gas_used_without_access_list as f64 - gas_used_with_access_list.to_f64_lossy()) -} - -pub fn settle_method( +fn settle_method( gas_price: GasPrice1559, contract: &GPv2Settlement, settlement: EncodedSettlement, @@ -180,19 +72,6 @@ pub fn settle_method_builder( .from(from) } -/// The call data of a settle call with this settlement. -pub fn call_data(settlement: EncodedSettlement) -> Vec { - let contract = GPv2Settlement::at(ðrpc::dummy::web3(), H160::default()); - let method = contract.settle( - settlement.tokens, - settlement.clearing_prices, - settlement.trades, - settlement.interactions, - ); - // Unwrap because there should always be calldata. - method.tx.data.unwrap().0 -} - // Creates a simulation link in the gp-v2 tenderly workspace pub fn tenderly_link( current_block: u64, @@ -245,40 +124,10 @@ pub fn tenderly_link( mod tests { use { super::*, - crate::{ - interactions::allowances::{Allowances, MockAllowanceManaging}, - liquidity::{ - balancer_v2::SettlementHandler, - order_converter::OrderConverter, - slippage::SlippageContext, - uniswap_v2::Inner, - ConstantProductOrder, - Liquidity, - StablePoolOrder, - }, - order_balance_filter::BalancedOrder, - settlement::Settlement, - solver::http_solver::settlement::{convert_settlement, SettlementContext}, - }, - contracts::{BalancerV2Vault, IUniswapLikeRouter, UniswapV2Router02, WETH9}, + crate::settlement::Settlement, ethcontract::{Account, PrivateKey}, - maplit::{btreemap, hashmap}, - model::{order::Order, TokenPair}, - num::rational::Ratio, - serde_json::json, - shared::{ - ethrpc::create_env_test_transport, - http_solver::model::{InternalizationStrategy, SettledBatchAuctionModel}, - sources::balancer_v2::{ - pools::{common::TokenState, stable::AmplificationParameter}, - swap::fixed_point::Bfp, - }, - tenderly_api::TenderlyHttpApi, - }, - std::{ - str::FromStr, - sync::{Arc, Mutex}, - }, + shared::{ethrpc::create_env_test_transport, http_solver::model::InternalizationStrategy}, + web3::Web3, }; // cargo test -p solver settlement_simulation::tests::mainnet -- --ignored @@ -293,8 +142,6 @@ mod tests { ); let transport = create_env_test_transport(); let web3 = Web3::new(transport); - let block = web3.eth().block_number().await.unwrap().as_u64(); - let network_id = web3.net().version().await.unwrap(); let contract = GPv2Settlement::deployed(&web3).await.unwrap(); let account = Account::Offline(PrivateKey::from_raw([1; 32]).unwrap(), None); @@ -312,17 +159,6 @@ mod tests { None, ), ]; - let result = simulate_and_error_with_tenderly_link( - settlements.iter().cloned(), - &contract, - &web3, - Default::default(), - network_id.as_str(), - block, - 15000000u128, - ) - .await; - let _ = dbg!(result); let result = simulate_and_estimate_gas_at_current_block( settlements.iter().cloned(), @@ -343,374 +179,6 @@ mod tests { let _ = dbg!(result); } - // cargo test - // decode_quasimodo_solution_with_liquidity_orders_and_simulate_onchain_tx -- - // --ignored --nocapture - #[tokio::test] - #[ignore] - async fn decode_quasimodo_solution_with_liquidity_orders_and_simulate_onchain_tx() { - // This e2e test re-simulates the settlement from here: https://etherscan.io/tx/0x6756c294eb84c899247f2ec64d6eee73e7aaf50d6cb49ba9bab636f450240f51 - // This settlement was wrongly settled, because the liquidity order did receive - // a surplus. The liquidity order is: - // https://explorer.cow.fi/orders/0x4da985bb7639bdac928553d0c39a3840388e27f825c572bb8addb47ef2de1f03e63a13eedd01b624958acfe32145298788a7a7ba61be1542 - - let transport = create_env_test_transport(); - let web3 = Web3::new(transport); - let native_token_contract = WETH9::deployed(&web3) - .await - .expect("couldn't load deployed native token"); - let network_id = web3.net().version().await.unwrap(); - let contract = GPv2Settlement::deployed(&web3).await.unwrap(); - let uniswap_router = UniswapV2Router02::deployed(&web3).await.unwrap(); - let balancer_vault = BalancerV2Vault::deployed(&web3).await.unwrap(); - - let account = Account::Local( - "0xa6DDBD0dE6B310819b49f680F65871beE85f517e" - .parse() - .unwrap(), - None, - ); - let order_converter = OrderConverter { - native_token: native_token_contract.clone(), - }; - let value = json!( - { - "creationDate": "2021-12-18T17:06:05.425889Z", - "owner": "0x295a0bc540f3d9a9bd67a777ca9da9fb5619d3a9", - "uid": "0x721f9c5c4bbadeff130c4b0279951a2703c91ccc440cd64acb6b11caba0c64e9295a0bc540f3d9a9bd67a777ca9da9fb5619d3a961be1c03", - "availableBalance": "9437822596", - "executedBuyAmount": "0", - "executedSellAmount": "0", - "executedSellAmountBeforeFees": "0", - "executedFeeAmount": "0", - "invalidated": false, - "sellToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "buyToken": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "receiver": "0x295a0bc540f3d9a9bd67a777ca9da9fb5619d3a9", - "sellAmount": "9413614019", - "buyAmount": "2377438739352985079", - "validTo": 1639848963u32, - "appData": "0x487b02c558d729abaf3ecf17881a4181e5bc2446429a0995142297e897b6eb37", - "feeAmount": "24208577", - "fullFeeAmount": "49817596", - "kind": "sell", - "partiallyFillable": false, - "signature": "0x50afa71e17cc7b1a7d5debf74b1baeebc9724539f92d056d58b0c1f95e19ef626583f3e9fe9ebc16324c28f88f80029ff04aa1d402972bfeacf93e052d3250ef1c", - "signingScheme": "eip712", - "status": "open", - "settlementContract": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", - "sellTokenBalance": "erc20", - "buyTokenBalance": "erc20", - "isLiquidityOrder": false, - "class": "market", - }); - let order0: Order = serde_json::from_value(value).unwrap(); - let value = json!( - { - "creationDate": "2021-12-24T05:02:18.624125Z", - "owner": "0xe63a13eedd01b624958acfe32145298788a7a7ba", - "uid": "0x4da985bb7639bdac928553d0c39a3840388e27f825c572bb8addb47ef2de1f03e63a13eedd01b624958acfe32145298788a7a7ba61be1542", - "availableBalance": "106526950853", - "executedBuyAmount": "0", - "executedSellAmount": "0", - "executedSellAmountBeforeFees": "0", - "executedFeeAmount": "0", - "invalidated": false, - "sellToken": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "buyToken": "0xdac17f958d2ee523a2206206994597c13d831ec7", - "receiver": "0xe63a13eedd01b624958acfe32145298788a7a7ba", - "sellAmount": "11722136152", - "buyAmount": "11727881818", - "validTo": 1639847234u32, - "appData": "0x00000000000000000000000055662e225a3376759c24331a9aed764f8f0c9fbb", - "feeAmount": "3400559", - "fullFeeAmount": "49915997", - "kind": "buy", - "partiallyFillable": false, - "signature": "0x0701b6c9c5314b4d446229ba2940b6f2ad7600ff6579a77627d50307528c2f2d53fb9c889ba3f566eddb34b42e3ad0a2604b0b497b33af5c24927773912d05601c", - "signingScheme": "ethsign", - "status": "open", - "settlementContract": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", - "sellTokenBalance": "erc20", - "buyTokenBalance": "erc20", - "isLiquidityOrder": true, - "class": "liquidity", - }); - let order1: Order = serde_json::from_value(value).unwrap(); - let value = json!( - { - "creationDate": "2021-12-18T16:46:41.271735Z", - "owner": "0xf105e7d4dc8b1592e806a36c3b351a8b63b5c07c", - "uid": "0x9a6986670e989c0bd4049d983ad574c2a8e8bdd6dd91473e197c2539caf8e025f105e7d4dc8b1592e806a36c3b351a8b63b5c07c61be1776", - "availableBalance": "380000000000000000", - "executedBuyAmount": "0", - "executedSellAmount": "0", - "executedSellAmountBeforeFees": "0", - "executedFeeAmount": "0", - "invalidated": false, - "sellToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buyToken": "0xdac17f958d2ee523a2206206994597c13d831ec7", - "receiver": "0xf105e7d4dc8b1592e806a36c3b351a8b63b5c07c", - "sellAmount": "372267382796377048", - "buyAmount": "1475587283", - "validTo": 1639847798u32, - "appData": "0x487b02c558d729abaf3ecf17881a4181e5bc2446429a0995142297e897b6eb37", - "feeAmount": "7732617203622952", - "fullFeeAmount": "14232617203622952", - "kind": "sell", - "partiallyFillable": false, - "signature": "0xaae201933a47b6e9d88ddeded8a64b11a4bcaa4e307263447af860a79301930d6c25b16e6ed15fa2e5a233be1e71e60acdd7fec6a52861dcf21d9c4720e1a2c01b", - "signingScheme": "eip712", - "status": "open", - "settlementContract": "0x9008d19f58aabd9ed0d60971565aa8510560ab41", - "sellTokenBalance": "erc20", - "buyTokenBalance": "erc20", - "isLiquidityOrder": false, - "class": "market", - }); - let order2: Order = serde_json::from_value(value).unwrap(); - - let orders = vec![order0, order1, order2]; - let orders = orders - .into_iter() - .map(|order| { - order_converter - .normalize_limit_order(BalancedOrder::full(order)) - .unwrap() - }) - .collect::>(); - - let cpo_0 = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new( - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - .parse() - .unwrap(), - "0xdac17f958d2ee523a2206206994597c13d831ec7" - .parse() - .unwrap(), - ) - .unwrap(), - reserves: (12779103255415685792803, 50331174049111), - fee: Ratio::new(3, 1000), - settlement_handling: Arc::new(Inner::new( - IUniswapLikeRouter::at(&web3, uniswap_router.address()), - contract.clone(), - Mutex::new(Allowances::new( - contract.address(), - hashmap! {uniswap_router.address() => U256::from_dec_str("18000000000000000000000000").unwrap()}, - )), - )), - }; - - let spo = StablePoolOrder { - address: H160::from_low_u64_be(1), - reserves: btreemap! { - "0x6b175474e89094c44da98b954eedeac495271d0f".parse().unwrap() => TokenState { - balance: 46_543_572_661_097_157_184_873_466_u128.into(), - scaling_factor: Bfp::exp10(0), - }, - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".parse().unwrap() => TokenState { - balance: 50_716_887_827_666_u128.into(), - scaling_factor: Bfp::exp10(12), - }, - "0xdac17f958d2ee523a2206206994597c13d831ec7".parse().unwrap() => TokenState{ - balance: 38_436_050_628_181_u128.into(), - scaling_factor: Bfp::exp10(12), - }, - }, - fee: "0.001".parse().unwrap(), - amplification_parameter: AmplificationParameter::new(1573.into(), 1.into()).unwrap(), - settlement_handling: Arc::new(SettlementHandler::new( - "0x06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063" - .parse() - .unwrap(), - contract.clone(), - balancer_vault, - Allowances::new( - contract.address(), - hashmap! {"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".parse().unwrap()=> U256::from_dec_str("18000000000000000000000000").unwrap()}, - ), - )), - }; - - let liquidity = vec![ - Liquidity::ConstantProduct(cpo_0), - Liquidity::BalancerStable(spo), - ]; - let settlement_context = SettlementContext { orders, liquidity }; - let quasimodo_response = r#" { - "amms": { - "1": { - "amplification_parameter": "1573.000000", - "cost": { - "amount": "7174016181720000", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "execution": [ - { - "buy_token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "exec_buy_amount": "21135750171", - "exec_plan": { - "sequence": 0, - "position": 0, - "internal": false - }, - "exec_sell_amount": "21129728791", - "sell_token": "0xdac17f958d2ee523a2206206994597c13d831ec7" - } - ], - "fee": "0.000100", - "kind": "Stable", - "mandatory": false, - "reserves": { - "0x6b175474e89094c44da98b954eedeac495271d0f": "46543572661097157184873466", - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "50716887827666", - "0xdac17f958d2ee523a2206206994597c13d831ec7": "38436050628181" - } - }, - "0": { - "cost": { - "amount": "5661255302867976", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "execution": [ - { - "buy_token": "0xdac17f958d2ee523a2206206994597c13d831ec7", - "exec_buy_amount": "7922480269", - "exec_plan": { - "sequence": 0, - "position": 1, - "internal": false - }, - "exec_sell_amount": "2005171356556612050", - "sell_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - } - ], - "fee": "0.003000", - "kind": "ConstantProduct", - "mandatory": false, - "reserves": { - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "1.277910e+22", - "0xdac17f958d2ee523a2206206994597c13d831ec7": "5.033117e+13" - } - } - }, - "metadata": { - "has_solution": true, - "result": "Optimal", - "total_runtime": 0.247607875 - }, - "orders": { - "0": { - "allow_partial_fill": false, - "buy_amount": "2377438739352985079", - "buy_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "cost": { - "amount": "3964540692423015", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "exec_buy_amount": "2377438739352985079", - "exec_sell_amount": "9413614019", - "fee": { - "amount": "49817596", - "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - }, - "is_liquidity_order": false, - "is_sell_order": true, - "mandatory": false, - "sell_amount": "9413614019", - "sell_token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - }, - "1": { - "allow_partial_fill": false, - "buy_amount": "11727881818", - "buy_token": "0xdac17f958d2ee523a2206206994597c13d831ec7", - "cost": { - "amount": "3964540692423015", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "exec_buy_amount": "11727881818", - "exec_sell_amount": "11722136152", - "fee": { - "amount": "49915997", - "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - }, - "is_liquidity_order": true, - "is_sell_order": false, - "mandatory": false, - "sell_amount": "11722136152", - "sell_token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - }, - "2": { - "allow_partial_fill": false, - "buy_amount": "1475587283", - "buy_token": "0xdac17f958d2ee523a2206206994597c13d831ec7", - "cost": { - "amount": "3964540692423015", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "exec_buy_amount": "1479366704", - "exec_sell_amount": "372267382796377048", - "fee": { - "amount": "14232617203622952", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "is_liquidity_order": false, - "is_sell_order": true, - "mandatory": false, - "sell_amount": "372267382796377048", - "sell_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - } - }, - "prices": { - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "252553241991277046325219637", - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "1000000000000000000", - "0xdac17f958d2ee523a2206206994597c13d831ec7": "251639692692125716448920105" - }, - "ref_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "tokens": { - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": { - "alias": "USDC", - "decimals": 6 - }, - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": { - "alias": "WETH", - "decimals": 18 - }, - "0xdac17f958d2ee523a2206206994597c13d831ec7": { - "alias": "USDT", - "decimals": 6 - } - } - } - "#; - let parsed_response = serde_json::from_str::(quasimodo_response); - - let settlements = convert_settlement( - parsed_response.unwrap(), - &settlement_context, - Arc::new(MockAllowanceManaging::new()), - &OrderConverter::test(H160([0x42; 20])), - SlippageContext::default(), - &Default::default(), - true, - ) - .await - .map(|settlement| vec![settlement]) - .unwrap(); - let settlement = settlements.first().unwrap(); - let settlement_encoded = settlement - .clone() - .encode(InternalizationStrategy::SkipInternalizableInteraction); - println!("Settlement_encoded: {settlement_encoded:?}"); - let settlement = settle_method_builder(&contract, settlement_encoded, account).tx; - println!( - "Tenderly simulation for generated tx: {:?}", - tenderly_link(13830346u64, &network_id, settlement, None, None) - ); - } - // cargo test -p solver settlement_simulation::tests::mainnet_chunked -- // --ignored --nocapture #[tokio::test] @@ -744,32 +212,4 @@ mod tests { .unwrap(); let _ = dbg!(result); } - - #[tokio::test] - #[ignore] - async fn simulate_before_after_access_list_test() { - let transport = create_env_test_transport(); - let web3 = Web3::new(transport); - let transaction_hash = - H256::from_str("e337fcd52afd6b98847baab279cda6c3980fcb185da9e959fd489ffd210eac60") - .unwrap(); - let tenderly_api = TenderlyHttpApi::test_from_env(); - let gas_saved = simulate_before_after_access_list( - &web3, - &*tenderly_api, - "1".to_string(), - transaction_hash, - ) - .await - .unwrap(); - - dbg!(gas_saved); - } - - #[test] - fn calldata_works() { - let settlement = EncodedSettlement::default(); - let data = call_data(settlement); - assert!(!data.is_empty()); - } } diff --git a/crates/solver/src/settlement_submission.rs b/crates/solver/src/settlement_submission.rs index 24a422b750..0f16249450 100644 --- a/crates/solver/src/settlement_submission.rs +++ b/crates/solver/src/settlement_submission.rs @@ -27,7 +27,6 @@ use { }, submitter::{ DisabledReason, - Strategy, Submitter, SubmitterGasPriceEstimator, SubmitterParams, @@ -50,8 +49,7 @@ pub fn gas_limit_for_estimate(gas_estimate: U256) -> U256 { } #[derive(Debug)] -pub struct SubTxPool { - pub strategy: Strategy, +struct SubTxPool { // Key (Address, U256) represents pair (sender, nonce) pub pools: HashMap<(Address, U256), Vec<(TransactionHandle, GasPrice1559)>>, } @@ -59,17 +57,16 @@ type TxPool = Arc>>; #[derive(Debug, Default, Clone)] pub struct GlobalTxPool { - pub pools: TxPool, + pools: TxPool, } impl GlobalTxPool { - pub fn add_sub_pool(&self, strategy: Strategy) -> SubTxPoolRef { + pub fn add_sub_pool(&self) -> SubTxPoolRef { let pools = self.pools.clone(); let index = { let mut pools = pools.lock().unwrap(); let index = pools.len(); pools.push(SubTxPool { - strategy, pools: Default::default(), }); index @@ -519,7 +516,7 @@ mod tests { let nonce = U256::zero(); let transactions: Vec<(TransactionHandle, GasPrice1559)> = Default::default(); - let submitted_transactions = GlobalTxPool::default().add_sub_pool(Strategy::PublicMempool); + let submitted_transactions = GlobalTxPool::default().add_sub_pool(); submitted_transactions.update(sender, nonce, transactions); let entry = submitted_transactions.get(sender, nonce); diff --git a/crates/solver/src/solver.rs b/crates/solver/src/solver.rs index fa4582b78d..e285094723 100644 --- a/crates/solver/src/solver.rs +++ b/crates/solver/src/solver.rs @@ -1,291 +1,10 @@ use { - self::{ - baseline_solver::BaselineSolver, - http_solver::HttpSolver, - naive_solver::NaiveSolver, - oneinch_solver::OneInchSolver, - optimizing_solver::OptimizingSolver, - paraswap_solver::ParaswapSolver, - single_order_solver::{SingleOrderSolver, SingleOrderSolving}, - zeroex_solver::ZeroExSolver, - }, - crate::{ - interactions::allowances::AllowanceManager, - liquidity::{ - order_converter::OrderConverter, - slippage::{self, SlippageCalculator}, - LimitOrder, - Liquidity, - }, - metrics::SolverMetrics, - s3_instance_upload::S3InstanceUploader, - settlement::Settlement, - settlement_post_processing::PostProcessing, - settlement_rater::SettlementRating, - solver::{ - balancer_sor_solver::BalancerSorSolver, - http_solver::{ - buffers::BufferRetriever, - instance_cache::SharedInstanceCreator, - instance_creation::InstanceCreator, - InstanceType, - }, - }, - }, - anyhow::{anyhow, Context, Result}, - contracts::{BalancerV2Vault, GPv2Settlement, WETH9}, - ethcontract::{errors::ExecutionError, transaction::kms, Account, PrivateKey, H160, U256}, - ethrpc::current_block::CurrentBlockStream, - futures::future::join_all, - model::{auction::AuctionId, order::Order, DomainSeparator}, - reqwest::Url, - shared::{ - account_balances, - balancer_sor_api::DefaultBalancerSorApi, - baseline_solver::BaseTokens, - ethrpc::Web3, - external_prices::ExternalPrices, - http_client::HttpClientFactory, - http_solver::{ - model::{AuctionResult, SimulatedTransaction}, - DefaultHttpSolverApi, - SolverConfig, - }, - token_info::TokenInfoFetching, - token_list::AutoUpdatingTokenList, - zeroex_api::ZeroExApi, - }, - std::{ - collections::HashMap, - fmt::{self, Debug, Formatter}, - str::FromStr, - sync::Arc, - time::{Duration, Instant}, - }, - web3::types::AccessList, + anyhow::anyhow, + std::{fmt::Debug, str::FromStr}, }; -pub mod balancer_sor_solver; mod baseline_solver; -pub mod http_solver; pub mod naive_solver; -mod oneinch_solver; -pub mod optimizing_solver; -mod paraswap_solver; -pub mod risk_computation; -pub mod single_order_solver; -mod zeroex_solver; - -/// Interface that all solvers must implement. -/// -/// A `solve` method transforming a collection of `Liquidity` (sources) into a -/// list of independent `Settlements`. Solvers are free to choose which types -/// `Liquidity` they would like to process, including their own private sources. -#[mockall::automock] -#[async_trait::async_trait] -pub trait Solver: Send + Sync + 'static { - /// Runs the solver. - /// - /// The returned settlements should be independent (for example not reusing - /// the same user order) so that they can be merged by the driver at its - /// leisure. - /// - /// id identifies this instance of solving by the driver in which it invokes - /// all solvers. - async fn solve(&self, auction: Auction) -> Result>; - - /// Callback to notify the solver how it performed in the given auction (if - /// it won or failed for some reason) Has to be non-blocking to not - /// delay settling the actual solution - fn notify_auction_result(&self, _auction_id: AuctionId, _result: AuctionResult) {} - - /// Returns solver's account that should be used to submit settlements. - fn account(&self) -> &Account; - - /// Returns displayable name of the solver. - /// - /// This method is used for logging and metrics collection. - fn name(&self) -> &str; -} - -/// A batch auction for a solver to produce a settlement for. -#[derive(Clone, Debug)] -pub struct Auction { - /// Note that multiple consecutive driver runs may use the same ID if the - /// previous run was unable to find a settlement. - pub id: AuctionId, - - /// An ID that identifies a driver run. - /// - /// Note that this ID is not unique across multiple instances of drivers, - /// in particular it cannot be used to uniquely identify batches across - /// service restarts. - pub run: u64, - - /// The GPv2 orders to match. - pub orders: Vec, - - /// The baseline on-chain liquidity that can be used by the solvers for - /// settling orders. - pub liquidity: Vec, - - /// On which block the liquidity got fetched. - pub liquidity_fetch_block: u64, - - /// The current gas price estimate. - pub gas_price: f64, - - /// The deadline for computing a solution. - /// - /// This can be used internally for the solver to decide when to stop - /// trying to optimize the settlement. The caller is expected poll the solve - /// future at most until the deadline is reach, at which point the future - /// will be dropped. - pub deadline: Instant, - - /// The set of external prices for this auction. - /// - /// The objective value is calculated with these prices so they can be - /// relevant for solvers. - /// - /// External prices are garanteed to exist for all orders included in the - /// current auction. - pub external_prices: ExternalPrices, - - /// Balances for `orders`. Not guaranteed to have an entry for all orders - /// because balance fetching can fail. - pub balances: HashMap, -} - -impl Default for Auction { - fn default() -> Self { - const SECONDS_IN_A_YEAR: u64 = 31_622_400; - - // Not actually never, but good enough... - let never = Instant::now() + Duration::from_secs(SECONDS_IN_A_YEAR); - Self { - id: Default::default(), - run: Default::default(), - orders: Default::default(), - liquidity: Default::default(), - liquidity_fetch_block: Default::default(), - gas_price: Default::default(), - deadline: never, - external_prices: Default::default(), - balances: Default::default(), - } - } -} - -/// A vector of solvers. -pub type Solvers = Vec>; - -/// A single settlement and a solver that produced it. -pub type SettlementWithSolver = (Arc, Settlement, Option); - -#[derive(Debug, Clone)] -pub struct SolverInfo { - /// Identifier used for metrics and logging. - pub name: String, - /// Address used for simulating settlements of that solver. - pub account: Account, -} - -#[derive(Debug)] -pub struct Simulation { - pub settlement: Settlement, - pub solver: SolverInfo, - pub transaction: SimulatedTransaction, -} - -#[derive(Debug)] -pub struct SimulationWithError { - pub simulation: Simulation, - pub error: SimulationError, -} - -#[derive(Debug, thiserror::Error)] -pub enum SimulationError { - #[error("web3 error: {0:?}")] - Web3(#[from] ExecutionError), - #[error("insufficient balance: needs {needs} has {has}")] - InsufficientBalance { needs: U256, has: U256 }, -} - -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, clap::ValueEnum)] -#[clap(rename_all = "verbatim")] -pub enum SolverType { - None, - Naive, - Baseline, - OneInch, - Paraswap, - ZeroEx, - Quasimodo, - BalancerSor, -} - -#[derive(Clone)] -pub enum SolverAccountArg { - PrivateKey(PrivateKey), - Kms(Arn), - Address(H160), -} - -impl Debug for SolverAccountArg { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - SolverAccountArg::PrivateKey(k) => write!(f, "PrivateKey({:?})", k.public_address()), - SolverAccountArg::Kms(key_id) => write!(f, "KMS({key_id:?})"), - SolverAccountArg::Address(a) => write!(f, "Address({a:?})"), - } - } -} - -impl SolverAccountArg { - pub async fn into_account(self, chain_id: u64) -> Account { - match self { - SolverAccountArg::PrivateKey(key) => Account::Offline(key, Some(chain_id)), - SolverAccountArg::Kms(key_id) => { - let config = ethcontract::aws_config::load_from_env().await; - let account = kms::Account::new((&config).into(), &key_id.0) - .await - .unwrap_or_else(|_| panic!("Unable to load KMS account {key_id:?}")); - Account::Kms(account, Some(chain_id)) - } - SolverAccountArg::Address(address) => Account::Local(address, None), - } - } -} - -impl FromStr for SolverAccountArg { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - s.parse::() - .map(SolverAccountArg::PrivateKey) - .map_err(|pk_err| anyhow!("could not parse as private key: {}", pk_err)) - .or_else(|error_chain| { - Ok(SolverAccountArg::Address(s.parse().map_err( - |addr_err| { - error_chain.context(anyhow!("could not parse as address: {}", addr_err)) - }, - )?)) - }) - .or_else(|error_chain: Self::Err| { - let key_id = Arn::from_str(s).map_err(|arn_err| { - error_chain.context(anyhow!("could not parse as AWS ARN: {}", arn_err)) - })?; - Ok(SolverAccountArg::Kms(key_id)) - }) - .map_err(|err: Self::Err| { - err.context( - "invalid solver account, it is neither a private key, an Ethereum address, \ - nor a KMS key", - ) - }) - } -} // Wrapper type for AWS ARN identifiers #[derive(Debug, Clone)] @@ -304,454 +23,3 @@ impl FromStr for Arn { } } } - -#[derive(Clone, Debug)] -pub struct ExternalSolverArg { - pub name: String, - pub url: Url, - pub account: SolverAccountArg, - pub use_liquidity: bool, - pub user_balance_support: UserBalanceSupport, -} - -/// Whether the solver supports assigning user sell token balance to orders or -/// whether the driver needs to do it instead. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum UserBalanceSupport { - None, - PartiallyFillable, - // Will be added later. - // All, -} - -impl FromStr for UserBalanceSupport { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "none" => Ok(Self::None), - "partially_fillable" => Ok(Self::PartiallyFillable), - _ => Err(anyhow::anyhow!("unknown variant {}", s)), - } - } -} - -impl FromStr for ExternalSolverArg { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let mut parts = s.split('|'); - let name = parts.next().context("missing name")?; - let url = parts.next().context("missing url")?; - let account = parts.next().context("missing account")?; - let use_liquidity = parts.next().context("missing use_liquidity")?; - // With a default temporarily until we configure the argument in our cluster. - let user_balance_support = parts.next().unwrap_or("none"); - Ok(Self { - name: name.to_string(), - url: url.parse().context("parse url")?, - account: account.parse().context("parse account")?, - use_liquidity: use_liquidity.parse().context("parse use_liquidity")?, - user_balance_support: user_balance_support - .parse() - .context("parse user_balance_support")?, - }) - } -} - -#[allow(clippy::too_many_arguments)] -pub async fn create( - web3: Web3, - solvers: Vec<(Account, SolverType)>, - base_tokens: Arc, - native_token: WETH9, - quasimodo_solver_url: Url, - balancer_sor_url: Url, - settlement_contract: &GPv2Settlement, - vault_contract: Option<&BalancerV2Vault>, - token_info_fetcher: Arc, - network_id: String, - chain_id: u64, - disabled_one_inch_protocols: Vec, - disabled_paraswap_dexs: Vec, - paraswap_partner: Option, - paraswap_api_url: String, - http_factory: &HttpClientFactory, - solver_metrics: Arc, - zeroex_api: Arc, - zeroex_disabled_sources: Vec, - zeroex_enable_rfqt: bool, - zeroex_enable_slippage_protection: bool, - use_internal_buffers: bool, - one_inch_url: Url, - one_inch_referrer_address: Option, - external_solvers: Vec, - order_converter: OrderConverter, - max_settlements_per_solver: usize, - max_merged_settlements: usize, - smallest_partial_fill: U256, - slippage_configuration: &slippage::Arguments, - market_makable_token_list: AutoUpdatingTokenList, - order_prioritization_config: &single_order_solver::Arguments, - post_processing_pipeline: Arc, - domain: &DomainSeparator, - s3_instance_uploader: Option>, - risk_configuration: &risk_computation::Arguments, - settlement_rater: Arc, - enforce_correct_fees: bool, - ethflow_contract: Option, - current_block_stream: CurrentBlockStream, -) -> Result { - // Tiny helper function to help out with type inference. Otherwise, all - // `Box::new(...)` expressions would have to be cast `as Box`. - fn shared(solver: impl Solver + 'static) -> Arc { - Arc::new(solver) - } - - let buffer_retriever = Arc::new(BufferRetriever::new( - web3.clone(), - settlement_contract.address(), - )); - let allowance_manager = Arc::new(AllowanceManager::new( - web3.clone(), - settlement_contract.address(), - )); - let instance_creator = InstanceCreator { - native_token, - ethflow_contract, - token_info_fetcher: token_info_fetcher.clone(), - buffer_retriever, - market_makable_token_list: market_makable_token_list.clone(), - environment_metadata: network_id.clone(), - }; - let shared_instance_creator = Arc::new(SharedInstanceCreator::new( - instance_creator, - s3_instance_uploader, - )); - - // Helper function to create http solver instances. - let create_http_solver = |account: Account, - url: Url, - name: String, - config: SolverConfig, - instance_type: InstanceType, - slippage_calculator: SlippageCalculator, - use_liquidity: bool| - -> HttpSolver { - HttpSolver::new( - DefaultHttpSolverApi { - name, - network_name: network_id.clone(), - chain_id, - base: url, - solve_path: "solve".to_owned(), - client: http_factory.create(), - gzip_requests: false, - config, - }, - account, - allowance_manager.clone(), - order_converter.clone(), - instance_type, - slippage_calculator, - market_makable_token_list.clone(), - *domain, - shared_instance_creator.clone(), - use_liquidity, - enforce_correct_fees, - ) - }; - - let mut solvers: Vec> = solvers - .into_iter() - .filter_map(|(account, solver_type)| { - let single_order = |inner: Box| { - SingleOrderSolver::new( - inner, - solver_metrics.clone(), - max_merged_settlements, - max_settlements_per_solver, - order_prioritization_config.clone(), - smallest_partial_fill, - settlement_rater.clone(), - ethflow_contract, - order_converter.clone(), - ) - }; - - let slippage_calculator = slippage_configuration.get_calculator(solver_type); - tracing::debug!( - solver = ?solver_type, slippage = ?slippage_calculator, - "configured slippage", - ); - - let risk_calculator = risk_configuration.get_calculator(solver_type); - - let solver = match solver_type { - SolverType::None => return None, - SolverType::Naive => shared(NaiveSolver::new( - account, - slippage_calculator, - enforce_correct_fees, - ethflow_contract, - order_converter.clone(), - )), - SolverType::Baseline => shared(BaselineSolver::new( - account, - base_tokens.clone(), - slippage_calculator, - ethflow_contract, - order_converter.clone(), - )), - SolverType::Quasimodo => shared(create_http_solver( - account, - quasimodo_solver_url.clone(), - "Quasimodo".to_string(), - SolverConfig { - use_internal_buffers: Some(use_internal_buffers), - ..Default::default() - }, - InstanceType::Filtered, - slippage_calculator, - true, - )), - SolverType::OneInch => shared(single_order(Box::new( - OneInchSolver::with_disabled_protocols( - account, - web3.clone(), - settlement_contract.clone(), - chain_id, - disabled_one_inch_protocols.clone(), - http_factory.create(), - one_inch_url.clone(), - slippage_calculator, - one_inch_referrer_address, - current_block_stream.clone(), - ) - .unwrap(), - ))), - SolverType::ZeroEx => { - let zeroex_solver = ZeroExSolver::new( - account, - web3.clone(), - settlement_contract.clone(), - chain_id, - zeroex_api.clone(), - zeroex_disabled_sources.clone(), - slippage_calculator, - ) - .unwrap() - .with_rfqt(zeroex_enable_rfqt) - .with_slippage_protection(zeroex_enable_slippage_protection); - shared(single_order(Box::new(zeroex_solver))) - } - SolverType::Paraswap => shared(single_order(Box::new(ParaswapSolver::new( - account, - web3.clone(), - settlement_contract.clone(), - token_info_fetcher.clone(), - disabled_paraswap_dexs.clone(), - http_factory.create(), - paraswap_partner.clone(), - paraswap_api_url.clone(), - slippage_calculator, - current_block_stream.clone(), - )))), - SolverType::BalancerSor => shared(single_order(Box::new(BalancerSorSolver::new( - account, - vault_contract - .expect("missing Balancer Vault deployment for SOR solver") - .clone(), - settlement_contract.clone(), - Arc::new( - DefaultBalancerSorApi::new( - http_factory.create(), - balancer_sor_url.clone(), - chain_id, - ) - .unwrap(), - ), - allowance_manager.clone(), - slippage_calculator, - )))), - }; - - Some(shared(OptimizingSolver { - inner: solver, - post_processing_pipeline: post_processing_pipeline.clone(), - risk_calculator, - })) - }) - .collect(); - - let external_solvers = join_all(external_solvers.into_iter().map(|solver| async move { - shared(create_http_solver( - solver.account.into_account(chain_id).await, - solver.url, - solver.name, - SolverConfig { - use_internal_buffers: Some(use_internal_buffers), - ..Default::default() - }, - InstanceType::Plain, - slippage_configuration.get_global_calculator(), - solver.use_liquidity, - )) - })) - .await; - solvers.extend(external_solvers); - - if solvers.is_empty() { - return Err(anyhow!("no solvers configured")); - } - - for solver in &solvers { - tracing::info!( - "initialized solver {} at address {:#x}", - solver.name(), - solver.account().address() - ) - } - - Ok(solvers) -} - -#[cfg(test)] -struct DummySolver; -#[cfg(test)] -#[async_trait::async_trait] -impl Solver for DummySolver { - async fn solve(&self, _: Auction) -> Result> { - unimplemented!() - } - - fn account(&self) -> ðcontract::Account { - unimplemented!() - } - - fn notify_auction_result(&self, _auction_id: AuctionId, _result: AuctionResult) {} - - fn name(&self) -> &'static str { - "DummySolver" - } -} - -#[cfg(test)] -pub fn dummy_arc_solver() -> Arc { - Arc::new(DummySolver) -} - -fn balance_and_convert_orders( - ethflow_contract: Option, - converter: &OrderConverter, - mut balances: HashMap, - orders: Vec, - external_prices: &ExternalPrices, -) -> Vec { - crate::order_balance_filter::balance_orders( - orders, - &mut balances, - ethflow_contract, - external_prices, - ) - .into_iter() - .filter_map(|order| match converter.normalize_limit_order(order) { - Ok(order) => Some(order), - Err(err) => { - tracing::debug!(?err, "error normalizing limit order"); - None - } - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Dummy solver returning no settlements - pub struct NoopSolver(); - #[async_trait::async_trait] - impl Solver for NoopSolver { - async fn solve(&self, _: Auction) -> Result> { - Ok(Vec::new()) - } - - fn notify_auction_result(&self, _auction_id: AuctionId, _result: AuctionResult) {} - - fn account(&self) -> &Account { - unimplemented!() - } - - fn name(&self) -> &'static str { - "NoopSolver" - } - } - - impl PartialEq for SolverAccountArg { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (SolverAccountArg::PrivateKey(a), SolverAccountArg::PrivateKey(b)) => { - a.public_address() == b.public_address() - } - (SolverAccountArg::Address(a), SolverAccountArg::Address(b)) => a == b, - _ => false, - } - } - } - - #[test] - fn parses_solver_account_arg() { - assert_eq!( - "0x4242424242424242424242424242424242424242424242424242424242424242" - .parse::() - .unwrap(), - SolverAccountArg::PrivateKey(PrivateKey::from_raw([0x42; 32]).unwrap()) - ); - assert_eq!( - "0x4242424242424242424242424242424242424242" - .parse::() - .unwrap(), - SolverAccountArg::Address(H160([0x42; 20])), - ); - - assert!(matches!( - "arn:aws:kms:eu-central-1:42:key/00000000-0000-0000-0000-00000000" - .parse::() - .unwrap(), - SolverAccountArg::Kms(_) - )); - } - - #[test] - fn errors_on_invalid_solver_account_arg() { - assert!("0x010203040506070809101112131415161718192021" - .parse::() - .is_err()); - assert!("not an account".parse::().is_err()); - } - - #[test] - fn parse_external_solver_arg() { - let arg = "name|http://solver.com/|0x4242424242424242424242424242424242424242424242424242424242424242|true|partially_fillable"; - let parsed = ExternalSolverArg::from_str(arg).unwrap(); - assert_eq!(parsed.name, "name"); - assert_eq!(parsed.url.to_string(), "http://solver.com/"); - assert_eq!( - parsed.account, - SolverAccountArg::PrivateKey(PrivateKey::from_raw([0x42; 32]).unwrap()) - ); - assert!(parsed.use_liquidity); - assert_eq!( - parsed.user_balance_support, - UserBalanceSupport::PartiallyFillable - ); - } - - #[test] - fn parse_external_solver_arg_user_balance_default() { - let arg = "name|http://solver.com/|0x4242424242424242424242424242424242424242424242424242424242424242|false"; - let parsed = ExternalSolverArg::from_str(arg).unwrap(); - assert_eq!(parsed.user_balance_support, UserBalanceSupport::None); - } -} diff --git a/crates/solver/src/solver/balancer_sor_solver.rs b/crates/solver/src/solver/balancer_sor_solver.rs deleted file mode 100644 index 29e5d15446..0000000000 --- a/crates/solver/src/solver/balancer_sor_solver.rs +++ /dev/null @@ -1,608 +0,0 @@ -//! Solver using the Balancer SOR. - -use { - super::single_order_solver::{ - execution_respects_order, - SettlementError, - SingleOrderSettlement, - SingleOrderSolving, - }, - crate::{ - interactions::{ - allowances::{AllowanceManaging, ApprovalRequest}, - balancer_v2::{self, SwapKind}, - }, - liquidity::{slippage::SlippageCalculator, LimitOrder}, - }, - anyhow::Result, - contracts::{BalancerV2Vault, GPv2Settlement}, - ethcontract::{Account, Bytes, I256, U256}, - model::order::OrderKind, - shared::{ - balancer_sor_api::{BalancerSorApi, Error as BalancerError, Query, Quote}, - external_prices::ExternalPrices, - interaction::{EncodedInteraction, Interaction}, - }, - std::sync::Arc, -}; - -/// A GPv2 solver that matches GP orders to direct 0x swaps. -pub struct BalancerSorSolver { - account: Account, - vault: BalancerV2Vault, - settlement: GPv2Settlement, - api: Arc, - allowance_fetcher: Arc, - slippage_calculator: SlippageCalculator, -} - -impl BalancerSorSolver { - pub fn new( - account: Account, - vault: BalancerV2Vault, - settlement: GPv2Settlement, - api: Arc, - allowance_fetcher: Arc, - slippage_calculator: SlippageCalculator, - ) -> Self { - Self { - account, - vault, - settlement, - api, - allowance_fetcher, - slippage_calculator, - } - } -} - -impl From for SettlementError { - fn from(err: BalancerError) -> Self { - match err { - BalancerError::Other(err) => Self::Other(err), - BalancerError::RateLimited => Self::RateLimited, - } - } -} - -#[async_trait::async_trait] -impl SingleOrderSolving for BalancerSorSolver { - async fn try_settle_order( - &self, - order: LimitOrder, - external_prices: &ExternalPrices, - gas_price: f64, - ) -> Result, SettlementError> { - let amount = match order.kind { - OrderKind::Sell => order.sell_amount, - OrderKind::Buy => order.buy_amount, - }; - let query = Query { - sell_token: order.sell_token, - buy_token: order.buy_token, - order_kind: order.kind, - amount, - gas_price: U256::from_f64_lossy(gas_price), - }; - - let quote = match self.api.quote(query).await? { - Some(quote) => quote, - None => { - tracing::debug!("No route found"); - return Ok(None); - } - }; - - let (quoted_sell_amount, quoted_buy_amount) = match order.kind { - OrderKind::Sell => (quote.swap_amount, quote.return_amount), - OrderKind::Buy => (quote.return_amount, quote.swap_amount), - }; - - if !execution_respects_order(&order, quoted_sell_amount, quoted_buy_amount) { - tracing::debug!("execution does not respect order"); - return Ok(None); - } - - let slippage = self.slippage_calculator.context(external_prices); - let (quoted_sell_amount_with_slippage, quoted_buy_amount_with_slippage) = match order.kind { - OrderKind::Sell => ( - quoted_sell_amount, - slippage.apply_to_amount_out(order.buy_token, quoted_buy_amount)?, - ), - OrderKind::Buy => ( - slippage.apply_to_amount_in(order.sell_token, quoted_sell_amount)?, - quoted_buy_amount, - ), - }; - - let mut settlement = SingleOrderSettlement { - sell_token_price: quoted_buy_amount, - buy_token_price: quoted_sell_amount, - interactions: Vec::new(), - executed_amount: order.full_execution_amount(), - order: order.clone(), - }; - - if let Some(approval) = self - .allowance_fetcher - .get_approval(&ApprovalRequest { - token: order.sell_token, - spender: self.vault.address(), - amount: quoted_sell_amount_with_slippage, - }) - .await? - { - settlement.interactions.push(Arc::new(approval)); - } - - let limits = compute_swap_limits( - "e, - quoted_sell_amount_with_slippage, - quoted_buy_amount_with_slippage, - )?; - let batch_swap = BatchSwap { - vault: self.vault.clone(), - settlement: self.settlement.clone(), - kind: order.kind, - quote, - limits, - }; - settlement.interactions.push(Arc::new(batch_swap)); - - Ok(Some(settlement)) - } - - fn account(&self) -> &Account { - &self.account - } - - fn name(&self) -> &'static str { - "BalancerSOR" - } -} - -fn compute_swap_limits( - quote: &Quote, - quoted_sell_amount_with_slippage: U256, - quoted_buy_amount_with_slippage: U256, -) -> Result> { - quote - .token_addresses - .iter() - .map(|&token| -> Result { - let limit = if token == quote.token_in { - // Use positive swap limit for sell amounts (that is, maximum - // amount that can be transferred in) - quoted_sell_amount_with_slippage.try_into()? - } else if token == quote.token_out { - // Use negative swap limit for buy amounts (that is, minimum - // amount that must be transferred out) - I256::try_from(quoted_buy_amount_with_slippage)? - .checked_neg() - .expect("positive integer can't overflow negation") - } else { - // For other tokens we don't want any net transfer in or out. - I256::zero() - }; - - Ok(limit) - }) - .collect() -} - -#[derive(Debug)] -struct BatchSwap { - vault: BalancerV2Vault, - settlement: GPv2Settlement, - kind: OrderKind, - quote: Quote, - limits: Vec, -} - -impl Interaction for BatchSwap { - fn encode(&self) -> Vec { - let kind = match self.kind { - OrderKind::Sell => SwapKind::GivenIn, - OrderKind::Buy => SwapKind::GivenOut, - } as _; - let swaps = self - .quote - .swaps - .iter() - .map(|swap| { - ( - Bytes(swap.pool_id.0), - swap.asset_in_index.into(), - swap.asset_out_index.into(), - swap.amount, - Bytes(swap.user_data.clone()), - ) - }) - .collect(); - let assets = self.quote.token_addresses.clone(); - let funds = ( - self.settlement.address(), // sender - false, // fromInternalBalance - self.settlement.address(), // recipient - false, // toInternalBalance - ); - let limits = self.limits.clone(); - - let calldata = self - .vault - .methods() - .batch_swap(kind, swaps, assets, funds, limits, *balancer_v2::NEVER) - .tx - .data - .expect("no calldata") - .0; - - vec![(self.vault.address(), 0.into(), Bytes(calldata))] - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::interactions::allowances::{AllowanceManager, MockAllowanceManaging}, - contracts::dummy_contract, - ethcontract::{H160, H256}, - mockall::predicate::*, - model::order::{Order, OrderData}, - reqwest::Client, - shared::{ - addr, - balancer_sor_api::{DefaultBalancerSorApi, MockBalancerSorApi, Swap}, - ethrpc::{create_env_test_transport, Web3}, - }, - std::env, - }; - - #[test] - fn computed_swap_sets_sign() { - let quote = Quote { - token_in: H160([1; 20]), - swap_amount: 1000000.into(), - token_out: H160([3; 20]), - return_amount: 1000000.into(), - token_addresses: vec![H160([1; 20]), H160([2; 20]), H160([3; 20])], - ..Default::default() - }; - - assert_eq!( - compute_swap_limits("e, 1000000.into(), 999000.into()).unwrap(), - vec![1000000.into(), 0.into(), (-999000).into()], - ); - } - - #[tokio::test] - async fn sell_order_swap() { - let sell_token = addr!("ba100000625a3754423978a60c9317c58a424e3d"); - let buy_token = addr!("6b175474e89094c44da98b954eedeac495271d0f"); - let sell_amount = U256::from(1_000_000); - let buy_amount = U256::from(2_000_000); - - let vault = dummy_contract!(BalancerV2Vault, H160([0xba; 20])); - let settlement = dummy_contract!(GPv2Settlement, H160([0x90; 20])); - - let mut api = MockBalancerSorApi::new(); - api.expect_quote() - .with(eq(Query { - sell_token, - buy_token, - order_kind: OrderKind::Sell, - amount: sell_amount, - gas_price: 100_000_000_000_u128.into(), - })) - .returning(move |_| { - Ok(Some(Quote { - swap_amount: sell_amount, - return_amount: buy_amount, - token_in: sell_token, - token_out: buy_token, - token_addresses: vec![sell_token, H160([0xff; 20]), buy_token], - swaps: vec![ - Swap { - pool_id: H256([0; 32]), - asset_in_index: 0, - asset_out_index: 1, - amount: sell_amount, - user_data: Default::default(), - }, - Swap { - pool_id: H256([1; 32]), - asset_in_index: 1, - asset_out_index: 2, - amount: 0.into(), - user_data: Default::default(), - }, - ], - })) - }); - - let mut allowance_fetcher = MockAllowanceManaging::new(); - allowance_fetcher - .expect_get_approval() - .with(eq(ApprovalRequest { - token: sell_token, - spender: vault.address(), - amount: sell_amount, - })) - .returning(|_| Ok(None)); - - let solver = BalancerSorSolver::new( - Account::Local(H160([0x42; 20]), None), - vault.clone(), - settlement.clone(), - Arc::new(api), - Arc::new(allowance_fetcher), - SlippageCalculator::default(), - ); - - let result = solver - .try_settle_order( - Order { - data: OrderData { - sell_token, - buy_token, - sell_amount, - buy_amount, - kind: OrderKind::Sell, - ..Default::default() - }, - ..Default::default() - } - .into(), - &Default::default(), - 100e9, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!(result.buy_token_price, sell_amount); - assert_eq!(result.sell_token_price, buy_amount); - - let calldata: &[u8] = &result.interactions[0].encode()[0].2 .0; - assert_eq!( - calldata, - vault - .methods() - .batch_swap( - SwapKind::GivenIn as _, - vec![ - ( - Bytes([0; 32]), - 0.into(), - 1.into(), - sell_amount, - Bytes(Default::default()) - ), - ( - Bytes([1; 32]), - 1.into(), - 2.into(), - 0.into(), - Bytes(Default::default()) - ) - ], - vec![sell_token, H160([0xff; 20]), buy_token], - (settlement.address(), false, settlement.address(), false), - vec![ - I256::from_raw(sell_amount), - I256::zero(), - -I256::from_raw(buy_amount * 999 / 1000) - ], - U256::one() << 255, - ) - .tx - .data - .unwrap() - .0, - ); - } - - #[tokio::test] - async fn buy_order_swap() { - let sell_token = addr!("ba100000625a3754423978a60c9317c58a424e3d"); - let buy_token = addr!("6b175474e89094c44da98b954eedeac495271d0f"); - let sell_amount = U256::from(1_000_000); - let buy_amount = U256::from(2_000_000); - - let vault = dummy_contract!(BalancerV2Vault, H160([0xba; 20])); - let settlement = dummy_contract!(GPv2Settlement, H160([0x90; 20])); - - let mut api = MockBalancerSorApi::new(); - api.expect_quote() - .with(eq(Query { - sell_token, - buy_token, - order_kind: OrderKind::Buy, - amount: buy_amount, - gas_price: 100_000_000_000_u128.into(), - })) - .returning(move |_| { - Ok(Some(Quote { - swap_amount: buy_amount, - return_amount: sell_amount, - token_in: sell_token, - token_out: buy_token, - token_addresses: vec![sell_token, buy_token], - swaps: vec![Swap { - pool_id: Default::default(), - asset_in_index: 0, - asset_out_index: 1, - amount: buy_amount, - user_data: Default::default(), - }], - })) - }); - - let mut allowance_fetcher = MockAllowanceManaging::new(); - allowance_fetcher - .expect_get_approval() - .with(eq(ApprovalRequest { - token: sell_token, - spender: vault.address(), - amount: sell_amount * 1001 / 1000, - })) - .returning(|_| Ok(None)); - - let solver = BalancerSorSolver::new( - Account::Local(H160([0x42; 20]), None), - vault.clone(), - settlement.clone(), - Arc::new(api), - Arc::new(allowance_fetcher), - SlippageCalculator::default(), - ); - - let result = solver - .try_settle_order( - Order { - data: OrderData { - sell_token, - buy_token, - sell_amount, - buy_amount, - kind: OrderKind::Buy, - ..Default::default() - }, - ..Default::default() - } - .into(), - &Default::default(), - 100e9, - ) - .await - .unwrap() - .unwrap(); - - assert_eq!(result.buy_token_price, sell_amount); - assert_eq!(result.sell_token_price, buy_amount); - - let calldata: &[u8] = &result.interactions[0].encode()[0].2 .0; - assert_eq!( - calldata, - vault - .methods() - .batch_swap( - SwapKind::GivenOut as _, - vec![( - Bytes([0; 32]), - 0.into(), - 1.into(), - buy_amount, - Bytes(Default::default()) - )], - vec![sell_token, buy_token], - (settlement.address(), false, settlement.address(), false), - vec![ - I256::from_raw(sell_amount * 1001 / 1000), - -I256::from_raw(buy_amount), - ], - U256::one() << 255, - ) - .tx - .data - .unwrap() - .0, - ); - } - - #[tokio::test] - async fn skips_settlement_on_empty_swaps() { - let vault = dummy_contract!(BalancerV2Vault, H160([0xba; 20])); - let settlement = dummy_contract!(GPv2Settlement, H160([0x90; 20])); - - let mut api = MockBalancerSorApi::new(); - api.expect_quote().returning(move |_| Ok(None)); - - let allowance_fetcher = MockAllowanceManaging::new(); - - let solver = BalancerSorSolver::new( - Account::Local(H160([0x42; 20]), None), - vault, - settlement, - Arc::new(api), - Arc::new(allowance_fetcher), - SlippageCalculator::default(), - ); - - assert!(matches!( - solver - .try_settle_order(LimitOrder::default(), &Default::default(), 1.) - .await, - Ok(None), - )); - } - - #[tokio::test] - #[ignore] - async fn balancer_sor_solve() { - let web3 = Web3::new(create_env_test_transport()); - let chain_id = web3.eth().chain_id().await.unwrap().as_u64(); - - let vault = BalancerV2Vault::deployed(&web3).await.unwrap(); - let settlement = GPv2Settlement::deployed(&web3).await.unwrap(); - - let url = env::var("BALANCER_SOR_URL").unwrap(); - let api = DefaultBalancerSorApi::new(Client::new(), url, chain_id).unwrap(); - - let allowance_fetcher = AllowanceManager::new(web3, settlement.address()); - - let solver = BalancerSorSolver::new( - Account::Local(addr!("a6DDBD0dE6B310819b49f680F65871beE85f517e"), None), - vault, - settlement, - Arc::new(api), - Arc::new(allowance_fetcher), - SlippageCalculator::default(), - ); - - let sell_settlement = solver - .try_settle_order( - Order { - data: OrderData { - sell_token: addr!("ba100000625a3754423978a60c9317c58a424e3d"), - buy_token: addr!("6b175474e89094c44da98b954eedeac495271d0f"), - sell_amount: 1_000_000_000_000_000_000_u128.into(), - buy_amount: 1u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - ..Default::default() - } - .into(), - &Default::default(), - 100e9, - ) - .await - .unwrap() - .unwrap(); - println!("Found settlement for sell order: {sell_settlement:#?}"); - - let buy_settlement = solver - .try_settle_order( - Order { - data: OrderData { - sell_token: addr!("ba100000625a3754423978a60c9317c58a424e3d"), - buy_token: addr!("6b175474e89094c44da98b954eedeac495271d0f"), - sell_amount: u128::MAX.into(), - buy_amount: 100_000_000_000_000_000_000_u128.into(), - kind: OrderKind::Buy, - ..Default::default() - }, - ..Default::default() - } - .into(), - &Default::default(), - 100e9, - ) - .await - .unwrap() - .unwrap(); - println!("Found settlement for buy order: {buy_settlement:#?}"); - } -} diff --git a/crates/solver/src/solver/baseline_solver.rs b/crates/solver/src/solver/baseline_solver.rs index a1ee3aeaad..be79d85a0e 100644 --- a/crates/solver/src/solver/baseline_solver.rs +++ b/crates/solver/src/solver/baseline_solver.rs @@ -1,90 +1,12 @@ use { - super::single_order_solver::SingleOrderSettlement, - crate::{ - liquidity::{ - order_converter::OrderConverter, - slippage::{SlippageCalculator, SlippageContext}, - token_pairs, - AmmOrderExecution, - ConstantProductOrder, - LimitOrder, - Liquidity, - WeightedProductOrder, - }, - settlement::Settlement, - solver::{Auction, Solver}, - }, - anyhow::{Context, Result}, - ethcontract::{Account, H160, U256}, - model::{order::OrderKind, TokenPair}, + crate::liquidity::{ConstantProductOrder, WeightedProductOrder}, + ethcontract::{H160, U256}, shared::{ - baseline_solver::{ - estimate_buy_amount, - estimate_sell_amount, - BaseTokens, - BaselineSolvable, - }, - http_solver::model::TokenAmount, + baseline_solver::BaselineSolvable, sources::{balancer_v2::swap::WeightedPoolRef, uniswap_v2::pool_fetching::Pool}, }, - std::{collections::HashMap, sync::Arc}, }; -pub struct BaselineSolver { - account: Account, - base_tokens: Arc, - slippage_calculator: SlippageCalculator, - ethflow_contract: Option, - order_converter: OrderConverter, -} - -#[async_trait::async_trait] -impl Solver for BaselineSolver { - async fn solve( - &self, - Auction { - orders, - liquidity, - external_prices, - gas_price, - balances, - .. - }: Auction, - ) -> Result> { - let slippage = self.slippage_calculator.context(&external_prices); - let orders = super::balance_and_convert_orders( - self.ethflow_contract, - &self.order_converter, - balances, - orders, - &external_prices, - ); - Ok(self.solve_(orders, liquidity, slippage, gas_price)) - } - - fn account(&self) -> &Account { - &self.account - } - - fn name(&self) -> &'static str { - "BaselineSolver" - } -} - -/// A type representing all possible AMM orders that are considered as on-chain -/// liquidity by the baseline solver. -#[derive(Debug, Clone)] -struct Amm { - tokens: TokenPair, - order: AmmOrder, -} - -#[derive(Debug, Clone)] -enum AmmOrder { - ConstantProduct(ConstantProductOrder), - WeightedProduct(WeightedProductOrder), -} - 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) @@ -113,258 +35,6 @@ impl BaselineSolvable for WeightedProductOrder { } } -impl BaselineSolvable for Amm { - fn get_amount_out(&self, out_token: H160, input: (U256, H160)) -> Option { - match &self.order { - AmmOrder::ConstantProduct(order) => order.get_amount_out(out_token, input), - AmmOrder::WeightedProduct(order) => order.get_amount_out(out_token, input), - } - } - - fn get_amount_in(&self, in_token: H160, output: (U256, H160)) -> Option { - match &self.order { - AmmOrder::ConstantProduct(order) => order.get_amount_in(in_token, output), - AmmOrder::WeightedProduct(order) => order.get_amount_in(in_token, output), - } - } - - fn gas_cost(&self) -> usize { - match &self.order { - AmmOrder::ConstantProduct(order) => order.gas_cost(), - AmmOrder::WeightedProduct(order) => order.gas_cost(), - } - } -} - -impl BaselineSolver { - pub fn new( - account: Account, - base_tokens: Arc, - slippage_calculator: SlippageCalculator, - ethflow_contract: Option, - order_converter: OrderConverter, - ) -> Self { - Self { - account, - base_tokens, - slippage_calculator, - ethflow_contract, - order_converter, - } - } - - fn solve_( - &self, - mut limit_orders: Vec, - liquidity: Vec, - slippage: SlippageContext, - gas_price: f64, - ) -> Vec { - limit_orders.retain(|order| !order.is_liquidity_order()); - let user_orders = limit_orders; - let amm_map = - liquidity - .into_iter() - .fold(HashMap::<_, Vec<_>>::new(), |mut amm_map, liquidity| { - match liquidity { - Liquidity::ConstantProduct(order) => { - amm_map.entry(order.tokens).or_default().push(Amm { - tokens: order.tokens, - order: AmmOrder::ConstantProduct(order), - }); - } - Liquidity::BalancerWeighted(order) => { - for tokens in token_pairs(&order.reserves) { - amm_map.entry(tokens).or_default().push(Amm { - tokens, - order: AmmOrder::WeightedProduct(order.clone()), - }); - } - } - // TODO(#80): support stable pool for baseline solving - Liquidity::BalancerStable(_order) => (), - Liquidity::LimitOrder(_) => (), - // Not being implemented right now since baseline solver is not winning - // anyway. - Liquidity::Concentrated(_) => (), - } - amm_map - }); - - // We assume that individual settlements do not move the amm pools significantly - // when returning multiple settlements. - let mut settlements = Vec::new(); - - // Return a solution for the first settle-able user order - for order in user_orders { - match Self::fills_for_order(&order).find_map(|fill| { - self.solve_order(&fill, &amm_map)? - .into_settlement(&order, &slippage, gas_price) - .transpose() - }) { - Some(Ok(settlement)) => settlements.push(settlement), - Some(Err(err)) => { - tracing::error!(?err, id = ?order.id, "failed to create settlement") - } - None => continue, - }; - } - - settlements - } - - fn fills_for_order(order: &LimitOrder) -> impl Iterator + '_ { - const MAX_PARTIAL_ATTEMPTS: usize = 5; - - let n = if order.partially_fillable { - MAX_PARTIAL_ATTEMPTS - } else { - 1 - }; - - (0..n) - .map(move |i| { - let divisor = U256::one() << i; - LimitOrder { - sell_amount: order.sell_amount / divisor, - buy_amount: order.buy_amount / divisor, - ..order.clone() - } - }) - .filter(|o| !o.sell_amount.is_zero() && !o.buy_amount.is_zero()) - } - - fn solve_order( - &self, - order: &LimitOrder, - amms: &HashMap>, - ) -> Option { - let candidates = self - .base_tokens - .path_candidates(order.sell_token, order.buy_token); - - let (path, executed_sell_amount, executed_buy_amount) = match order.kind { - model::order::OrderKind::Buy => { - let best = candidates - .iter() - .filter_map(|path| estimate_sell_amount(order.buy_amount, path, amms)) - .filter(|estimate| estimate.value <= order.sell_amount) - // For buy orders we find the best path starting at the buy token ending at the - // sell token. When we turn this into a settlement however we need to go from - // the sell token to the buy token. This reversing of the direction can fail or - // yield different amounts as explained in the BaselineSolvable trait. - .filter(|estimate| { - matches!( - traverse_path_forward( - order.sell_token, - estimate.value, - &estimate.path, - ), Some(amount) if amount >= order.buy_amount - ) - }) - .min_by_key(|estimate| estimate.value)?; - (best.path, best.value, order.buy_amount) - } - model::order::OrderKind::Sell => { - let best = candidates - .iter() - .filter_map(|path| estimate_buy_amount(order.sell_amount, path, amms)) - .filter(|estimate| estimate.value >= order.buy_amount) - .max_by_key(|estimate| estimate.value)?; - (best.path, order.sell_amount, best.value) - } - }; - Some(Solution { - path: path.into_iter().cloned().collect(), - executed_sell_amount, - executed_buy_amount, - }) - } - - #[cfg(test)] - fn must_solve(&self, orders: Vec, liquidity: Vec) -> Settlement { - self.solve_(orders, liquidity, SlippageContext::default(), 0.) - .into_iter() - .next() - .unwrap() - } -} - -fn traverse_path_forward( - mut sell_token: H160, - mut sell_amount: U256, - path: &[&Amm], -) -> Option { - for amm in path { - let buy_token = amm.tokens.other(&sell_token).expect("Inconsistent path"); - let buy_amount = amm.get_amount_out(buy_token, (sell_amount, sell_token))?; - sell_token = buy_token; - sell_amount = buy_amount; - } - Some(sell_amount) -} - -#[derive(Debug)] -struct Solution { - path: Vec, - executed_sell_amount: U256, - executed_buy_amount: U256, -} - -impl Solution { - fn into_settlement( - self, - order: &LimitOrder, - slippage: &SlippageContext, - gas_price: f64, - ) -> Result> { - let gas_used = self - .path - .iter() - .fold(U256::zero(), |acc, amm| acc + amm.gas_cost()); - let gas_cost = gas_used - .checked_mul(U256::from_f64_lossy(gas_price)) - .context("overflow during gas cost computation")?; - - let settlement = SingleOrderSettlement { - sell_token_price: self.executed_buy_amount, - buy_token_price: self.executed_sell_amount, - interactions: vec![], - executed_amount: match order.kind { - OrderKind::Buy => self.executed_buy_amount, - OrderKind::Sell => self.executed_sell_amount, - }, - order: order.clone(), - } - .into_settlement(slippage.prices(), &gas_cost)?; - - let Some(mut settlement) = settlement else { - return Ok(None); - }; - - let (mut sell_amount, mut sell_token) = (self.executed_sell_amount, order.sell_token); - for amm in self.path { - let buy_token = amm.tokens.other(&sell_token).expect("Inconsistent path"); - let buy_amount = amm - .get_amount_out(buy_token, (sell_amount, sell_token)) - .expect("Path was found, so amount must be calculable"); - let execution = slippage.apply_to_amm_execution(AmmOrderExecution { - input_max: TokenAmount::new(sell_token, sell_amount), - output: TokenAmount::new(buy_token, buy_amount), - internalizable: false, - })?; - match &amm.order { - AmmOrder::ConstantProduct(order) => settlement.with_liquidity(order, execution), - AmmOrder::WeightedProduct(order) => settlement.with_liquidity(order, execution), - }?; - sell_amount = buy_amount; - sell_token = buy_token; - } - - Ok(Some(settlement)) - } -} - fn amm_to_pool(amm: &ConstantProductOrder) -> Pool { Pool { address: amm.address, @@ -381,484 +51,3 @@ fn amm_to_weighted_pool(amm: &WeightedProductOrder) -> WeightedPoolRef { version: amm.version, } } - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - liquidity::{ - tests::CapturingSettlementHandler, - AmmOrderExecution, - ConstantProductOrder, - LimitOrder, - LimitOrderExecution, - }, - test::account, - }, - maplit::{btreemap, hashmap}, - model::order::OrderKind, - num::rational::Ratio, - shared::{ - addr, - sources::balancer_v2::{ - pool_fetching::{TokenState, WeightedTokenState}, - swap::fixed_point::Bfp, - }, - }, - }; - - #[test] - fn finds_best_route_sell_order() { - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(0); - let native_token = H160::from_low_u64_be(3); - - let order_handler = vec![ - CapturingSettlementHandler::arc(), - CapturingSettlementHandler::arc(), - ]; - let orders = vec![ - LimitOrder { - sell_amount: 100_000.into(), - buy_amount: 100_000.into(), - sell_token, - buy_token, - kind: OrderKind::Sell, - settlement_handling: order_handler[0].clone(), - id: 0.into(), - ..Default::default() - }, - // Second order has a more lax limit - LimitOrder { - sell_amount: 100_000.into(), - buy_amount: 90_000.into(), - buy_token, - sell_token, - kind: OrderKind::Sell, - settlement_handling: order_handler[1].clone(), - id: 1.into(), - ..Default::default() - }, - ]; - - let amm_handler = vec![ - CapturingSettlementHandler::arc(), - CapturingSettlementHandler::arc(), - CapturingSettlementHandler::arc(), - ]; - let amms = vec![ - ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(buy_token, sell_token).unwrap(), - reserves: (1_000_000, 1_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler[0].clone(), - }, - // Path via native token has more liquidity - ConstantProductOrder { - address: H160::from_low_u64_be(2), - tokens: TokenPair::new(sell_token, native_token).unwrap(), - reserves: (10_000_000, 10_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler[1].clone(), - }, - // Second native token pool has a worse price despite larger k - ConstantProductOrder { - address: H160::from_low_u64_be(3), - tokens: TokenPair::new(sell_token, native_token).unwrap(), - reserves: (11_000_000, 10_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler[1].clone(), - }, - ConstantProductOrder { - address: H160::from_low_u64_be(4), - tokens: TokenPair::new(native_token, buy_token).unwrap(), - reserves: (10_000_000, 10_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler[2].clone(), - }, - ]; - let liquidity = amms.into_iter().map(Liquidity::ConstantProduct).collect(); - - let base_tokens = Arc::new(BaseTokens::new(native_token, &[])); - let solver = BaselineSolver::new( - account(), - base_tokens, - SlippageCalculator::default(), - None, - OrderConverter::test(Default::default()), - ); - let result = solver.must_solve(orders, liquidity); - assert_eq!( - result.clearing_prices(), - &hashmap! { - sell_token => 97_459.into(), - buy_token => 100_000.into(), - } - ); - - // Second order is fully matched - assert_eq!(order_handler[0].clone().calls().len(), 0); - assert_eq!( - order_handler[1].clone().calls()[0], - LimitOrderExecution::new(100_000.into(), 0.into()) - ); - - // Second & Third AMM are matched with slippage applied - let slippage = SlippageContext::default(); - assert_eq!(amm_handler[0].clone().calls().len(), 0); - assert_eq!( - amm_handler[1].clone().calls()[0], - slippage - .apply_to_amm_execution(AmmOrderExecution { - input_max: TokenAmount::new(sell_token, 100_000), - output: TokenAmount::new(native_token, 98_715), - internalizable: false - }) - .unwrap(), - ); - assert_eq!( - amm_handler[2].clone().calls()[0], - slippage - .apply_to_amm_execution(AmmOrderExecution { - input_max: TokenAmount::new(native_token, 98_715), - output: TokenAmount::new(buy_token, 97_459), - internalizable: false - }) - .unwrap(), - ); - } - - #[test] - fn finds_best_route_buy_order() { - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(0); - let native_token = H160::from_low_u64_be(3); - - let order_handler = vec![ - CapturingSettlementHandler::arc(), - CapturingSettlementHandler::arc(), - ]; - let orders = vec![ - LimitOrder { - sell_amount: 100_000.into(), - buy_amount: 100_000.into(), - sell_token, - buy_token, - kind: OrderKind::Buy, - settlement_handling: order_handler[0].clone(), - id: 0.into(), - ..Default::default() - }, - // Second order has a more lax limit - LimitOrder { - sell_amount: 110_000.into(), - buy_amount: 100_000.into(), - buy_token, - sell_token, - kind: OrderKind::Buy, - settlement_handling: order_handler[1].clone(), - id: 1.into(), - ..Default::default() - }, - ]; - - let amm_handler = vec![ - CapturingSettlementHandler::arc(), - CapturingSettlementHandler::arc(), - CapturingSettlementHandler::arc(), - ]; - let amms = vec![ - ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(buy_token, sell_token).unwrap(), - reserves: (1_000_000, 1_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler[0].clone(), - }, - // Path via native token has more liquidity - ConstantProductOrder { - address: H160::from_low_u64_be(2), - tokens: TokenPair::new(sell_token, native_token).unwrap(), - reserves: (10_000_000, 10_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler[1].clone(), - }, - // Second native token pool has a worse price despite larger k - ConstantProductOrder { - address: H160::from_low_u64_be(3), - tokens: TokenPair::new(sell_token, native_token).unwrap(), - reserves: (11_000_000, 10_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler[1].clone(), - }, - ConstantProductOrder { - address: H160::from_low_u64_be(4), - tokens: TokenPair::new(native_token, buy_token).unwrap(), - reserves: (10_000_000, 10_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: amm_handler[2].clone(), - }, - ]; - let liquidity = amms.into_iter().map(Liquidity::ConstantProduct).collect(); - - let base_tokens = Arc::new(BaseTokens::new(native_token, &[])); - let solver = BaselineSolver::new( - account(), - base_tokens, - SlippageCalculator::default(), - None, - OrderConverter::test(Default::default()), - ); - let result = solver.must_solve(orders, liquidity); - assert_eq!( - result.clearing_prices(), - &hashmap! { - sell_token => 100_000.into(), - buy_token => 102_660.into(), - } - ); - - // Second order is fully matched - assert_eq!(order_handler[0].clone().calls().len(), 0); - assert_eq!( - order_handler[1].clone().calls()[0], - LimitOrderExecution::new(100_000.into(), 0.into()) - ); - - // Second & Third AMM are matched with slippage applied - let slippage = SlippageContext::default(); - assert_eq!(amm_handler[0].clone().calls().len(), 0); - assert_eq!( - amm_handler[1].clone().calls()[0], - slippage - .apply_to_amm_execution(AmmOrderExecution { - input_max: TokenAmount::new(sell_token, 102_660), - output: TokenAmount::new(native_token, 101_315), - internalizable: false - }) - .unwrap(), - ); - assert_eq!( - amm_handler[2].clone().calls()[0], - slippage - .apply_to_amm_execution(AmmOrderExecution { - input_max: TokenAmount::new(native_token, 101_315), - output: TokenAmount::new(buy_token, 100_000), - internalizable: false - }) - .unwrap(), - ); - } - - #[test] - fn finds_best_route_when_pool_returns_none() { - // Regression test for https://github.com/gnosis/gp-v2-services/issues/530 - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(0); - - let orders = vec![LimitOrder { - sell_amount: 110_000.into(), - buy_amount: 100_000.into(), - sell_token, - buy_token, - kind: OrderKind::Buy, - id: 0.into(), - ..Default::default() - }]; - - let amms = vec![ - ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(buy_token, sell_token).unwrap(), - reserves: (10_000_000, 10_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }, - // Other direct pool has not enough liquidity to compute a valid estimate - ConstantProductOrder { - address: H160::from_low_u64_be(2), - tokens: TokenPair::new(buy_token, sell_token).unwrap(), - reserves: (0, 0), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }, - ]; - let liquidity = amms.into_iter().map(Liquidity::ConstantProduct).collect(); - - let base_tokens = Arc::new(BaseTokens::new(H160::zero(), &[])); - let solver = BaselineSolver::new( - account(), - base_tokens, - SlippageCalculator::default(), - None, - OrderConverter::test(Default::default()), - ); - assert_eq!( - solver - .solve_(orders, liquidity, SlippageContext::default(), 0.) - .len(), - 1 - ); - } - - #[test] - fn does_not_panic_when_building_solution() { - // Regression test for https://github.com/gnosis/gp-v2-services/issues/838 - let order = LimitOrder { - sell_token: addr!("e4b9895e638f54c3bee2a3a78d6a297cc03e0353"), - buy_token: addr!("a7d1c04faf998f9161fc9f800a99a809b84cfc9d"), - sell_amount: 1_741_103_528_769_588_955_u128.into(), - buy_amount: 500_000_000_000_000_000_000_u128.into(), - kind: OrderKind::Buy, - id: 0.into(), - ..Default::default() - }; - let liquidity = vec![ - Liquidity::ConstantProduct(ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new( - addr!("a7d1c04faf998f9161fc9f800a99a809b84cfc9d"), - addr!("c778417e063141139fce010982780140aa0cd5ab"), - ) - .unwrap(), - reserves: (596_652_163_418_904_202_462_071, 225_949_669_025_168_181_644), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }), - Liquidity::BalancerWeighted(WeightedProductOrder { - address: H160::from_low_u64_be(2), - reserves: btreemap! { - addr!("c778417e063141139fce010982780140aa0cd5ab") => WeightedTokenState { - common: TokenState { - balance: 799_086_982_149_629_058_u128.into(), - scaling_factor: Bfp::exp10(0), - }, - weight: "0.5".parse().unwrap(), - }, - addr!("e4b9895e638f54c3bee2a3a78d6a297cc03e0353") => WeightedTokenState { - common: TokenState { - balance: 1_251_682_293_173_877_359_u128.into(), - scaling_factor: Bfp::exp10(0), - }, - weight: "0.5".parse().unwrap(), - }, - }, - fee: "0.001".parse().unwrap(), - version: Default::default(), - settlement_handling: CapturingSettlementHandler::arc(), - }), - ]; - - let base_tokens = Arc::new(BaseTokens::new( - addr!("c778417e063141139fce010982780140aa0cd5ab"), - &[], - )); - let solver = BaselineSolver::new( - account(), - base_tokens, - SlippageCalculator::default(), - None, - OrderConverter::test(Default::default()), - ); - assert_eq!( - solver - .solve_(vec![order], liquidity, SlippageContext::default(), 0.) - .len(), - 0 - ); - } - - #[test] - fn does_not_panic_for_asymmetrical_pool() { - let tokens: Vec = (0..3).map(H160::from_low_u64_be).collect(); - let order = LimitOrder { - id: 0.into(), - sell_token: tokens[0], - buy_token: tokens[2], - sell_amount: 7999613.into(), - buy_amount: 1.into(), - kind: OrderKind::Buy, - ..Default::default() - }; - let pool_0 = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(tokens[1], tokens[2]).unwrap(), - reserves: (10, 12), - fee: Ratio::new(0, 1), - settlement_handling: CapturingSettlementHandler::arc(), - }; - let pool_1 = WeightedProductOrder { - address: H160::from_low_u64_be(2), - reserves: [ - ( - tokens[0], - WeightedTokenState { - common: TokenState { - balance: 4294966784u64.into(), - scaling_factor: Bfp::exp10(0), - }, - weight: 255.into(), - }, - ), - ( - tokens[1], - WeightedTokenState { - common: TokenState { - balance: 4278190173u64.into(), - scaling_factor: Bfp::exp10(0), - }, - weight: 2030043135usize.into(), - }, - ), - ] - .iter() - .cloned() - .collect(), - fee: Bfp::zero(), - version: Default::default(), - settlement_handling: CapturingSettlementHandler::arc(), - }; - // When baseline solver goes from the buy token to the sell token it sees that a - // path with a sell amount of 7999613. However, since we "bump" the sell amount, - // we will compute an input amount that is sufficiently high. - assert_eq!( - pool_0.get_amount_in(tokens[1], (1.into(), tokens[2])), - Some(1.into()) - ); - assert_eq!( - pool_1.get_amount_in(tokens[0], (1.into(), tokens[1])), - Some(15999226.into()) - ); - // But then when it goes from the sell token to the buy token to construct the - // settlement it encounters the asymmetry of the weighted pool. With the - // same in amount the out amount has changed from 1 to 0. - assert_eq!( - pool_1.get_amount_out(tokens[1], (7999613.into(), tokens[0])), - Some(0.into()), - ); - // Note that the bumped input amount will be high enough. - assert_eq!( - pool_1.get_amount_out(tokens[1], (15999226.into(), tokens[0])), - Some(1.into()), - ); - // This makes using the second pool fail. - assert_eq!(pool_0.get_amount_in(tokens[2], (0.into(), tokens[1])), None); - - let liquidity = vec![ - Liquidity::ConstantProduct(pool_0), - Liquidity::BalancerWeighted(pool_1), - ]; - let base_tokens = Arc::new(BaseTokens::new(tokens[0], &tokens)); - let solver = BaselineSolver::new( - account(), - base_tokens, - SlippageCalculator::default(), - None, - OrderConverter::test(Default::default()), - ); - let settlements = solver.solve_(vec![order], liquidity, Default::default(), 0.); - assert!(settlements.is_empty()); - } -} diff --git a/crates/solver/src/solver/http_solver.rs b/crates/solver/src/solver/http_solver.rs deleted file mode 100644 index 7fce4768a4..0000000000 --- a/crates/solver/src/solver/http_solver.rs +++ /dev/null @@ -1,543 +0,0 @@ -pub mod buffers; -pub mod instance_cache; -pub mod instance_creation; -pub mod settlement; - -use { - self::{ - instance_cache::SharedInstanceCreator, - instance_creation::Instances, - settlement::ConversionError, - }, - super::{Auction, AuctionResult, Solver}, - crate::{ - interactions::allowances::AllowanceManaging, - liquidity::{order_converter::OrderConverter, slippage::SlippageCalculator}, - settlement::Settlement, - }, - anyhow::{Context, Result}, - ethcontract::Account, - model::{auction::AuctionId, DomainSeparator}, - primitive_types::H160, - shared::{ - http_solver::{ - model::{ - BatchAuctionModel, - InteractionData, - SettledBatchAuctionModel, - SolverRejectionReason, - }, - DefaultHttpSolverApi, - HttpSolverApi, - }, - token_list::AutoUpdatingTokenList, - }, - std::{ - borrow::Cow, - collections::{BTreeSet, HashSet}, - sync::Arc, - time::Instant, - }, -}; - -#[derive(Copy, Clone)] -pub enum InstanceType { - Plain, - /// without orders that are not connected to the fee token - Filtered, -} - -pub struct HttpSolver { - solver: DefaultHttpSolverApi, - account: Account, - allowance_manager: Arc, - order_converter: OrderConverter, - instance_type: InstanceType, - slippage_calculator: SlippageCalculator, - market_makable_token_list: AutoUpdatingTokenList, - domain: DomainSeparator, - instance_cache: Arc, - // Liquidity information takes up a lot of space in the instance. Only some solvers (only - // Quasimodo?) uses it so it is wasteful to send it to all of them. - use_liquidity: bool, - enforce_correct_fees: bool, -} - -impl HttpSolver { - #[allow(clippy::too_many_arguments)] - pub fn new( - solver: DefaultHttpSolverApi, - account: Account, - allowance_manager: Arc, - order_converter: OrderConverter, - instance_type: InstanceType, - slippage_calculator: SlippageCalculator, - market_makable_token_list: AutoUpdatingTokenList, - domain: DomainSeparator, - instance_cache: Arc, - use_liquidity: bool, - enforce_correct_fees: bool, - ) -> Self { - Self { - solver, - account, - allowance_manager, - order_converter, - instance_type, - slippage_calculator, - market_makable_token_list, - domain, - instance_cache, - use_liquidity, - enforce_correct_fees, - } - } -} - -fn non_bufferable_tokens_used( - interactions: &[InteractionData], - market_makable_token_list: &HashSet, -) -> BTreeSet { - interactions - .iter() - .filter(|interaction| { - interaction - .exec_plan - .as_ref() - .map(|plan| plan.internal) - .unwrap_or_default() - }) - .flat_map(|interaction| &interaction.inputs) - .filter(|input| !market_makable_token_list.contains(&input.token)) - .map(|input| input.token) - .collect() -} - -#[async_trait::async_trait] -impl Solver for HttpSolver { - async fn solve(&self, auction: Auction) -> Result> { - if auction.orders.is_empty() { - return Ok(Vec::new()); - }; - - let id = auction.id; - let external_prices = auction.external_prices.clone(); - - let (settled, instances) = self.solve_(auction).await?; - - if settled.orders.is_empty() { - return Ok(vec![]); - } - - // verify internal custom interactions return only bufferable tokens to - // settlement contract - let non_bufferable_tokens = non_bufferable_tokens_used( - &settled.interaction_data, - &self.market_makable_token_list.all(), - ); - if !non_bufferable_tokens.is_empty() { - tracing::warn!( - "Solution filtered out for using non bufferable output tokens for solver {}, \ - tokens: {:?}", - self.solver.name, - non_bufferable_tokens - ); - self.notify_auction_result( - id, - AuctionResult::Rejected(SolverRejectionReason::NonBufferableTokensUsed( - non_bufferable_tokens, - )), - ); - return Ok(vec![]); - } - - let slippage = self.slippage_calculator.context(&external_prices); - match settlement::convert_settlement( - settled.clone(), - &instances.context, - self.allowance_manager.clone(), - &self.order_converter, - slippage, - &self.domain, - self.enforce_correct_fees, - ) - .await - { - Ok(settlement) => Ok(vec![settlement]), - Err(err) => { - tracing::debug!( - name = %self.name(), ?settled, ?err, - "failed to process HTTP solver result", - ); - if matches!(err, ConversionError::InvalidExecutionPlans(_)) { - self.notify_auction_result( - id, - AuctionResult::Rejected(SolverRejectionReason::InvalidExecutionPlans), - ); - } - Err(err.into()) - } - } - } - - fn notify_auction_result(&self, auction_id: AuctionId, result: AuctionResult) { - self.solver.notify_auction_result(auction_id, result); - } - - fn account(&self) -> &Account { - &self.account - } - - fn name(&self) -> &str { - &self.solver.name - } -} - -impl HttpSolver { - async fn solve_(&self, auction: Auction) -> Result<(SettledBatchAuctionModel, Arc)> { - let deadline = auction.deadline; - let instances = self.instance_cache.get_instances(auction).await; - let model = match self.instance_type { - InstanceType::Plain => &instances.plain, - InstanceType::Filtered => &instances.filtered, - }; - - let mut model: Cow = Cow::Borrowed(model); - if !self.use_liquidity { - model.to_mut().amms.clear(); - } - - let timeout = deadline - .checked_duration_since(Instant::now()) - .context("no time left to send request")?; - let mut settled = self.solver.solve(&model, timeout).await?; - settled.add_missing_execution_plans(); - - tracing::debug!( - "Solution received from http solver {} (json):\n{}", - self.solver.name, - serde_json::to_string_pretty(&settled).unwrap() - ); - - Ok((settled, instances)) - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - interactions::allowances::MockAllowanceManaging, - liquidity::{tests::CapturingSettlementHandler, ConstantProductOrder, Liquidity}, - solver::http_solver::{ - buffers::MockBufferRetrieving, - instance_creation::InstanceCreator, - }, - }, - ::model::TokenPair, - contracts::{dummy_contract, WETH9}, - ethcontract::Address, - maplit::hashmap, - model::order::{Order, OrderData, OrderKind}, - num::rational::Ratio, - primitive_types::U256, - reqwest::Client, - shared::{ - http_solver::{ - model::{ExecutionPlan, TokenAmount}, - SolverConfig, - }, - token_info::{MockTokenInfoFetching, TokenInfo}, - }, - std::{sync::Arc, time::Duration}, - }; - - // cargo test real_solver -- --ignored --nocapture - // set the env variable GP_V2_OPTIMIZER_URL to use a non localhost optimizer - #[tokio::test] - #[ignore] - async fn real_solver() { - tracing_subscriber::fmt::fmt() - .with_env_filter("solver=trace") - .init(); - let url = std::env::var("GP_V2_OPTIMIZER_URL") - .unwrap_or_else(|_| "http://localhost:8000".to_string()); - - let buy_token = H160::from_low_u64_be(1337); - let sell_token = H160::from_low_u64_be(43110); - - let mut mock_token_info_fetcher = MockTokenInfoFetching::new(); - mock_token_info_fetcher - .expect_get_token_infos() - .return_once(move |_| { - hashmap! { - buy_token => TokenInfo { decimals: Some(18), symbol: Some("CAT".to_string()) }, - sell_token => TokenInfo { decimals: Some(18), symbol: Some("CAT".to_string()) }, - } - }); - - let mut mock_buffer_retriever = MockBufferRetrieving::new(); - mock_buffer_retriever - .expect_get_buffers() - .return_once(move |_| { - hashmap! { - buy_token => Ok(U256::from(42)), - sell_token => Ok(U256::from(1337)), - } - }); - - let gas_price = 100.; - - let solver = HttpSolver::new( - DefaultHttpSolverApi { - name: "Test Solver".to_string(), - network_name: "mock_network_id".to_string(), - chain_id: 0, - base: url.parse().unwrap(), - solve_path: "solve".to_owned(), - client: Client::new(), - gzip_requests: false, - config: SolverConfig::default(), - }, - Account::Local(Address::default(), None), - Arc::new(MockAllowanceManaging::new()), - OrderConverter::test(H160([0x42; 20])), - InstanceType::Filtered, - SlippageCalculator::default(), - Default::default(), - Default::default(), - Arc::new(SharedInstanceCreator::new( - InstanceCreator { - native_token: dummy_contract!(WETH9, [0x00; 20]), - ethflow_contract: None, - token_info_fetcher: Arc::new(mock_token_info_fetcher), - buffer_retriever: Arc::new(mock_buffer_retriever), - market_makable_token_list: Default::default(), - environment_metadata: Default::default(), - }, - None, - )), - true, - true, - ); - let base = |x: u128| x * 10u128.pow(18); - let orders = vec![Order { - data: OrderData { - buy_token, - sell_token, - buy_amount: base(1).into(), - sell_amount: base(2).into(), - kind: OrderKind::Sell, - ..Default::default() - }, - ..Default::default() - }]; - let liquidity = vec![Liquidity::ConstantProduct(ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(buy_token, sell_token).unwrap(), - reserves: (base(100), base(100)), - fee: Ratio::new(0, 1), - settlement_handling: CapturingSettlementHandler::arc(), - })]; - let (settled, _context) = solver - .solve_(Auction { - id: 0, - run: 1, - orders, - liquidity, - gas_price, - deadline: Instant::now() + Duration::from_secs(100), - ..Default::default() - }) - .await - .unwrap(); - - let exec_order = settled.orders.values().next().unwrap(); - assert_eq!(exec_order.exec_sell_amount.as_u128(), base(2)); - assert!(exec_order.exec_buy_amount.as_u128() > 0); - - let uniswap = settled.amms.values().next().unwrap(); - let execution = &uniswap.execution[0]; - assert!(execution.exec_buy_amount.gt(&U256::zero())); - assert_eq!(execution.exec_sell_amount, U256::from(base(2))); - assert_eq!(execution.exec_plan, ExecutionPlan::default()); - - assert_eq!(settled.prices.len(), 2); - } - - #[test] - fn decode_response() { - let example_response = r#" - { - "extra_crap": ["Hello"], - "orders": { - "0": { - "sell_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buy_token": "0xba100000625a3754423978a60c9317c58a424e3d", - "sell_amount": "195160000000000000", - "buy_amount": "18529625032931383084", - "allow_partial_fill": false, - "is_sell_order": true, - "fee": { - "amount": "4840000000000000", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "cost": { - "amount": "1604823000000000", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "exec_buy_amount": "18689825362370811941", - "exec_sell_amount": "195160000000000000" - }, - "1": { - "sell_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buy_token": "0xba100000625a3754423978a60c9317c58a424e3d", - "sell_amount": "395160000000000000", - "buy_amount": "37314737669229514851", - "allow_partial_fill": false, - "is_sell_order": true, - "fee": { - "amount": "4840000000000000", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "cost": { - "amount": "1604823000000000", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "exec_buy_amount": "37843161458262200293", - "exec_sell_amount": "395160000000000000" - } - }, - "ref_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "prices": { - "0xba100000625a3754423978a60c9317c58a424e3d": "10442045135045813", - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "1000000000000000000" - }, - "amms": { - "0x0000000000000000000000000000000000000000": { - "kind": "WeightedProduct", - "reserves": { - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": { - "balance": "99572200495363891220", - "weight": "0.5" - }, - "0xba100000625a3754423978a60c9317c58a424e3d": { - "balance": "9605600791222732320384", - "weight": "0.5" - } - }, - "fee": "0.0014", - "cost": { - "amount": "2904000000000000", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - }, - "execution": [ - { - "sell_token": "0xba100000625a3754423978a60c9317c58a424e3d", - "buy_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "exec_sell_amount": "56532986820633012234", - "exec_buy_amount": "590320000000000032", - "exec_plan": { - "sequence": 0, - "position": 0, - "internal": false - } - } - ] - } - } - } - "#; - let parsed_response = serde_json::from_str::(example_response); - assert!(parsed_response.is_ok()); - } - - #[test] - fn non_bufferable_tokens_used_test_all_empty() { - let interactions = vec![]; - let market_makable_token_list = HashSet::::new(); - assert_eq!( - non_bufferable_tokens_used(&interactions, &market_makable_token_list), - BTreeSet::new() - ); - } - - // Interaction is internal and it contains only bufferable tokens - #[test] - fn non_bufferable_tokens_used_test_ok() { - let bufferable_token = H160::from_low_u64_be(1); - let market_makable_token_list = HashSet::from([bufferable_token]); - - let token_amount = TokenAmount { - token: bufferable_token, - ..Default::default() - }; - - let interactions = vec![InteractionData { - inputs: vec![token_amount], - exec_plan: Some(ExecutionPlan { - internal: true, - ..Default::default() - }), - ..Default::default() - }]; - - assert_eq!( - non_bufferable_tokens_used(&interactions, &market_makable_token_list), - BTreeSet::new() - ); - } - - // Interaction is internal but it contains non bufferable tokens - #[test] - fn non_bufferable_tokens_used_test_not_ok() { - let non_bufferable_token = H160::from_low_u64_be(1); - let market_makable_token_list = HashSet::from([]); - - let token_amount = TokenAmount { - token: non_bufferable_token, - ..Default::default() - }; - - let interactions = vec![InteractionData { - inputs: vec![token_amount], - exec_plan: Some(ExecutionPlan { - internal: true, - ..Default::default() - }), - ..Default::default() - }]; - - assert_eq!( - non_bufferable_tokens_used(&interactions, &market_makable_token_list), - BTreeSet::from([non_bufferable_token]) - ); - } - - // Interaction is **not** internal and it contains non bufferable tokens - #[test] - fn non_bufferable_tokens_used_test_ok2() { - let non_bufferable_token = H160::from_low_u64_be(1); - let market_makable_token_list = HashSet::from([]); - - let token_amount = TokenAmount { - token: non_bufferable_token, - ..Default::default() - }; - - let interactions = vec![InteractionData { - inputs: vec![token_amount], - exec_plan: Some(ExecutionPlan { - internal: false, - ..Default::default() - }), - ..Default::default() - }]; - - assert_eq!( - non_bufferable_tokens_used(&interactions, &market_makable_token_list), - BTreeSet::new() - ); - } -} diff --git a/crates/solver/src/solver/http_solver/buffers.rs b/crates/solver/src/solver/http_solver/buffers.rs deleted file mode 100644 index d53e954442..0000000000 --- a/crates/solver/src/solver/http_solver/buffers.rs +++ /dev/null @@ -1,156 +0,0 @@ -use { - contracts::ERC20, - ethcontract::{batch::CallBatch, errors::MethodError, H160, U256}, - futures::{future::join_all, join}, - model::order::BUY_ETH_ADDRESS, - shared::ethrpc::Web3, - std::collections::HashMap, -}; - -const MAX_BATCH_SIZE: usize = 100; - -#[derive(Clone)] -/// Computes the amount of "buffer" ERC20 balance that the http solver can use -/// to offset possible rounding errors in computing the amounts in a solution. -pub struct BufferRetriever { - web3: Web3, - settlement_contract: H160, -} - -impl BufferRetriever { - pub fn new(web3: Web3, settlement_contract: H160) -> Self { - Self { - web3, - settlement_contract, - } - } -} - -#[derive(Debug)] -pub enum BufferRetrievalError { - Eth(web3::Error), - Erc20(MethodError), -} - -#[cfg_attr(test, mockall::automock)] -#[async_trait::async_trait] -pub trait BufferRetrieving: Send + Sync { - async fn get_buffers( - &self, - tokens: &[H160], - ) -> HashMap>; -} - -#[async_trait::async_trait] -impl BufferRetrieving for BufferRetriever { - async fn get_buffers( - &self, - tokens: &[H160], - ) -> HashMap> { - let mut batch = CallBatch::new(self.web3.transport()); - let tokens_without_eth: Vec<_> = tokens - .iter() - .filter(|&&address| address != BUY_ETH_ADDRESS) - .collect(); - - let futures = tokens_without_eth - .iter() - .map(|&&address| { - let erc20 = ERC20::at(&self.web3, address); - erc20 - .methods() - .balance_of(self.settlement_contract) - .batch_call(&mut batch) - }) - .collect::>(); - - let mut buffers = HashMap::new(); - - if tokens_without_eth.len() == tokens.len() { - batch.execute_all(MAX_BATCH_SIZE).await; - } else { - let (_, eth_balance) = join!( - batch.execute_all(MAX_BATCH_SIZE), - self.web3.eth().balance(self.settlement_contract, None) - ); - buffers.insert( - BUY_ETH_ADDRESS, - eth_balance.map_err(BufferRetrievalError::Eth), - ); - } - - buffers - .into_iter() - .chain( - tokens_without_eth - .into_iter() - .zip(join_all(futures).await) - .map(|(&address, balance)| { - (address, balance.map_err(BufferRetrievalError::Erc20)) - }), - ) - .collect() - } -} - -#[cfg(test)] -mod test { - use { - super::*, - contracts::GPv2Settlement, - hex_literal::hex, - shared::ethrpc::create_test_transport, - }; - - #[tokio::test] - #[ignore] - async fn retrieves_buffers_on_rinkeby() { - let web3 = Web3::new(create_test_transport( - &std::env::var("NODE_URL_RINKEBY").unwrap(), - )); - let settlement_contract = GPv2Settlement::deployed(&web3).await.unwrap(); - let weth = H160(hex!("c778417E063141139Fce010982780140Aa0cD5Ab")); - let dai = H160(hex!("c7ad46e0b8a400bb3c915120d284aafba8fc4735")); - let not_a_token = H160(hex!("badbadbadbadbadbadbadbadbadbadbadbadbadb")); - - let buffer_retriever = BufferRetriever::new(web3, settlement_contract.address()); - let buffers = buffer_retriever - .get_buffers(&[weth, dai, BUY_ETH_ADDRESS, not_a_token]) - .await; - println!("Buffers: {buffers:#?}"); - assert!(buffers.get(&weth).unwrap().is_ok()); - assert!(buffers.get(&dai).unwrap().is_ok()); - assert!(buffers.get(&BUY_ETH_ADDRESS).unwrap().is_ok()); - assert!(buffers.get(¬_a_token).unwrap().is_err()); - } - - #[tokio::test] - #[ignore] - async fn retrieving_buffers_not_affected_by_eth() { - let web3 = Web3::new(create_test_transport( - &std::env::var("NODE_URL_RINKEBY").unwrap(), - )); - let settlement_contract = GPv2Settlement::deployed(&web3).await.unwrap(); - let weth = H160(hex!("c778417E063141139Fce010982780140Aa0cD5Ab")); - let dai = H160(hex!("c7ad46e0b8a400bb3c915120d284aafba8fc4735")); - let not_a_token = H160(hex!("badbadbadbadbadbadbadbadbadbadbadbadbadb")); - - let buffer_retriever = BufferRetriever::new(web3, settlement_contract.address()); - let buffers_with_eth = buffer_retriever - .get_buffers(&[weth, dai, not_a_token]) - .await; - let buffers_without_eth = buffer_retriever - .get_buffers(&[weth, dai, not_a_token, BUY_ETH_ADDRESS]) - .await; - assert_eq!( - buffers_with_eth.get(&weth).unwrap().as_ref().unwrap(), - buffers_without_eth.get(&weth).unwrap().as_ref().unwrap() - ); - assert_eq!( - buffers_with_eth.get(&dai).unwrap().as_ref().unwrap(), - buffers_without_eth.get(&dai).unwrap().as_ref().unwrap() - ); - assert!(buffers_with_eth.get(¬_a_token).unwrap().is_err()); - assert!(buffers_without_eth.get(¬_a_token).unwrap().is_err()); - } -} diff --git a/crates/solver/src/solver/http_solver/instance_cache.rs b/crates/solver/src/solver/http_solver/instance_cache.rs deleted file mode 100644 index adb7f87c2d..0000000000 --- a/crates/solver/src/solver/http_solver/instance_cache.rs +++ /dev/null @@ -1,189 +0,0 @@ -use { - super::instance_creation::{InstanceCreator, Instances}, - crate::{s3_instance_upload::S3InstanceUploader, solver::Auction}, - model::auction::AuctionId, - once_cell::sync::OnceCell, - prometheus::IntCounterVec, - std::sync::Arc, - tokio::sync::Mutex, - tracing::{Instrument, Span}, -}; - -/// To `Driver` every http solver is presented as an individual `Solver` -/// implementor. Internally http solvers share the same data that is needed to -/// create the instance for the same auction. In order to waste less resources -/// we create the instance once per auction in this component. -pub struct SharedInstanceCreator { - creator: InstanceCreator, - uploader: Option>, - last: Mutex>, -} - -struct Cache { - run: u64, - // Arc because the instance data is big and only needs to be read. - instances: Arc, -} - -impl SharedInstanceCreator { - pub fn new(creator: InstanceCreator, uploader: Option>) -> Self { - Self { - creator, - uploader, - last: Default::default(), - } - } - - /// The first call for a new run id creates the instance and stores it in a - /// cache. Subsequent calls copy the instance from the cache (and block - /// until the first call completes). - pub async fn get_instances(&self, auction: Auction) -> Arc { - let mut guard = self.last.lock().await; - let cache: &Cache = match guard.as_ref() { - Some(cache) if cache.run == auction.run => cache, - _ => { - let instances = Arc::new( - self.creator - .prepare_instances( - auction.id, - auction.run, - auction.orders, - auction.liquidity, - auction.gas_price, - &auction.external_prices, - auction.balances, - ) - .await, - ); - self.upload_instance_in_background(auction.id, instances.clone()); - *guard = Some(Cache { - run: auction.run, - instances, - }); - // Unwrap because we just assigned Some. - guard.as_ref().unwrap() - } - }; - cache.instances.clone() - } - - // Happens in a task to not delay solving. - fn upload_instance_in_background(&self, id: AuctionId, instances: Arc) { - if let Some(uploader) = &self.uploader { - let uploader = uploader.clone(); - let task = async move { - let auction = match serde_json::to_vec(&instances.plain) { - Ok(auction) => auction, - Err(err) => { - tracing::error!(?err, "encode auction for instance upload"); - return; - } - }; - std::mem::drop(instances); - - let label = match uploader.upload_instance(id, &auction).await { - Ok(()) => "success", - Err(err) => { - tracing::warn!(%id, ?err, "error uploading instance"); - "failure" - } - }; - Metrics::get() - .instance_cache_uploads - .with_label_values(&[label]) - .inc(); - }; - tokio::task::spawn(task.instrument(Span::current())); - } - } -} - -#[derive(prometheus_metric_storage::MetricStorage)] -pub struct Metrics { - /// Auction filtered orders grouped by class. - #[metric(labels("result"))] - instance_cache_uploads: IntCounterVec, -} - -impl Metrics { - fn get() -> &'static Self { - static INIT: OnceCell<&'static Metrics> = OnceCell::new(); - INIT.get_or_init(|| { - let metrics = Metrics::instance(observe::metrics::get_storage_registry()).unwrap(); - for result in ["success", "failure"] { - metrics - .instance_cache_uploads - .with_label_values(&[result]) - .reset(); - } - metrics - }) - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::solver::http_solver::buffers::MockBufferRetrieving, - contracts::{dummy_contract, WETH9}, - model::order::{Order, OrderData}, - primitive_types::U256, - shared::token_info::{MockTokenInfoFetching, TokenInfo}, - }; - - #[tokio::test] - async fn cache_ok() { - let mut token_infos = MockTokenInfoFetching::new(); - token_infos.expect_get_token_infos().returning(|tokens| { - tokens - .iter() - .map(|token| (*token, TokenInfo::default())) - .collect() - }); - let mut buffer_retriever = MockBufferRetrieving::new(); - buffer_retriever.expect_get_buffers().returning(|tokens| { - tokens - .iter() - .map(|token| (*token, Ok(U256::zero()))) - .collect() - }); - let creator = InstanceCreator { - native_token: dummy_contract!(WETH9, [0x00; 20]), - ethflow_contract: None, - token_info_fetcher: Arc::new(token_infos), - buffer_retriever: Arc::new(buffer_retriever), - market_makable_token_list: Default::default(), - environment_metadata: Default::default(), - }; - let shared = SharedInstanceCreator::new(creator, None); - - // Query the cache a couple of times with different auction and run ids. We - // check whether the inner instance creator has been called by comparing - // the size of orders vec in the model. - - let mut auction = Auction::default(); - let instance = shared.get_instances(auction.clone()).await; - assert_eq!(instance.plain.orders.len(), 0); - - // Size stays the same even though auction has one more order because cached - // result is used because id in cache. - auction.orders.push(Order { - data: OrderData { - sell_amount: 1.into(), - buy_amount: 1.into(), - ..Default::default() - }, - ..Default::default() - }); - auction.balances = crate::order_balance_filter::max_balance(&auction.orders); - let instance = shared.get_instances(auction.clone()).await; - assert_eq!(instance.plain.orders.len(), 0); - - // Size changes because id changes. - auction.id = 1; - auction.run = 1; - let instance = shared.get_instances(auction).await; - assert_eq!(instance.plain.orders.len(), 1); - } -} diff --git a/crates/solver/src/solver/http_solver/instance_creation.rs b/crates/solver/src/solver/http_solver/instance_creation.rs deleted file mode 100644 index a92d09cd83..0000000000 --- a/crates/solver/src/solver/http_solver/instance_creation.rs +++ /dev/null @@ -1,620 +0,0 @@ -use { - super::{ - buffers::{BufferRetrievalError, BufferRetrieving}, - settlement::SettlementContext, - }, - crate::liquidity::{order_converter::OrderConverter, Exchange, LimitOrder, Liquidity}, - anyhow::{Context, Result}, - contracts::WETH9, - ethcontract::{errors::ExecutionError, U256}, - itertools::{Either, Itertools as _}, - maplit::{btreemap, hashset}, - model::{ - auction::AuctionId, - order::{Order, OrderKind}, - }, - num::{BigInt, BigRational}, - primitive_types::H160, - shared::{ - external_prices::ExternalPrices, - http_solver::{gas_model::GasModel, model::*}, - sources::balancer_v2::pools::common::compute_scaling_rate, - token_info::{TokenInfo, TokenInfoFetching}, - token_list::AutoUpdatingTokenList, - }, - std::{ - collections::{BTreeMap, HashMap, HashSet}, - iter::FromIterator as _, - sync::Arc, - }, -}; - -pub struct Instances { - pub plain: BatchAuctionModel, - pub filtered: BatchAuctionModel, - pub context: SettlementContext, -} - -pub struct InstanceCreator { - pub native_token: WETH9, - pub ethflow_contract: Option, - pub token_info_fetcher: Arc, - pub buffer_retriever: Arc, - pub market_makable_token_list: AutoUpdatingTokenList, - pub environment_metadata: String, -} - -impl InstanceCreator { - #[allow(clippy::too_many_arguments)] - pub async fn prepare_instances( - &self, - auction_id: AuctionId, - run_id: u64, - orders: Vec, - liquidity: Vec, - gas_price: f64, - external_prices: &ExternalPrices, - balances: HashMap, - ) -> Instances { - let converter = OrderConverter { - native_token: self.native_token.clone(), - }; - let mut orders = crate::solver::balance_and_convert_orders( - self.ethflow_contract, - &converter, - balances, - orders, - external_prices, - ); - // The HTTP solver interface expects liquidity limit orders (like 0x - // limit orders) to be added to the `orders` models and NOT the - // `liquidity` models. Split the two here to avoid indexing errors - // later on. - let (limit_orders, amms): (Vec<_>, Vec<_>) = - liquidity - .into_iter() - .partition_map(|liquidity| match liquidity { - Liquidity::LimitOrder(limit_order) => Either::Left(limit_order), - amm => Either::Right(amm), - }); - orders.extend(limit_orders); - - let market_makable_token_list = self.market_makable_token_list.all(); - - let tokens = map_tokens_for_solver(&orders, &amms, &market_makable_token_list); - let (token_infos, buffers_result) = futures::join!( - shared::measure_time( - self.token_info_fetcher.get_token_infos(tokens.as_slice()), - |duration| tracing::debug!("get_token_infos took {} s", duration.as_secs_f32()), - ), - shared::measure_time( - self.buffer_retriever.get_buffers(tokens.as_slice()), - |duration| tracing::debug!("get_buffers took {} s", duration.as_secs_f32()), - ), - ); - - let buffers: HashMap<_, _> = buffers_result - .into_iter() - .filter_map(|(token, buffer)| match buffer { - Err(BufferRetrievalError::Erc20(err)) if is_transaction_failure(&err.inner) => { - tracing::debug!( - "Failed to fetch buffers for token {} with transaction failure {}", - token, - err - ); - None - } - Err(err) => { - tracing::error!( - "Failed to fetch buffers contract balance for token {} with error {:?}", - token, - err - ); - None - } - Ok(b) => Some((token, b)), - }) - .collect(); - - // We are guaranteed to have price estimates for all tokens that are relevant to - // the objective value by the driver. It is possible that we have AMM - // pools that contain tokens that are not any order's tokens. We used to - // fetch these extra prices but it would often slow down the solver and - // the solver can estimate them on its own. - let price_estimates = external_prices.into_http_solver_prices(); - - let gas_model = GasModel { - native_token: self.native_token.address(), - gas_price, - }; - - // Some solvers require that there are no isolated islands of orders whose - // tokens are unconnected to the native token. - let fee_connected_tokens: HashSet = - compute_fee_connected_tokens(&amms, self.native_token.address()); - let filtered_order_models = order_models(&orders, &fee_connected_tokens, &gas_model); - - let tokens: HashSet = tokens.into_iter().collect(); - let order_models = order_models(&orders, &tokens, &gas_model); - - let token_models = token_models( - &token_infos, - &price_estimates, - &buffers, - &gas_model, - &market_makable_token_list, - ); - - let amm_models = amm_models(&amms, &gas_model); - - let model = BatchAuctionModel { - tokens: token_models, - orders: order_models, - amms: amm_models, - metadata: Some(MetadataModel { - environment: Some(self.environment_metadata.clone()), - auction_id: Some(auction_id), - run_id: Some(run_id), - gas_price: Some(gas_price), - native_token: Some(self.native_token.address()), - }), - }; - - let mut filtered_model = model.clone(); - filtered_model.orders = filtered_order_models; - - let context = SettlementContext { - orders, - liquidity: amms, - }; - - Instances { - plain: model, - filtered: filtered_model, - context, - } - } -} - -fn map_tokens_for_solver( - orders: &[LimitOrder], - liquidity: &[Liquidity], - market_makable_token_list: &HashSet, -) -> Vec { - let mut token_set = HashSet::new(); - token_set.extend( - orders - .iter() - .flat_map(|order| [order.sell_token, order.buy_token]), - ); - for liquidity in liquidity.iter() { - match liquidity { - Liquidity::ConstantProduct(amm) => token_set.extend(amm.tokens), - Liquidity::BalancerWeighted(amm) => token_set.extend(amm.reserves.keys()), - Liquidity::BalancerStable(amm) => token_set.extend(amm.reserves.keys()), - Liquidity::LimitOrder(_) => panic!("limit orders are expected to be filtered out"), - Liquidity::Concentrated(amm) => token_set.extend(amm.tokens), - } - } - token_set.extend(market_makable_token_list); - - Vec::from_iter(token_set) -} - -fn token_models( - token_infos: &HashMap, - price_estimates: &HashMap, - buffers: &HashMap, - gas_model: &GasModel, - market_makable_token_list: &HashSet, -) -> BTreeMap { - token_infos - .iter() - .map(|(address, token_info)| { - let external_price = match price_estimates.get(address).copied() { - Some(price) if price.is_finite() => Some(price), - _ => None, - }; - ( - *address, - TokenInfoModel { - decimals: token_info.decimals, - alias: token_info.symbol.clone(), - external_price, - normalize_priority: Some(u64::from(&gas_model.native_token == address)), - internal_buffer: buffers.get(address).copied(), - accepted_for_internalization: market_makable_token_list.contains(address), - }, - ) - }) - .collect() -} - -fn order_models( - orders: &[LimitOrder], - fee_connected_tokens: &HashSet, - gas_model: &GasModel, -) -> BTreeMap { - orders - .iter() - .enumerate() - .filter_map(|(index, order)| { - if ![order.sell_token, order.buy_token] - .iter() - .any(|token| fee_connected_tokens.contains(token)) - { - return None; - } - - let cost = match order.exchange { - Exchange::GnosisProtocol => gas_model.gp_order_cost(), - Exchange::ZeroEx => gas_model.zeroex_order_cost(), - }; - - Some(( - index, - OrderModel { - id: order.id.order_uid(), - sell_token: order.sell_token, - buy_token: order.buy_token, - sell_amount: order.sell_amount, - buy_amount: order.buy_amount, - allow_partial_fill: order.partially_fillable, - is_sell_order: matches!(order.kind, OrderKind::Sell), - fee: TokenAmount { - amount: order.user_fee, - token: order.sell_token, - }, - cost, - is_liquidity_order: order.is_liquidity_order(), - mandatory: false, - has_atomic_execution: !matches!(order.exchange, Exchange::GnosisProtocol), - reward: 0., - is_mature: true, - }, - )) - }) - .collect() -} - -fn amm_models(liquidity: &[Liquidity], gas_model: &GasModel) -> BTreeMap { - liquidity - .iter() - .map(|liquidity| -> Result<_> { - Ok(match liquidity { - Liquidity::ConstantProduct(amm) => AmmModel { - parameters: AmmParameters::ConstantProduct(ConstantProductPoolParameters { - reserves: btreemap! { - amm.tokens.get().0 => amm.reserves.0.into(), - amm.tokens.get().1 => amm.reserves.1.into(), - }, - }), - fee: BigRational::new( - BigInt::from(*amm.fee.numer()), - BigInt::from(*amm.fee.denom()), - ), - cost: gas_model.uniswap_cost(), - mandatory: false, - address: amm.address, - }, - Liquidity::BalancerWeighted(amm) => AmmModel { - parameters: AmmParameters::WeightedProduct(WeightedProductPoolParameters { - reserves: amm - .reserves - .iter() - .map(|(token, state)| { - ( - *token, - WeightedPoolTokenData { - balance: state.common.balance, - weight: BigRational::from(state.weight), - }, - ) - }) - .collect(), - }), - fee: amm.fee.into(), - cost: gas_model.balancer_cost(), - mandatory: false, - address: amm.address, - }, - Liquidity::BalancerStable(amm) => AmmModel { - parameters: AmmParameters::Stable(StablePoolParameters { - reserves: amm - .reserves_without_bpt() - .map(|(token, state)| (token, state.balance)) - .collect(), - scaling_rates: amm - .reserves_without_bpt() - .map(|(token, state)| { - Ok((token, compute_scaling_rate(state.scaling_factor)?)) - }) - .collect::>() - .with_context(|| { - format!("error converting stable pool to solver model: {amm:?}") - })?, - amplification_parameter: amm.amplification_parameter.as_big_rational(), - }), - fee: amm.fee.into(), - cost: gas_model.balancer_cost(), - mandatory: false, - address: amm.address, - }, - Liquidity::LimitOrder(_) => panic!("limit orders are expected to be filtered out"), - Liquidity::Concentrated(amm) => AmmModel { - parameters: AmmParameters::Concentrated(ConcentratedPoolParameters { - pool: amm.pool.clone(), - }), - fee: BigRational::new( - BigInt::from(*amm.pool.state.fee.numer()), - BigInt::from(*amm.pool.state.fee.denom()), - ), - cost: gas_model.cost_for_gas(amm.pool.gas_stats.mean_gas), - mandatory: false, - address: amm.pool.address, - }, - }) - }) - .filter_map(|result| match result { - Ok(value) => Some((value.address, value)), - Err(err) => { - tracing::error!(?err, "error converting liquidity to solver model"); - None - } - }) - .collect() -} - -fn compute_fee_connected_tokens(liquidity: &[Liquidity], native_token: H160) -> HashSet { - // Find all tokens that are connected through potentially multiple amm hops to - // the fee. TODO: Replace with a more optimal graph algorithm. - let mut pairs = liquidity - .iter() - .flat_map(|amm| amm.all_token_pairs()) - .collect::>(); - let mut fee_connected_tokens = hashset![native_token]; - loop { - let mut added_token = false; - pairs.retain(|token_pair| { - let tokens = token_pair.get(); - if fee_connected_tokens.contains(&tokens.0) { - fee_connected_tokens.insert(tokens.1); - added_token = true; - false - } else if fee_connected_tokens.contains(&tokens.1) { - fee_connected_tokens.insert(tokens.0); - added_token = true; - false - } else { - true - } - }); - if pairs.is_empty() || !added_token { - break; - } - } - - fee_connected_tokens -} - -/// Failure indicating the transaction reverted for some reason -fn is_transaction_failure(error: &ExecutionError) -> bool { - matches!(error, ExecutionError::Failure(_)) - || matches!(error, ExecutionError::Revert(_)) - || matches!(error, ExecutionError::InvalidOpcode) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - liquidity::{tests::CapturingSettlementHandler, ConstantProductOrder}, - order_balance_filter::max_balance, - solver::http_solver::buffers::MockBufferRetrieving, - }, - contracts::dummy_contract, - maplit::hashmap, - model::{order::OrderData, TokenPair}, - shared::{externalprices, token_info::MockTokenInfoFetching}, - }; - - #[tokio::test] - async fn remove_orders_without_native_connection_() { - let amm_handling = CapturingSettlementHandler::arc(); - - let native_token = H160::from_low_u64_be(0); - let tokens = [ - H160::from_low_u64_be(1), - H160::from_low_u64_be(2), - H160::from_low_u64_be(3), - H160::from_low_u64_be(4), - ]; - let external_prices = ExternalPrices::new( - native_token, - hashmap! { - native_token => BigRational::from_float(1.).unwrap(), - tokens[0] => BigRational::from_float(1.).unwrap(), - tokens[1] => BigRational::from_float(1.).unwrap(), - tokens[2] => BigRational::from_float(1.).unwrap(), - tokens[3] => BigRational::from_float(1.).unwrap(), - }, - ) - .unwrap(); - - let amms = [(native_token, tokens[0]), (tokens[0], tokens[1])] - .iter() - .map(|tokens| { - Liquidity::ConstantProduct(ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(tokens.0, tokens.1).unwrap(), - reserves: (0, 0), - fee: 0.into(), - settlement_handling: amm_handling.clone(), - }) - }) - .collect::>(); - - let orders = [ - (native_token, tokens[0]), - (native_token, tokens[1]), - (tokens[0], tokens[1]), - (tokens[1], tokens[0]), - (tokens[1], tokens[2]), - (tokens[2], tokens[1]), - (tokens[2], tokens[3]), - (tokens[3], tokens[2]), - ] - .iter() - .map(|tokens| Order { - data: OrderData { - sell_token: tokens.0, - buy_token: tokens.1, - sell_amount: 1.into(), - buy_amount: 1.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - ..Default::default() - }) - .collect::>(); - - let mut token_infos = MockTokenInfoFetching::new(); - token_infos.expect_get_token_infos().returning(|tokens| { - tokens - .iter() - .map(|token| (*token, TokenInfo::default())) - .collect() - }); - - let mut buffer_retriever = MockBufferRetrieving::new(); - buffer_retriever.expect_get_buffers().returning(|tokens| { - tokens - .iter() - .map(|token| (*token, Ok(U256::zero()))) - .collect() - }); - - let solver = InstanceCreator { - native_token: dummy_contract!(WETH9, [0x00; 20]), - ethflow_contract: None, - token_info_fetcher: Arc::new(token_infos), - buffer_retriever: Arc::new(buffer_retriever), - market_makable_token_list: Default::default(), - environment_metadata: Default::default(), - }; - - let balances = max_balance(&orders); - let instances = solver - .prepare_instances(0, 0, orders, amms, 0., &external_prices, balances) - .await; - assert_eq!(instances.filtered.orders.len(), 6); - assert_eq!(instances.plain.orders.len(), 8); - } - - #[tokio::test] - async fn prepares_models_with_mixed_liquidity() { - let address = |x| H160([x; 20]); - let native_token = address(0xef); - - let mut token_infos = MockTokenInfoFetching::new(); - token_infos.expect_get_token_infos().returning(|tokens| { - tokens - .iter() - .map(|token| (*token, TokenInfo::default())) - .collect() - }); - - let mut buffer_retriever = MockBufferRetrieving::new(); - buffer_retriever.expect_get_buffers().returning(|tokens| { - tokens - .iter() - .map(|token| (*token, Ok(U256::zero()))) - .collect() - }); - - let solver = InstanceCreator { - native_token: dummy_contract!(WETH9, [0x00; 20]), - ethflow_contract: None, - token_info_fetcher: Arc::new(token_infos), - buffer_retriever: Arc::new(buffer_retriever), - market_makable_token_list: Default::default(), - environment_metadata: Default::default(), - }; - - let orders = vec![ - Order { - data: OrderData { - sell_token: address(1), - buy_token: address(2), - sell_amount: 1.into(), - buy_amount: 2.into(), - ..Default::default() - }, - ..Default::default() - }, - Order { - data: OrderData { - sell_token: address(3), - buy_token: address(4), - sell_amount: 3.into(), - buy_amount: 4.into(), - ..Default::default() - }, - ..Default::default() - }, - ]; - let balances = max_balance(&orders); - - let instances = solver - .prepare_instances( - 42, - 1337, - orders, - vec![ - Liquidity::ConstantProduct(ConstantProductOrder { - address: address(0x56), - tokens: TokenPair::new(address(5), address(6)).unwrap(), - reserves: (5, 6), - ..Default::default() - }), - Liquidity::LimitOrder(LimitOrder { - sell_token: address(7), - buy_token: address(8), - sell_amount: 7.into(), - buy_amount: 8.into(), - ..Default::default() - }), - Liquidity::ConstantProduct(ConstantProductOrder { - address: address(0x9a), - tokens: TokenPair::new(address(9), address(10)).unwrap(), - reserves: (9, 10), - ..Default::default() - }), - ], - 1e9, - &externalprices! { - native_token: native_token, - address(1) => BigRational::new(1.into(), 1.into()), - address(2) => BigRational::new(2.into(), 2.into()), - address(3) => BigRational::new(3.into(), 3.into()), - address(4) => BigRational::new(4.into(), 4.into()), - }, - balances, - ) - .await; - - assert_btreemap_size(&instances.plain.orders, 3); - assert_eq!(instances.plain.amms.len(), 2); - - assert_eq!(instances.context.orders.len(), 3); - assert_eq!(instances.context.liquidity.len(), 2); - } - - fn assert_btreemap_size(map: &BTreeMap, len: usize) { - assert_eq!(map.len(), len); - for i in 0..len { - assert!(map.contains_key(&i)); - } - } -} diff --git a/crates/solver/src/solver/http_solver/settlement.rs b/crates/solver/src/solver/http_solver/settlement.rs deleted file mode 100644 index ced4aba4ec..0000000000 --- a/crates/solver/src/solver/http_solver/settlement.rs +++ /dev/null @@ -1,1295 +0,0 @@ -use { - crate::{ - interactions::allowances::{AllowanceManaging, Approval, ApprovalRequest}, - liquidity::{ - order_converter::OrderConverter, - slippage::SlippageContext, - AmmOrderExecution, - LimitOrder, - LimitOrderExecution, - LimitOrderId, - Liquidity, - }, - order_balance_filter::BalancedOrder, - settlement::Settlement, - }, - anyhow::{anyhow, ensure, Context as _, Result}, - model::{ - order::{Order, OrderClass, OrderKind, OrderMetadata}, - DomainSeparator, - }, - primitive_types::{H160, U256}, - shared::http_solver::model::*, - std::{ - collections::{hash_map::Entry, HashMap, HashSet}, - sync::Arc, - }, -}; - -// To send an instance to the solver we need to identify tokens and orders -// through strings. This struct combines the created model and a mapping of -// those identifiers to their original value. -#[derive(Clone, Debug)] -pub struct SettlementContext { - pub orders: Vec, - pub liquidity: Vec, -} - -pub async fn convert_settlement( - settled: SettledBatchAuctionModel, - context: &SettlementContext, - allowance_manager: Arc, - order_converter: &OrderConverter, - slippage: SlippageContext<'_>, - domain: &DomainSeparator, - enforce_correct_fees: bool, -) -> Result { - IntermediateSettlement::new( - settled, - context, - allowance_manager, - order_converter, - slippage, - domain, - enforce_correct_fees, - ) - .await? - .into_settlement() - .map_err(Into::into) -} - -#[derive(Clone, Debug)] -#[cfg_attr(test, derive(PartialEq))] -enum Execution { - Amm(Box), - CustomInteraction(Box), - LimitOrder(Box), -} - -impl Execution { - fn execution_plan(&self) -> Option<&ExecutionPlan> { - match self { - Execution::Amm(executed_amm) => Some(&executed_amm.exec_plan), - Execution::CustomInteraction(interaction) => interaction.exec_plan.as_ref(), - Execution::LimitOrder(order) => order.exec_plan.as_ref(), - } - } - - fn coordinates(&self) -> Option { - self.execution_plan() - .map(|exec_plan| exec_plan.coordinates.clone()) - } - - fn add_to_settlement( - &self, - settlement: &mut Settlement, - slippage: &SlippageContext, - internalizable: bool, - enforce_correct_fees: bool, - ) -> Result<()> { - match self { - Execution::LimitOrder(order) => { - let fee = match order.order.solver_determines_fee() { - true => { - let fee = order.executed_fee_amount; - match enforce_correct_fees { - true => fee.context("no fee for limit order")?, - false => fee.unwrap_or_default(), - } - } - false => order.order.user_fee, - }; - - let execution = LimitOrderExecution { - filled: order.executed_amount(), - fee, - }; - - settlement.with_liquidity(&order.order, execution) - } - Execution::Amm(executed_amm) => { - let execution = slippage.apply_to_amm_execution(AmmOrderExecution { - input_max: executed_amm.input.clone(), - output: executed_amm.output.clone(), - internalizable, - })?; - match &executed_amm.order { - Liquidity::ConstantProduct(liquidity) => { - settlement.with_liquidity(liquidity, execution) - } - Liquidity::BalancerWeighted(liquidity) => { - settlement.with_liquidity(liquidity, execution) - } - Liquidity::BalancerStable(liquidity) => { - settlement.with_liquidity(liquidity, execution) - } - // This sort of liquidity gets used elsewhere - Liquidity::LimitOrder(_) => Ok(()), - Liquidity::Concentrated(liquidity) => { - settlement.with_liquidity(liquidity, execution) - } - } - } - Execution::CustomInteraction(interaction_data) => { - settlement.encoder.append_to_execution_plan_internalizable( - Arc::new(*interaction_data.clone()), - internalizable, - ); - Ok(()) - } - } - } -} - -// An intermediate representation between SettledBatchAuctionModel and -// Settlement useful for doing the error checking up front and then working with -// a more convenient representation. -struct IntermediateSettlement<'a> { - approvals: Vec, - executions: Vec, // executions are sorted by execution coordinate. - prices: HashMap, - slippage: SlippageContext<'a>, - score: Score, - // Causes either an error or a fee of 0 whenever a fee is expected but none was provided. - enforce_correct_fees: bool, -} - -// Conversion error happens during building a settlement from a solution -// received from searcher -#[derive(Debug)] -pub enum ConversionError { - InvalidExecutionPlans(anyhow::Error), - Other(anyhow::Error), -} - -impl From for ConversionError { - fn from(err: anyhow::Error) -> Self { - Self::Other(err) - } -} - -impl From for anyhow::Error { - fn from(err: ConversionError) -> Self { - match err { - ConversionError::InvalidExecutionPlans(err) => err, - ConversionError::Other(err) => err, - } - } -} - -#[derive(Clone, Debug)] -#[cfg_attr(test, derive(PartialEq))] -struct ExecutedLimitOrder { - order: LimitOrder, - executed_buy_amount: U256, - executed_sell_amount: U256, - /// The fee for this order execution computed by the solver. - /// This exact number of sell token atoms will be kept by the protocol for - /// this trade execution. It will also be used in the objective value - /// computation. - executed_fee_amount: Option, - exec_plan: Option, -} - -impl ExecutedLimitOrder { - fn executed_amount(&self) -> U256 { - match self.order.kind { - OrderKind::Buy => self.executed_buy_amount, - OrderKind::Sell => self.executed_sell_amount, - } - } -} - -#[derive(Clone, Debug)] -#[cfg_attr(test, derive(PartialEq))] -struct ExecutedAmm { - input: TokenAmount, - output: TokenAmount, - order: Liquidity, - exec_plan: ExecutionPlan, -} - -impl<'a> IntermediateSettlement<'a> { - async fn new( - settled: SettledBatchAuctionModel, - context: &SettlementContext, - allowance_manager: Arc, - order_converter: &OrderConverter, - slippage: SlippageContext<'a>, - domain: &DomainSeparator, - enforce_correct_fees: bool, - ) -> Result, ConversionError> { - let executed_limit_orders = - match_prepared_and_settled_orders(&context.orders, settled.orders)?; - let foreign_liquidity_orders = convert_foreign_liquidity_orders( - order_converter, - settled.foreign_liquidity_orders, - domain, - )?; - let prices = match_settled_prices(executed_limit_orders.as_slice(), settled.prices)?; - let approvals = compute_approvals(allowance_manager, settled.approvals).await?; - let executions_amm = match_prepared_and_settled_amms(&context.liquidity, settled.amms)?; - - let executions = merge_and_order_executions( - executions_amm, - settled.interaction_data, - [executed_limit_orders, foreign_liquidity_orders].concat(), - ); - let score = settled.score; - - if duplicate_coordinates(&executions) { - return Err(ConversionError::InvalidExecutionPlans(anyhow!( - "Duplicate coordinates found." - ))); - } - - Ok(Self { - executions, - prices, - approvals, - slippage, - score, - enforce_correct_fees, - }) - } - - fn into_settlement(self) -> Result { - let mut settlement = Settlement::new(self.prices); - settlement.score = self.score; - - // Make sure to always add approval interactions **before** any - // interactions from the execution plan - the execution plan typically - // consists of AMM swaps that require these approvals to be in place. - for approval in self.approvals { - settlement - .encoder - .append_to_execution_plan(Arc::new(approval)); - } - - for execution in &self.executions { - let internalizable = execution - .execution_plan() - .map(|exec_plan| exec_plan.internal) - .unwrap_or_default(); - execution.add_to_settlement( - &mut settlement, - &self.slippage, - internalizable, - self.enforce_correct_fees, - )?; - } - - Ok(settlement) - } -} - -fn match_prepared_and_settled_orders( - prepared_orders: &[LimitOrder], - settled_orders: HashMap, -) -> Result> { - settled_orders - .into_iter() - .filter(|(_, settled)| { - !(settled.exec_sell_amount.is_zero() && settled.exec_buy_amount.is_zero()) - }) - .map(|(index, settled)| { - let prepared = prepared_orders - .get(index) - .ok_or_else(|| anyhow!("invalid order {}", index))?; - if prepared.is_liquidity_order() { - if let Some(internalizable) = settled.exec_plan.as_ref().map(|plan| plan.internal) { - ensure!( - !internalizable, - "liquidity orders are not allowed to be internalizable" - ) - } - } - Ok(ExecutedLimitOrder { - order: prepared.clone(), - executed_buy_amount: settled.exec_buy_amount, - executed_sell_amount: settled.exec_sell_amount, - exec_plan: settled.exec_plan, - executed_fee_amount: settled.exec_fee_amount, - }) - }) - .collect() -} - -fn convert_foreign_liquidity_orders( - order_converter: &OrderConverter, - foreign_liquidity_orders: Vec, - domain: &DomainSeparator, -) -> Result> { - foreign_liquidity_orders - .into_iter() - .map(|liquidity| { - let order = Order { - metadata: OrderMetadata { - owner: liquidity.order.from, - full_fee_amount: liquidity.order.data.fee_amount, - // All foreign orders **MUST** be liquidity, this is - // important so they cannot be used to affect the objective. - class: OrderClass::Liquidity, - // Not needed for encoding but nice to have for logs and competition info. - uid: liquidity.order.data.uid(domain, &liquidity.order.from), - // These remaining fields do not seem to be used at all for order - // encoding, so we just use the default values. - ..Default::default() - }, - data: liquidity.order.data, - signature: liquidity.order.signature, - interactions: Default::default(), - }; - let converted = order_converter.normalize_limit_order(BalancedOrder::full(order))?; - Ok(ExecutedLimitOrder { - order: converted, - executed_sell_amount: liquidity.exec_sell_amount, - executed_buy_amount: liquidity.exec_buy_amount, - executed_fee_amount: None, - exec_plan: None, - }) - }) - .collect() -} - -fn match_prepared_and_settled_amms( - prepared_amms: &[Liquidity], - settled_amms: HashMap, -) -> Result> { - let prepared_amms: HashMap = prepared_amms - .iter() - .filter_map(|amm| amm.address().map(|address| (address, amm))) - .collect(); - settled_amms - .into_iter() - .filter(|(_, settled)| settled.is_non_trivial()) - .flat_map(|(address, settled)| { - settled - .execution - .into_iter() - .map(move |exec| (address, exec)) - }) - .map(|(address, settled)| { - Ok(ExecutedAmm { - order: prepared_amms - .get(&address) - .copied() - .ok_or_else(|| anyhow!("Invalid AMM {}", address))? - .clone(), - input: TokenAmount { - token: settled.buy_token, - amount: settled.exec_buy_amount, - }, - output: TokenAmount { - token: settled.sell_token, - amount: settled.exec_sell_amount, - }, - exec_plan: settled.exec_plan, - }) - }) - .collect() -} - -fn merge_and_order_executions( - executions_amms: Vec, - interactions: Vec, - orders: Vec, -) -> Vec { - let mut executions: Vec<_> = executions_amms - .into_iter() - .map(|amm| Execution::Amm(Box::new(amm))) - .chain( - interactions - .into_iter() - .map(|interaction| Execution::CustomInteraction(Box::new(interaction))), - ) - .chain( - orders - .into_iter() - .map(|order| Execution::LimitOrder(Box::new(order))), - ) - .collect(); - // executions with optional execution plan will be executed first - executions.sort_by_key(|execution| execution.coordinates()); - executions -} - -fn match_settled_prices( - executed_limit_orders: &[ExecutedLimitOrder], - solver_prices: HashMap, -) -> Result> { - let mut prices = HashMap::new(); - let executed_tokens = executed_limit_orders - .iter() - .flat_map(|order| match order.order.id { - LimitOrderId::Market(_) | LimitOrderId::Limit(_) => { - vec![order.order.buy_token, order.order.sell_token] - } - LimitOrderId::Liquidity(_) => vec![], - }); - for token in executed_tokens { - if let Entry::Vacant(entry) = prices.entry(token) { - let price = solver_prices - .get(&token) - .ok_or_else(|| anyhow!("invalid token {}", token))?; - entry.insert(*price); - } - } - Ok(prices) -} - -async fn compute_approvals( - allowance_manager: Arc, - approvals: Vec, -) -> Result> { - if approvals.is_empty() { - return Ok(Vec::new()); - } - - let requests = approvals - .into_iter() - .try_fold(HashMap::new(), |mut grouped, approval| { - let amount = grouped - .entry((approval.token, approval.spender)) - .or_insert(U256::zero()); - *amount = amount - .checked_add(approval.amount) - .context("overflow when computing total approval amount")?; - - Result::<_>::Ok(grouped) - })? - .into_iter() - .map(|((token, spender), amount)| ApprovalRequest { - token, - spender, - amount, - }) - .collect::>(); - - allowance_manager.get_approvals(&requests).await -} - -/// Check if executions contain execution plans with the same coordinates -fn duplicate_coordinates(executions: &[Execution]) -> bool { - let mut coordinates = HashSet::new(); - executions.iter().any(|execution| { - execution - .coordinates() - .map(|coordinate| !coordinates.insert(coordinate)) - .unwrap_or(false) - }) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - interactions::allowances::MockAllowanceManaging, - liquidity::{ - tests::CapturingSettlementHandler, - ConstantProductOrder, - LiquidityOrderId, - StablePoolOrder, - WeightedProductOrder, - }, - settlement::{PricedTrade, Trade}, - }, - hex_literal::hex, - maplit::{btreemap, hashmap}, - model::{ - order::{OrderData, OrderUid}, - signature::Signature, - TokenPair, - }, - num::rational::Ratio, - shared::sources::balancer_v2::{ - pool_fetching::{AmplificationParameter, TokenState, WeightedTokenState}, - swap::fixed_point::Bfp, - }, - }; - - #[tokio::test] - async fn convert_settlement_() { - let weth = H160([0xe7; 20]); - - let t0 = H160::zero(); - let t1 = H160::from_low_u64_be(1); - - let limit_handler = CapturingSettlementHandler::arc(); - let orders = vec![LimitOrder { - sell_token: t0, - buy_token: t1, - sell_amount: 1.into(), - buy_amount: 2.into(), - kind: OrderKind::Sell, - settlement_handling: limit_handler.clone(), - id: 0.into(), - ..Default::default() - }]; - - let cp_amm_handler = CapturingSettlementHandler::arc(); - let internal_amm_handler = CapturingSettlementHandler::arc(); - let wp_amm_handler = CapturingSettlementHandler::arc(); - let sp_amm_handler = CapturingSettlementHandler::arc(); - let liquidity = vec![ - Liquidity::ConstantProduct(ConstantProductOrder { - address: H160::from_low_u64_be(0), - tokens: TokenPair::new(t0, t1).unwrap(), - reserves: (3, 4), - fee: 5.into(), - settlement_handling: cp_amm_handler.clone(), - }), - Liquidity::ConstantProduct(ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(t0, t1).unwrap(), - reserves: (6, 7), - fee: 8.into(), - settlement_handling: internal_amm_handler.clone(), - }), - Liquidity::BalancerWeighted(WeightedProductOrder { - address: H160::from_low_u64_be(2), - reserves: btreemap! { - t0 => WeightedTokenState { - common: TokenState { - balance: U256::from(200), - scaling_factor: Bfp::exp10(4), - }, - weight: Bfp::from(200_000_000_000_000_000), - }, - t1 => WeightedTokenState { - common: TokenState { - balance: U256::from(800), - scaling_factor: Bfp::exp10(6), - }, - weight: Bfp::from(800_000_000_000_000_000), - } - }, - fee: "0.03".parse().unwrap(), - version: Default::default(), - settlement_handling: wp_amm_handler.clone(), - }), - Liquidity::BalancerStable(StablePoolOrder { - address: H160::from_low_u64_be(3), - reserves: btreemap! { - t0 => TokenState { - balance: U256::from(300), - scaling_factor: Bfp::exp10(0), - }, - t1 => TokenState { - balance: U256::from(400), - scaling_factor: Bfp::exp10(0), - }, - }, - fee: "3".parse().unwrap(), - amplification_parameter: AmplificationParameter::new(1.into(), 1.into()).unwrap(), - settlement_handling: sp_amm_handler.clone(), - }), - ]; - - let executed_order = ExecutedOrderModel { - exec_buy_amount: 6.into(), - exec_sell_amount: 7.into(), - exec_fee_amount: None, - cost: Default::default(), - fee: Default::default(), - exec_plan: None, - }; - let foreign_liquidity_order = ExecutedLiquidityOrderModel { - order: NativeLiquidityOrder { - from: H160([99; 20]), - data: OrderData { - sell_token: t1, - buy_token: t0, - sell_amount: 101.into(), - buy_amount: 102.into(), - fee_amount: 42.into(), - valid_to: u32::MAX, - kind: OrderKind::Sell, - ..Default::default() - }, - signature: Signature::PreSign, - }, - exec_sell_amount: 101.into(), - exec_buy_amount: 102.into(), - }; - let foreign_liquidity_order_uid = foreign_liquidity_order - .order - .data - .uid(&Default::default(), &foreign_liquidity_order.order.from); - let updated_uniswap = UpdatedAmmModel { - execution: vec![ExecutedAmmModel { - sell_token: t1, - buy_token: t0, - exec_sell_amount: U256::from(9), - exec_buy_amount: U256::from(8), - exec_plan: ExecutionPlan { - coordinates: ExecutionPlanCoordinatesModel { - sequence: 0, - position: 0, - }, - internal: false, - }, - }], - cost: Default::default(), - }; - let internal_uniswap = UpdatedAmmModel { - execution: vec![ExecutedAmmModel { - sell_token: t1, - buy_token: t0, - exec_sell_amount: U256::from(1), - exec_buy_amount: U256::from(1), - exec_plan: ExecutionPlan { - coordinates: ExecutionPlanCoordinatesModel { - sequence: 1, - position: 0, - }, - internal: true, - }, - }], - cost: Default::default(), - }; - let updated_balancer_weighted = UpdatedAmmModel { - execution: vec![ExecutedAmmModel { - sell_token: t1, - buy_token: t0, - exec_sell_amount: U256::from(2), - exec_buy_amount: U256::from(1), - exec_plan: ExecutionPlan { - coordinates: ExecutionPlanCoordinatesModel { - sequence: 2, - position: 0, - }, - internal: false, - }, - }], - cost: Default::default(), - }; - let updated_balancer_stable = UpdatedAmmModel { - execution: vec![ExecutedAmmModel { - sell_token: t1, - buy_token: t0, - exec_sell_amount: U256::from(6), - exec_buy_amount: U256::from(4), - exec_plan: ExecutionPlan { - coordinates: ExecutionPlanCoordinatesModel { - sequence: 3, - position: 0, - }, - internal: false, - }, - }], - cost: Default::default(), - }; - let settled = SettledBatchAuctionModel { - orders: hashmap! { 0 => executed_order }, - foreign_liquidity_orders: vec![foreign_liquidity_order], - amms: hashmap! { - H160::from_low_u64_be(0) => updated_uniswap, - H160::from_low_u64_be(1) => internal_uniswap, - H160::from_low_u64_be(2) => updated_balancer_weighted, - H160::from_low_u64_be(3) => updated_balancer_stable, - }, - ref_token: Some(t0), - prices: hashmap! { t0 => 10.into(), t1 => 11.into() }, - ..Default::default() - }; - - let prepared = SettlementContext { orders, liquidity }; - - let settlement = convert_settlement( - settled, - &prepared, - Arc::new(MockAllowanceManaging::new()), - &OrderConverter::test(weth), - SlippageContext::default(), - &Default::default(), - true, - ) - .await - .unwrap(); - assert_eq!( - settlement.clearing_prices(), - &hashmap! { t0 => 10.into(), t1 => 11.into() } - ); - - assert_eq!( - settlement.encoder.all_trades().collect::>(), - [PricedTrade { - data: &Trade { - order: Order { - metadata: OrderMetadata { - owner: H160([99; 20]), - full_fee_amount: 42.into(), - class: OrderClass::Liquidity, - uid: foreign_liquidity_order_uid, - ..Default::default() - }, - data: OrderData { - sell_token: t1, - buy_token: t0, - sell_amount: 101.into(), - buy_amount: 102.into(), - fee_amount: 42.into(), - valid_to: u32::MAX, - kind: OrderKind::Sell, - ..Default::default() - }, - signature: Signature::PreSign, - ..Default::default() - }, - executed_amount: 101.into(), - fee: 42.into(), - }, - sell_token_price: 102.into(), - buy_token_price: 101.into(), - }] - ); - - assert_eq!( - limit_handler.calls(), - vec![LimitOrderExecution::new(7.into(), 0.into())] - ); - assert_eq!( - cp_amm_handler.calls(), - vec![AmmOrderExecution { - input_max: TokenAmount { - token: t0, - amount: 9.into() - }, - output: TokenAmount { - token: t1, - amount: 9.into() - }, - internalizable: false - }] - ); - assert_eq!( - internal_amm_handler.calls(), - vec![AmmOrderExecution { - input_max: TokenAmount { - token: t0, - amount: 2.into() - }, - output: TokenAmount { - token: t1, - amount: 1.into() - }, - internalizable: true - }] - ); - assert_eq!( - wp_amm_handler.calls(), - vec![AmmOrderExecution { - input_max: TokenAmount::new(t0, 2), - output: TokenAmount::new(t1, 2), - internalizable: false - }] - ); - assert_eq!( - sp_amm_handler.calls(), - vec![AmmOrderExecution { - input_max: TokenAmount::new(t0, 5), - output: TokenAmount::new(t1, 6), - internalizable: false - }] - ); - } - - #[test] - fn match_prepared_and_settled_amms_() { - let token_a = H160::from_slice(&hex!("a7d1c04faf998f9161fc9f800a99a809b84cfc9d")); - let token_b = H160::from_slice(&hex!("c778417e063141139fce010982780140aa0cd5ab")); - let token_c = H160::from_slice(&hex!("e4b9895e638f54c3bee2a3a78d6a297cc03e0353")); - - let cpo_0 = ConstantProductOrder { - address: H160::from_low_u64_be(0), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (597249810824827988770940, 225724246562756585230), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }; - let cpo_1 = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_b, token_c).unwrap(), - reserves: (8488677530563931705, 75408146511005299032), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }; - - let lo_1 = LimitOrder { - id: crate::liquidity::LimitOrderId::Liquidity(LiquidityOrderId::Protocol( - OrderUid::from_integer(1), - )), - sell_token: token_a, - buy_token: token_a, - sell_amount: U256::from(996570293625199060u128), - buy_amount: U256::from(289046068204476404625u128), - kind: OrderKind::Buy, - partially_fillable: false, - settlement_handling: CapturingSettlementHandler::arc(), - exchange: crate::liquidity::Exchange::ZeroEx, - ..Default::default() - }; - - let wpo = WeightedProductOrder { - address: H160::from_low_u64_be(2), - reserves: btreemap! { - token_c => WeightedTokenState { - common: TokenState { - balance: U256::from(1251682293173877359u128), - scaling_factor: Bfp::exp10(0), - }, - weight: Bfp::from(500_000_000_000_000_000), - }, - token_b => WeightedTokenState { - common: TokenState { - balance: U256::from(799086982149629058u128), - scaling_factor: Bfp::exp10(0), - }, - weight: Bfp::from(500_000_000_000_000_000), - } - }, - fee: "0.001".parse().unwrap(), - version: Default::default(), - settlement_handling: CapturingSettlementHandler::arc(), - }; - - let spo = StablePoolOrder { - address: H160::from_low_u64_be(3), - reserves: btreemap! { - token_c => TokenState { - balance: U256::from(1234u128), - scaling_factor: Bfp::exp10(0), - }, - token_b => TokenState { - balance: U256::from(5678u128), - scaling_factor: Bfp::exp10(0), - }, - }, - fee: "0.001".parse().unwrap(), - amplification_parameter: AmplificationParameter::new(1.into(), 1.into()).unwrap(), - settlement_handling: CapturingSettlementHandler::arc(), - }; - - let liquidity = vec![ - Liquidity::ConstantProduct(cpo_0.clone()), - Liquidity::ConstantProduct(cpo_1.clone()), - Liquidity::LimitOrder(lo_1), - Liquidity::BalancerWeighted(wpo.clone()), - Liquidity::BalancerStable(spo.clone()), - ]; - let solution_response = serde_json::from_str::( - r#"{ - "ref_token": "0xc778417e063141139fce010982780140aa0cd5ab", - "tokens": { - "0xa7d1c04faf998f9161fc9f800a99a809b84cfc9d": { - "decimals": 18, - "estimated_price": "377939419103409", - "normalize_priority": "0" - }, - "0xc778417e063141139fce010982780140aa0cd5ab": { - "decimals": 18, - "estimated_price": "1000000000000000000", - "normalize_priority": "1" - }, - "0xe4b9895e638f54c3bee2a3a78d6a297cc03e0353": { - "decimals": 18, - "estimated_price": "112874952666826941", - "normalize_priority": "0" - } - }, - "prices": { - "0xa7d1c04faf998f9161fc9f800a99a809b84cfc9d": "379669381779741", - "0xc778417e063141139fce010982780140aa0cd5ab": "1000000000000000000", - "0xe4b9895e638f54c3bee2a3a78d6a297cc03e0353": "355227837551346618" - }, - "orders": { - "0": { - "sell_token": "0xe4b9895e638f54c3bee2a3a78d6a297cc03e0353", - "buy_token": "0xa7d1c04faf998f9161fc9f800a99a809b84cfc9d", - "sell_amount": "996570293625199060", - "buy_amount": "289046068204476404625", - "allow_partial_fill": false, - "is_sell_order": true, - "fee": { - "token": "0xe4b9895e638f54c3bee2a3a78d6a297cc03e0353", - "amount": "3429706374800940" - }, - "cost": { - "token": "0xc778417e063141139fce010982780140aa0cd5ab", - "amount": "98173121900550" - }, - "exec_sell_amount": "996570293625199060", - "exec_buy_amount": "932415220613609833982" - } - }, - "amms": { - "0x0000000000000000000000000000000000000000": { - "kind": "ConstantProduct", - "reserves": { - "0xa7d1c04faf998f9161fc9f800a99a809b84cfc9d": "597249810824827988770940", - "0xc778417e063141139fce010982780140aa0cd5ab": "225724246562756585230" - }, - "fee": "0.003", - "cost": { - "token": "0xc778417e063141139fce010982780140aa0cd5ab", - "amount": "140188523735120" - }, - "execution": [ - { - "sell_token": "0xa7d1c04faf998f9161fc9f800a99a809b84cfc9d", - "buy_token": "0xc778417e063141139fce010982780140aa0cd5ab", - "exec_sell_amount": "932415220613609833982", - "exec_buy_amount": "354009510372389956", - "exec_plan": { - "sequence": 0, - "position": 1, - "internal": false - } - } - ] - }, - "0x0000000000000000000000000000000000000001": { - "execution": [ - { - "sell_token": "0xc778417e063141139fce010982780140aa0cd5ab", - "buy_token": "0xe4b9895e638f54c3bee2a3a78d6a297cc03e0353", - "exec_sell_amount": "1", - "exec_buy_amount": "2", - "exec_plan": { - "sequence": 0, - "position": 2, - "internal": false - } - } - ] - }, - "0x0000000000000000000000000000000000000002": { - "kind": "WeightedProduct", - "reserves": { - "0xe4b9895e638f54c3bee2a3a78d6a297cc03e0353": { - "balance": "1251682293173877359", - "weight": "0.5" - }, - "0xc778417e063141139fce010982780140aa0cd5ab": { - "balance": "799086982149629058", - "weight": "0.5" - } - }, - "fee": "0.001", - "cost": { - "token": "0xc778417e063141139fce010982780140aa0cd5ab", - "amount": "177648716400000" - }, - "execution": [ - { - "sell_token": "0xc778417e063141139fce010982780140aa0cd5ab", - "buy_token": "0xe4b9895e638f54c3bee2a3a78d6a297cc03e0353", - "exec_sell_amount": "354009510372384890", - "exec_buy_amount": "996570293625184642", - "exec_plan": { - "sequence": 0, - "position": 0, - "internal": false - } - } - ] - }, - "0x0000000000000000000000000000000000000003": { - "kind": "Stable", - "reserves": { - "0xe4b9895e638f54c3bee2a3a78d6a297cc03e0353": "1234", - "0xc778417e063141139fce010982780140aa0cd5ab": "5678" - }, - "fee": "0.001", - "cost": { - "token": "0xc778417e063141139fce010982780140aa0cd5ab", - "amount": "1771" - }, - "execution": [ - { - "sell_token": "0xc778417e063141139fce010982780140aa0cd5ab", - "buy_token": "0xe4b9895e638f54c3bee2a3a78d6a297cc03e0353", - "exec_sell_amount": "3", - "exec_buy_amount": "4", - "exec_plan": { - "sequence": 0, - "position": 3, - "internal": false - } - } - ] - } - }, - "solver": { - "name": "standard", - "args": [ - "--write_auxiliary_files", - "--solver", - "SCIP", - "--output_dir", - "/app/results" - ], - "runtime": 0.0, - "runtime_preprocessing": 17.097073793411255, - "runtime_solving": 123.31747031211853, - "runtime_ring_finding": 0.0, - "runtime_validation": 0.14400219917297363, - "nr_variables": 24, - "nr_bool_variables": 8, - "optimality_gap": null, - "solver_status": "ok", - "termination_condition": "optimal", - "exit_status": "completed" - } - }"#, - ) - .unwrap(); - - let amms = match_prepared_and_settled_amms(&liquidity, solution_response.amms).unwrap(); - let executions = merge_and_order_executions(amms, vec![], vec![]); - assert_eq!( - executions, - vec![ - Execution::Amm(Box::new(ExecutedAmm { - order: Liquidity::BalancerWeighted(wpo), - input: TokenAmount::new(token_c, 996570293625184642u128), - output: TokenAmount::new(token_b, 354009510372384890u128), - exec_plan: ExecutionPlan { - coordinates: ExecutionPlanCoordinatesModel { - sequence: 0, - position: 0, - }, - internal: false, - } - })), - Execution::Amm(Box::new(ExecutedAmm { - order: Liquidity::ConstantProduct(cpo_0), - input: TokenAmount::new(token_b, 354009510372389956u128), - output: TokenAmount::new(token_a, 932415220613609833982u128), - exec_plan: ExecutionPlan { - coordinates: ExecutionPlanCoordinatesModel { - sequence: 0, - position: 1, - }, - internal: false, - } - })), - Execution::Amm(Box::new(ExecutedAmm { - order: Liquidity::ConstantProduct(cpo_1), - input: TokenAmount::new(token_c, 2), - output: TokenAmount::new(token_b, 1), - exec_plan: ExecutionPlan { - coordinates: ExecutionPlanCoordinatesModel { - sequence: 0, - position: 2, - }, - internal: false, - } - })), - Execution::Amm(Box::new(ExecutedAmm { - order: Liquidity::BalancerStable(spo), - input: TokenAmount::new(token_c, 4), - output: TokenAmount::new(token_b, 3), - exec_plan: ExecutionPlan { - coordinates: ExecutionPlanCoordinatesModel { - sequence: 0, - position: 3, - }, - internal: false, - } - })), - ], - ); - } - - #[test] - fn merge_and_order_executions_() { - let token_a = H160::from_slice(&hex!("a7d1c04faf998f9161fc9f800a99a809b84cfc9d")); - let token_b = H160::from_slice(&hex!("c778417e063141139fce010982780140aa0cd5ab")); - - let cpo_1 = ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: TokenPair::new(token_a, token_b).unwrap(), - reserves: (8488677530563931705, 75408146511005299032), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }; - let executions_amms = vec![ExecutedAmm { - order: Liquidity::ConstantProduct(cpo_1), - input: TokenAmount::new(token_a, 2), - output: TokenAmount::new(token_b, 1), - exec_plan: ExecutionPlan { - coordinates: ExecutionPlanCoordinatesModel { - sequence: 1u32, - position: 2u32, - }, - internal: false, - }, - }]; - let interactions = vec![InteractionData { - target: H160::zero(), - value: U256::zero(), - call_data: Vec::new(), - inputs: vec![], - outputs: vec![], - exec_plan: Some(ExecutionPlan { - coordinates: ExecutionPlanCoordinatesModel { - sequence: 1u32, - position: 1u32, - }, - internal: false, - }), - cost: None, - }]; - let orders = vec![ExecutedLimitOrder { - order: Default::default(), - executed_buy_amount: U256::zero(), - executed_sell_amount: U256::zero(), - executed_fee_amount: None, - exec_plan: None, - }]; - let merged_executions = merge_and_order_executions( - executions_amms.clone(), - interactions.clone(), - orders.clone(), - ); - assert_eq!(3, merged_executions.len()); - assert!( - matches!(&merged_executions[0], Execution::LimitOrder(order) if order.as_ref() == &orders[0]) - ); - assert!( - matches!(&merged_executions[1], Execution::CustomInteraction(interaction) if interaction.as_ref() == &interactions[0]) - ); - assert!( - matches!(&merged_executions[2], Execution::Amm(amm) if amm.as_ref() == &executions_amms[0]) - ); - } - - #[tokio::test] - pub async fn compute_approvals_groups_approvals_by_spender_and_token() { - let mut allowance_manager = MockAllowanceManaging::new(); - allowance_manager - .expect_get_approvals() - .withf(|requests| { - // deal with underterministic ordering because of grouping - // implementation. - let grouped = requests - .iter() - .map(|request| ((request.token, request.spender), request.amount)) - .collect::>(); - - requests.len() == grouped.len() - && grouped - == hashmap! { - (H160([1; 20]), H160([0xf1; 20])) => U256::from(12), - (H160([1; 20]), H160([0xf2; 20])) => U256::from(3), - (H160([2; 20]), H160([0xf1; 20])) => U256::from(4), - (H160([2; 20]), H160([0xf2; 20])) => U256::from(5), - } - }) - .returning(|_| Ok(Vec::new())); - - assert_eq!( - compute_approvals( - Arc::new(allowance_manager), - vec![ - ApprovalModel { - token: H160([1; 20]), - spender: H160([0xf1; 20]), - amount: 10.into() - }, - ApprovalModel { - token: H160([1; 20]), - spender: H160([0xf2; 20]), - amount: 3.into(), - }, - ApprovalModel { - token: H160([1; 20]), - spender: H160([0xf1; 20]), - amount: 2.into(), - }, - ApprovalModel { - token: H160([2; 20]), - spender: H160([0xf1; 20]), - amount: 4.into(), - }, - ApprovalModel { - token: H160([2; 20]), - spender: H160([0xf2; 20]), - amount: 5.into(), - }, - ], - ) - .await - .unwrap(), - Vec::new(), - ); - } - - #[tokio::test] - pub async fn compute_approvals_errors_on_overflow() { - assert!(compute_approvals( - Arc::new(MockAllowanceManaging::new()), - vec![ - ApprovalModel { - token: H160([1; 20]), - spender: H160([2; 20]), - amount: U256::MAX, - }, - ApprovalModel { - token: H160([1; 20]), - spender: H160([2; 20]), - amount: 1.into(), - }, - ], - ) - .await - .is_err()); - } - - fn interaction_with_coordinate( - coordinates: Option, - ) -> Execution { - Execution::CustomInteraction(Box::new(InteractionData { - exec_plan: coordinates.map(|coordinates| ExecutionPlan { - coordinates, - ..Default::default() - }), - ..Default::default() - })) - } - - #[test] - pub fn duplicate_coordinates_false() { - let executions = vec![ - interaction_with_coordinate(None), - interaction_with_coordinate(Some(ExecutionPlanCoordinatesModel { - sequence: 0, - position: 0, - })), - interaction_with_coordinate(Some(ExecutionPlanCoordinatesModel { - sequence: 0, - position: 1, - })), - ]; - assert!(!duplicate_coordinates(&executions)); - } - - #[test] - pub fn duplicate_coordinates_true() { - let executions = vec![ - interaction_with_coordinate(None), - interaction_with_coordinate(Some(ExecutionPlanCoordinatesModel { - sequence: 0, - position: 0, - })), - interaction_with_coordinate(Some(ExecutionPlanCoordinatesModel { - sequence: 0, - position: 0, - })), - ]; - assert!(duplicate_coordinates(&executions)); - } -} diff --git a/crates/solver/src/solver/naive_solver.rs b/crates/solver/src/solver/naive_solver.rs index 7972d50a52..7d492c32e2 100644 --- a/crates/solver/src/solver/naive_solver.rs +++ b/crates/solver/src/solver/naive_solver.rs @@ -1,443 +1 @@ pub mod multi_order_solver; - -use { - crate::{ - liquidity::{ - order_converter::OrderConverter, - slippage::{SlippageCalculator, SlippageContext}, - ConstantProductOrder, - LimitOrder, - Liquidity, - }, - settlement::Settlement, - solver::{Auction, Solver}, - }, - anyhow::Result, - ethcontract::Account, - model::TokenPair, - primitive_types::H160, - std::collections::HashMap, -}; - -pub struct NaiveSolver { - account: Account, - slippage_calculator: SlippageCalculator, - enforce_correct_fees: bool, - ethflow_contract: Option, - order_converter: OrderConverter, -} - -impl NaiveSolver { - pub fn new( - account: Account, - slippage_calculator: SlippageCalculator, - enforce_correct_fees: bool, - ethflow_contract: Option, - order_converter: OrderConverter, - ) -> Self { - Self { - account, - slippage_calculator, - enforce_correct_fees, - ethflow_contract, - order_converter, - } - } -} - -#[async_trait::async_trait] -impl Solver for NaiveSolver { - async fn solve( - &self, - Auction { - orders, - liquidity, - external_prices, - balances, - .. - }: Auction, - ) -> Result> { - let mut orders = super::balance_and_convert_orders( - self.ethflow_contract, - &self.order_converter, - balances, - orders, - &external_prices, - ); - // Filter out limit orders until we add support for computing - // a reasonable `solver_fee` (#1414). - orders.retain(|o| !o.solver_determines_fee() || !self.enforce_correct_fees); - let slippage = self.slippage_calculator.context(&external_prices); - let uniswaps = extract_deepest_amm_liquidity(&liquidity); - Ok(settle(slippage, orders, uniswaps)) - } - - fn account(&self) -> &Account { - &self.account - } - - fn name(&self) -> &'static str { - "NaiveSolver" - } -} - -fn settle( - slippage: SlippageContext, - orders: Vec, - uniswaps: HashMap, -) -> Vec { - // The multi order solver matches as many orders as possible together with one - // uniswap pool. Settlements between different token pairs are thus - // independent. - organize_orders_by_token_pair(orders) - .into_iter() - .filter_map(|(pair, orders)| settle_pair(&slippage, pair, orders, &uniswaps)) - .collect() -} - -fn settle_pair( - slippage: &SlippageContext, - pair: TokenPair, - orders: Vec, - uniswaps: &HashMap, -) -> Option { - if orders.iter().all(|order| order.is_liquidity_order()) { - tracing::debug!(?pair, "no user orders"); - return None; - } - let uniswap = match uniswaps.get(&pair) { - Some(uniswap) => uniswap, - None => { - tracing::debug!(?pair, "no AMM"); - return None; - } - }; - multi_order_solver::solve(slippage, orders, uniswap) -} - -fn organize_orders_by_token_pair(orders: Vec) -> HashMap> { - let mut result = HashMap::<_, Vec>::new(); - for (order, token_pair) in orders.into_iter().filter(usable_order).filter_map(|order| { - let pair = TokenPair::new(order.buy_token, order.sell_token)?; - Some((order, pair)) - }) { - result.entry(token_pair).or_default().push(order); - } - result -} - -fn usable_order(order: &LimitOrder) -> bool { - !order.sell_amount.is_zero() && !order.buy_amount.is_zero() -} - -fn extract_deepest_amm_liquidity( - liquidity: &[Liquidity], -) -> HashMap { - let mut result = HashMap::new(); - for liquidity in liquidity { - match liquidity { - Liquidity::ConstantProduct(order) => { - let deepest_so_far = result.entry(order.tokens).or_insert_with(|| order.clone()); - if deepest_so_far.constant_product() < order.constant_product() { - result.insert(order.tokens, order.clone()); - } - } - _ => continue, - } - } - result -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - liquidity::{ - order_converter::OrderConverter, - tests::CapturingSettlementHandler, - LimitOrderId, - LiquidityOrderId, - }, - order_balance_filter::BalancedOrder, - }, - ethcontract::H160, - maplit::hashmap, - model::order::{ - Order, - OrderClass, - OrderData, - OrderKind, - OrderMetadata, - OrderUid, - BUY_ETH_ADDRESS, - }, - num::rational::Ratio, - shared::addr, - }; - - #[test] - fn test_extract_deepest_amm_liquidity() { - let token_pair = - TokenPair::new(H160::from_low_u64_be(0), H160::from_low_u64_be(1)).unwrap(); - let unrelated_token_pair = - TokenPair::new(H160::from_low_u64_be(2), H160::from_low_u64_be(3)).unwrap(); - let handler = CapturingSettlementHandler::arc(); - let liquidity = vec![ - // Deep pool - ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens: token_pair, - reserves: (10_000_000, 10_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: handler.clone(), - }, - // Shallow pool - ConstantProductOrder { - address: H160::from_low_u64_be(2), - tokens: token_pair, - reserves: (100, 100), - fee: Ratio::new(3, 1000), - settlement_handling: handler.clone(), - }, - // unrelated pool - ConstantProductOrder { - address: H160::from_low_u64_be(3), - tokens: unrelated_token_pair, - reserves: (10_000_000, 10_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: handler, - }, - ]; - let result = extract_deepest_amm_liquidity( - &liquidity - .iter() - .cloned() - .map(Liquidity::ConstantProduct) - .collect::>(), - ); - assert_eq!(result[&token_pair].reserves, liquidity[0].reserves); - assert_eq!( - result[&unrelated_token_pair].reserves, - liquidity[2].reserves - ); - } - - #[test] - fn respects_liquidity_order_limit_price() { - // We have a "perfect CoW" where the spot price of the Uniswap pool does - // not satisfy the liquidity order's limit price. Hence, there should be - // NO solutions for this auction. - // Test case recovered from the following settlement where a user order - // was settled directly with a liquidity order, and we paid out WAY more - // than the market maker order provided: - // - - let orders = vec![ - LimitOrder::from(Order { - data: OrderData { - sell_token: addr!("d533a949740bb3306d119cc777fa900ba034cd52"), - buy_token: addr!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), - sell_amount: 995952859647034749952_u128.into(), - buy_amount: 2461209365_u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - ..Default::default() - }), - LimitOrder { - id: LimitOrderId::Liquidity(LiquidityOrderId::Protocol(OrderUid::from_integer(1))), - ..LimitOrder::from(Order { - data: OrderData { - sell_token: addr!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), - buy_token: addr!("d533a949740bb3306d119cc777fa900ba034cd52"), - sell_amount: 2469904889_u128.into(), - buy_amount: 995952859647034749952_u128.into(), - kind: OrderKind::Buy, - ..Default::default() - }, - metadata: OrderMetadata { - class: OrderClass::Liquidity, - ..Default::default() - }, - ..Default::default() - }) - }, - ]; - - let tokens = TokenPair::new( - addr!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), - addr!("d533a949740bb3306d119cc777fa900ba034cd52"), - ) - .unwrap(); - let liquidity = hashmap! { - tokens => ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens, - reserves: (58360914, 17856367410307570970), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }, - }; - - assert!(settle(SlippageContext::default(), orders, liquidity).is_empty()); - } - - #[test] - fn requires_at_least_one_non_liquidity_order() { - let orders = vec![ - LimitOrder::from(Order { - data: OrderData { - sell_token: H160([1; 20]), - buy_token: H160([2; 20]), - sell_amount: 1_000_000_000_u128.into(), - buy_amount: 900_000_000_u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - metadata: OrderMetadata { - class: OrderClass::Liquidity, - ..Default::default() - }, - ..Default::default() - }), - LimitOrder::from(Order { - data: OrderData { - sell_token: H160([1; 20]), - buy_token: H160([2; 20]), - sell_amount: 1_000_000_000_u128.into(), - buy_amount: 900_000_000_u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - metadata: OrderMetadata { - class: OrderClass::Liquidity, - ..Default::default() - }, - ..Default::default() - }), - ]; - - let tokens = TokenPair::new(H160([1; 20]), H160([2; 20])).unwrap(); - let liquidity = hashmap! { - tokens => ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens, - reserves: (1_000_000_000_000_000_000, 1_000_000_000_000_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }, - }; - - assert!(settle(SlippageContext::default(), orders, liquidity).is_empty()); - } - - #[test] - fn works_with_eth_liquidity_orders() { - let native_token = H160([1; 20]); - let converter = OrderConverter::test(native_token); - - let orders = vec![ - converter - .normalize_limit_order(BalancedOrder::full(Order { - data: OrderData { - sell_token: native_token, - buy_token: H160([2; 20]), - sell_amount: 1_000_000_000_u128.into(), - buy_amount: 900_000_000_u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - ..Default::default() - })) - .unwrap(), - converter - .normalize_limit_order(BalancedOrder::full(Order { - data: OrderData { - sell_token: H160([2; 20]), - buy_token: BUY_ETH_ADDRESS, - sell_amount: 1_000_000_000_u128.into(), - buy_amount: 900_000_000_u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - metadata: OrderMetadata { - class: OrderClass::Liquidity, - ..Default::default() - }, - ..Default::default() - })) - .unwrap(), - ]; - - let tokens = TokenPair::new(native_token, H160([2; 20])).unwrap(); - let liquidity = hashmap! { - tokens => ConstantProductOrder { - address: H160::from_low_u64_be(1), - tokens, - reserves: (1_000_000_000_000_000_000, 1_000_000_000_000_000_000), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }, - }; - - assert_eq!( - settle(SlippageContext::default(), orders, liquidity).len(), - 1 - ); - } - - #[test] - fn does_not_swap_more_than_reserves() { - let usdc = addr!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); - let crv = addr!("D533a949740bb3306d119CC777fa900bA034cd52"); - - let orders = vec![ - LimitOrder::from(Order { - data: OrderData { - sell_token: crv, - buy_token: usdc, - sell_amount: 2161740107040163317224_u128.into(), - buy_amount: 2146544862_u128.into(), - fee_amount: 6177386651128093696_u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - metadata: OrderMetadata { - class: OrderClass::Liquidity, - ..Default::default() - }, - ..Default::default() - }), - LimitOrder::from(Order { - data: OrderData { - sell_token: usdc, - buy_token: crv, - sell_amount: 500000000_u128.into(), - buy_amount: 1428571428571428571428_u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - metadata: OrderMetadata { - class: OrderClass::Limit, - ..Default::default() - }, - ..Default::default() - }), - ]; - - let tokens = TokenPair::new(usdc, crv).unwrap(); - let liquidity = hashmap! { - tokens => ConstantProductOrder { - address: addr!("210a97ba874a8e279c95b350ae8ba143a143c159"), - tokens, - reserves: (32275540, 33308141034569852391), - fee: Ratio::new(3, 1000), - settlement_handling: CapturingSettlementHandler::arc(), - }, - }; - - let settlements = settle(SlippageContext::default(), orders, liquidity); - assert!(settlements.is_empty()); - } -} diff --git a/crates/solver/src/solver/oneinch_solver.rs b/crates/solver/src/solver/oneinch_solver.rs deleted file mode 100644 index 20cdda9778..0000000000 --- a/crates/solver/src/solver/oneinch_solver.rs +++ /dev/null @@ -1,534 +0,0 @@ -//! Module containing implementation of the 1Inch solver. -//! -//! This simple solver will simply use the 1Inch API to get a quote for a -//! single GPv2 order and produce a settlement directly against 1Inch. - -use { - super::single_order_solver::{ - execution_respects_order, - SettlementError, - SingleOrderSettlement, - SingleOrderSolving, - }, - crate::{ - interactions::allowances::{AllowanceManager, AllowanceManaging, ApprovalRequest}, - liquidity::{slippage::SlippageCalculator, LimitOrder}, - }, - anyhow::Result, - contracts::GPv2Settlement, - derivative::Derivative, - ethcontract::Account, - ethrpc::current_block::CurrentBlockStream, - model::order::OrderKind, - primitive_types::H160, - reqwest::{Client, Url}, - shared::{ - ethrpc::Web3, - external_prices::ExternalPrices, - interaction::Interaction, - oneinch_api::{Cache, OneInchClient, OneInchClientImpl, OneInchError, Slippage, SwapQuery}, - }, - std::{ - fmt::{self, Display, Formatter}, - sync::Arc, - }, -}; - -/// A GPv2 solver that matches GP **sell** orders to direct 1Inch swaps. -#[derive(Derivative)] -#[derivative(Debug)] -pub struct OneInchSolver { - account: Account, - settlement_contract: GPv2Settlement, - disabled_protocols: Vec, - #[derivative(Debug = "ignore")] - client: Box, - #[derivative(Debug = "ignore")] - allowance_fetcher: Box, - cache: Cache, - slippage_calculator: SlippageCalculator, - referrer_address: Option, -} - -impl OneInchSolver { - /// Creates a new 1Inch solver with a list of disabled protocols. - #[allow(clippy::too_many_arguments)] - pub fn with_disabled_protocols( - account: Account, - web3: Web3, - settlement_contract: GPv2Settlement, - chain_id: u64, - disabled_protocols: impl IntoIterator, - client: Client, - one_inch_url: Url, - slippage_calculator: SlippageCalculator, - referrer_address: Option, - block_stream: CurrentBlockStream, - ) -> Result { - let settlement_address = settlement_contract.address(); - Ok(Self { - account, - settlement_contract, - disabled_protocols: disabled_protocols.into_iter().collect(), - client: Box::new(OneInchClientImpl::new( - one_inch_url, - client, - chain_id, - block_stream, - )?), - allowance_fetcher: Box::new(AllowanceManager::new(web3, settlement_address)), - cache: Cache::default(), - slippage_calculator, - referrer_address, - }) - } -} - -impl OneInchSolver { - /// Settles a single sell order against a 1Inch swap using the specified - /// protocols and slippage. - async fn settle_order_with_protocols_and_slippage( - &self, - order: LimitOrder, - protocols: Option>, - slippage: Slippage, - ) -> Result, SettlementError> { - debug_assert_eq!( - order.kind, - OrderKind::Sell, - "only sell orders should be passed to try_settle_order" - ); - - let mut interactions: Vec> = Vec::new(); - - let spender = self.cache.spender(self.client.as_ref()).await?; - // Fetching allowance before making the SwapQuery so that the Swap info is as - // recent as possible - if let Some(approval) = self - .allowance_fetcher - .get_approval(&ApprovalRequest { - token: order.sell_token, - spender: spender.address, - amount: order.sell_amount, - }) - .await? - { - interactions.push(Arc::new(approval)); - } - - let query = SwapQuery::with_default_options( - order.sell_token, - order.buy_token, - order.sell_amount, - self.settlement_contract.address(), - protocols, - slippage, - self.referrer_address, - ); - - tracing::debug!("querying 1Inch swap api with {:?}", query); - let swap = self.client.get_swap(query, true).await?; - if !execution_respects_order(&order, swap.from_token_amount, swap.to_token_amount) { - tracing::debug!("execution does not respect order"); - return Ok(None); - } - - let (sell_token_price, buy_token_price) = (swap.to_token_amount, swap.from_token_amount); - interactions.push(Arc::new(swap)); - - Ok(Some(SingleOrderSettlement { - sell_token_price, - buy_token_price, - interactions, - executed_amount: order.full_execution_amount(), - order, - })) - } -} - -#[async_trait::async_trait] -impl SingleOrderSolving for OneInchSolver { - async fn try_settle_order( - &self, - order: LimitOrder, - external_prices: &ExternalPrices, - _gas_price: f64, - ) -> Result, SettlementError> { - if order.kind != OrderKind::Sell { - // 1Inch only supports sell orders - return Ok(None); - } - let protocols = self - .cache - .allowed_protocols(&self.disabled_protocols, self.client.as_ref()) - .await?; - let slippage = Slippage::percentage( - self.slippage_calculator - .context(external_prices) - .relative_for_order(&order)? - .as_percentage(), - )?; - self.settle_order_with_protocols_and_slippage(order, protocols, slippage) - .await - } - - fn account(&self) -> &Account { - &self.account - } - - fn name(&self) -> &'static str { - "1Inch" - } -} - -impl Display for OneInchSolver { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "OneInchSolver") - } -} - -impl From for SettlementError { - fn from(err: OneInchError) -> Self { - match err { - err if err.is_insuffucient_liquidity() => Self::Benign(err.into()), - OneInchError::Api(err) if err.status_code == 429 => Self::RateLimited, - OneInchError::Api(err) if err.status_code == 500 => { - Self::Retryable(OneInchError::Api(err).into()) - } - err => Self::Other(err.into()), - } - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - interactions::allowances::{Approval, MockAllowanceManaging}, - liquidity::LimitOrder, - solver::ExternalPrices, - test::account, - }, - contracts::{dummy_contract, GPv2Settlement, WETH9}, - ethcontract::{Web3, H160, U256}, - ethrpc::current_block::BlockInfo, - futures::FutureExt as _, - maplit::hashmap, - mockall::{predicate::*, Sequence}, - model::order::{Order, OrderData, OrderKind}, - shared::{ - conversions::U256Ext, - ethrpc::create_env_test_transport, - oneinch_api::{MockOneInchClient, Protocols, Spender, Swap}, - }, - tokio::sync::watch, - }; - - fn dummy_solver( - client: MockOneInchClient, - allowance_fetcher: MockAllowanceManaging, - ) -> OneInchSolver { - let settlement_contract = dummy_contract!(GPv2Settlement, H160::zero()); - OneInchSolver { - account: account(), - settlement_contract, - disabled_protocols: Vec::default(), - client: Box::new(client), - allowance_fetcher: Box::new(allowance_fetcher), - cache: Cache::default(), - slippage_calculator: SlippageCalculator::default(), - referrer_address: None, - } - } - - #[tokio::test] - async fn ignores_buy_orders() { - assert!( - dummy_solver(MockOneInchClient::new(), MockAllowanceManaging::new()) - .try_settle_order( - LimitOrder { - kind: OrderKind::Buy, - ..Default::default() - }, - &Default::default(), - 1. - ) - .await - .unwrap() - .is_none() - ); - } - - #[tokio::test] - async fn test_satisfies_limit_price() { - let mut client = MockOneInchClient::new(); - let mut allowance_fetcher = MockAllowanceManaging::new(); - - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(2); - let native_token = H160::from_low_u64_be(3); - - client.expect_get_spender().returning(|| { - async { - Ok(Spender { - address: H160::zero(), - }) - } - .boxed() - }); - client.expect_get_swap().returning(|_, _| { - async { - Ok(Swap { - from_token_amount: 100.into(), - to_token_amount: 99.into(), - ..Default::default() - }) - } - .boxed() - }); - - allowance_fetcher - .expect_get_approval() - .returning(|_| Ok(None)); - - let solver = dummy_solver(client, allowance_fetcher); - - let order_passing_limit = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 90.into(), - kind: model::order::OrderKind::Sell, - ..Default::default() - }; - let order_violating_limit = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 110.into(), - kind: model::order::OrderKind::Sell, - ..Default::default() - }; - - let external_prices = ExternalPrices::new( - native_token, - hashmap! { - buy_token => U256::exp10(18).to_big_rational(), - }, - ) - .unwrap(); - - let result = solver - .try_settle_order(order_passing_limit, &external_prices, 1.) - .await - .unwrap() - .unwrap(); - // Note that prices are the inverted amounts. Another way to look at - // it is if the swap requires 100 sell token to get only 99 buy - // token, then the sell token is worth less (i.e. lower price) than - // the buy token. - assert_eq!(result.sell_token_price, 99.into()); - assert_eq!(result.buy_token_price, 100.into()); - - let result = solver - .try_settle_order(order_violating_limit, &external_prices, 1.) - .await - .unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn filters_disabled_protocols() { - let mut client = MockOneInchClient::new(); - let mut allowance_fetcher = MockAllowanceManaging::new(); - - allowance_fetcher - .expect_get_approval() - .returning(|_| Ok(None)); - - client.expect_get_liquidity_sources().returning(|| { - async { - Ok(Protocols { - protocols: vec!["GoodProtocol".into(), "BadProtocol".into()], - }) - } - .boxed() - }); - client.expect_get_spender().returning(|| { - async { - Ok(Spender { - address: H160::zero(), - }) - } - .boxed() - }); - client.expect_get_swap().times(1).returning(|query, _| { - async move { - assert_eq!(query.quote.protocols, Some(vec!["GoodProtocol".into()])); - Ok(Swap { - from_token_amount: 100.into(), - to_token_amount: 100.into(), - ..Default::default() - }) - } - .boxed() - }); - - let solver = OneInchSolver { - disabled_protocols: vec!["BadProtocol".to_string(), "VeryBadProtocol".to_string()], - ..dummy_solver(client, allowance_fetcher) - }; - - // Limit price violated. Actual assert is happening in `expect_get_swap()` - assert!(solver - .try_settle_order( - LimitOrder { - kind: OrderKind::Sell, - buy_amount: U256::max_value(), - ..Default::default() - }, - &Default::default(), - 1. - ) - .await - .unwrap() - .is_none()); - } - - #[tokio::test] - async fn test_sets_allowance_if_necessary() { - let mut client = MockOneInchClient::new(); - let mut allowance_fetcher = MockAllowanceManaging::new(); - - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(2); - let spender = H160::from_low_u64_be(3); - - client - .expect_get_spender() - .returning(move || async move { Ok(Spender { address: spender }) }.boxed()); - client.expect_get_swap().returning(|_, _| { - async { - Ok(Swap { - from_token_amount: 100.into(), - to_token_amount: 100.into(), - ..Default::default() - }) - } - .boxed() - }); - - // On first invocation no prior allowance, then max allowance set. - let mut seq = Sequence::new(); - allowance_fetcher - .expect_get_approval() - .times(1) - .with(eq(ApprovalRequest { - token: sell_token, - spender, - amount: U256::from(100), - })) - .returning(move |_| { - Ok(Some(Approval { - token: sell_token, - spender, - })) - }) - .in_sequence(&mut seq); - allowance_fetcher - .expect_get_approval() - .times(1) - .with(eq(ApprovalRequest { - token: sell_token, - spender, - amount: U256::from(100), - })) - .returning(|_| Ok(None)) - .in_sequence(&mut seq); - - let solver = dummy_solver(client, allowance_fetcher); - - let order = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 90.into(), - kind: OrderKind::Sell, - ..Default::default() - }; - - let native_token = H160::from_low_u64_be(4); - let external_prices = ExternalPrices::new( - native_token, - hashmap! { - buy_token => U256::exp10(18).to_big_rational(), - }, - ) - .unwrap(); - - // On first run we have two main interactions (approve + swap) - let result = solver - .try_settle_order(order.clone(), &external_prices, 1.) - .await - .unwrap() - .unwrap(); - assert_eq!(result.interactions.len(), 2); - - // On second run we have only have one main interactions (swap) - let result = solver - .try_settle_order(order, &external_prices, 1.) - .await - .unwrap() - .unwrap(); - assert_eq!(result.interactions.len(), 1) - } - - #[tokio::test] - #[ignore] - async fn solve_order_on_oneinch() { - let web3 = Web3::new(create_env_test_transport()); - let chain_id = web3.eth().chain_id().await.unwrap().as_u64(); - let settlement = GPv2Settlement::deployed(&web3).await.unwrap(); - - let weth = WETH9::deployed(&web3).await.unwrap(); - let gno = testlib::tokens::GNO; - let (_, block_stream) = watch::channel(BlockInfo::default()); - - let solver = OneInchSolver::with_disabled_protocols( - account(), - web3, - settlement, - chain_id, - vec!["PMM1".to_string()], - Client::new(), - OneInchClientImpl::DEFAULT_URL.try_into().unwrap(), - SlippageCalculator::default(), - None, - block_stream, - ) - .unwrap(); - let settlement = solver - .settle_order_with_protocols_and_slippage( - Order { - data: OrderData { - sell_token: weth.address(), - buy_token: gno, - sell_amount: 1_000_000_000_000_000_000u128.into(), - buy_amount: 1u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - ..Default::default() - } - .into(), - None, - Slippage::ONE_PERCENT, - ) - .await - .unwrap() - .unwrap(); - - println!("{settlement:#?}"); - } -} diff --git a/crates/solver/src/solver/optimizing_solver.rs b/crates/solver/src/solver/optimizing_solver.rs deleted file mode 100644 index a68d2abd6d..0000000000 --- a/crates/solver/src/solver/optimizing_solver.rs +++ /dev/null @@ -1,123 +0,0 @@ -use { - super::risk_computation::RiskCalculator, - crate::{ - settlement::Settlement, - settlement_post_processing::PostProcessing, - solver::{Auction, Solver}, - }, - anyhow::Result, - ethcontract::Account, - gas_estimation::GasPrice1559, - model::auction::AuctionId, - shared::http_solver::model::AuctionResult, - std::sync::Arc, -}; - -/// A wrapper for solvers that applies a set of optimizations to all the -/// generated settlements. -pub struct OptimizingSolver { - pub inner: Arc, - pub post_processing_pipeline: Arc, - pub risk_calculator: Option, -} - -#[async_trait::async_trait] -impl Solver for OptimizingSolver { - async fn solve(&self, auction: Auction) -> Result> { - let gas_price = GasPrice1559 { - base_fee_per_gas: auction.gas_price, - max_fee_per_gas: auction.gas_price, - max_priority_fee_per_gas: 0., - }; - let results = self.inner.solve(auction).await?; - let optimizations = results.into_iter().map(|settlement| { - self.post_processing_pipeline.optimize_settlement( - settlement, - self.account().clone(), - gas_price, - self.risk_calculator.as_ref(), - ) - }); - let optimized = futures::future::join_all(optimizations).await; - Ok(optimized) - } - - fn notify_auction_result(&self, auction_id: AuctionId, result: AuctionResult) { - self.inner.notify_auction_result(auction_id, result) - } - - fn account(&self) -> &Account { - self.inner.account() - } - - fn name(&self) -> &str { - self.inner.name() - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - interactions::UnwrapWethInteraction, - settlement_post_processing::MockPostProcessing, - solver::MockSolver, - }, - contracts::{dummy_contract, WETH9}, - ethcontract::PrivateKey, - futures::FutureExt, - primitive_types::H160, - }; - - #[tokio::test] - async fn optimizes_solutions() { - let account = Account::Offline(PrivateKey::from_raw([0x1; 32]).unwrap(), None); - - let mut inner = MockSolver::new(); - inner - .expect_solve() - .returning(|_| Ok(vec![Default::default()])); - inner.expect_account().return_const(account); - - let mut post_processing = MockPostProcessing::new(); - post_processing - .expect_optimize_settlement() - .withf(|settlement, _, gas_price, _| { - gas_price.effective_gas_price() == 9_999. - && settlement - .encoder - .amount_to_unwrap(H160([0x42; 20])) - .is_zero() - }) - .returning(|_, _, _, _| { - async { - let mut settlement = Settlement::default(); - settlement.encoder.add_unwrap(UnwrapWethInteraction { - amount: 42.into(), - weth: dummy_contract!(WETH9, [0x42; 20]), - }); - settlement - } - .boxed() - }) - .times(1); - - let optimizing_solver = OptimizingSolver { - inner: Arc::new(inner), - post_processing_pipeline: Arc::new(post_processing), - risk_calculator: None, - }; - - let auction = Auction { - gas_price: 9_999., - ..Default::default() - }; - let solutions = optimizing_solver.solve(auction).await.unwrap(); - assert_eq!(solutions.len(), 1); - assert_eq!( - solutions[0].encoder.amount_to_unwrap(H160([0x42; 20])), - 42.into() - ); - } -} diff --git a/crates/solver/src/solver/paraswap_solver.rs b/crates/solver/src/solver/paraswap_solver.rs deleted file mode 100644 index d70ba2ac66..0000000000 --- a/crates/solver/src/solver/paraswap_solver.rs +++ /dev/null @@ -1,602 +0,0 @@ -use { - super::single_order_solver::{ - execution_respects_order, - SettlementError, - SingleOrderSettlement, - SingleOrderSolving, - }, - crate::{ - interactions::allowances::{AllowanceManager, AllowanceManaging, ApprovalRequest}, - liquidity::{slippage::SlippageCalculator, LimitOrder}, - }, - anyhow::{anyhow, Result}, - contracts::GPv2Settlement, - derivative::Derivative, - ethcontract::{Account, H160}, - ethrpc::current_block::CurrentBlockStream, - model::order::OrderKind, - reqwest::Client, - shared::{ - ethrpc::Web3, - external_prices::ExternalPrices, - paraswap_api::{ - DefaultParaswapApi, - ParaswapApi, - ParaswapResponseError, - PriceQuery, - PriceResponse, - Side, - TradeAmount, - TransactionBuilderQuery, - }, - token_info::{TokenInfo, TokenInfoFetching}, - }, - std::{collections::HashMap, sync::Arc}, -}; - -const REFERRER: &str = "GPv2"; - -/// A GPv2 solver that matches GP orders to direct ParaSwap swaps. -#[derive(Derivative)] -#[derivative(Debug)] -pub struct ParaswapSolver { - account: Account, - settlement_contract: GPv2Settlement, - #[derivative(Debug = "ignore")] - token_info: Arc, - #[derivative(Debug = "ignore")] - allowance_fetcher: Box, - #[derivative(Debug = "ignore")] - client: Box, - disabled_paraswap_dexs: Vec, - slippage_calculator: SlippageCalculator, -} - -impl ParaswapSolver { - #[allow(clippy::too_many_arguments)] - pub fn new( - account: Account, - web3: Web3, - settlement_contract: GPv2Settlement, - token_info: Arc, - disabled_paraswap_dexs: Vec, - client: Client, - partner: Option, - base_url: String, - slippage_calculator: SlippageCalculator, - block_stream: CurrentBlockStream, - ) -> Self { - let allowance_fetcher = AllowanceManager::new(web3.clone(), settlement_contract.address()); - - Self { - account, - settlement_contract, - token_info, - allowance_fetcher: Box::new(allowance_fetcher), - client: Box::new(DefaultParaswapApi { - client, - base_url, - partner: partner.unwrap_or_else(|| REFERRER.into()), - block_stream, - }), - disabled_paraswap_dexs, - slippage_calculator, - } - } -} - -impl From for SettlementError { - fn from(err: ParaswapResponseError) -> Self { - match err { - err @ ParaswapResponseError::Request(_) | err @ ParaswapResponseError::Retryable(_) => { - Self::Retryable(anyhow!(err)) - } - ParaswapResponseError::RateLimited => Self::RateLimited, - ParaswapResponseError::InsufficientLiquidity(_) => Self::Benign(anyhow!(err)), - err => Self::Other(anyhow!(err)), - } - } -} - -#[async_trait::async_trait] -impl SingleOrderSolving for ParaswapSolver { - async fn try_settle_order( - &self, - order: LimitOrder, - external_prices: &ExternalPrices, - _gas_price: f64, - ) -> Result, SettlementError> { - let token_info = self - .token_info - .get_token_infos(&[order.sell_token, order.buy_token]) - .await; - let price_response = self.get_price_for_order(&order, &token_info).await?; - if !execution_respects_order( - &order, - price_response.src_amount, - price_response.dest_amount, - ) { - tracing::debug!("execution does not respect order"); - return Ok(None); - } - let transaction_query = - self.transaction_query_from(external_prices, &order, &price_response, &token_info)?; - let transaction = self.client.transaction(transaction_query, true).await?; - let mut settlement = SingleOrderSettlement { - sell_token_price: price_response.dest_amount, - buy_token_price: price_response.src_amount, - interactions: Vec::new(), - executed_amount: order.full_execution_amount(), - order: order.clone(), - }; - if let Some(approval) = self - .allowance_fetcher - .get_approval(&ApprovalRequest { - token: order.sell_token, - spender: price_response.token_transfer_proxy, - amount: price_response.src_amount, - }) - .await? - { - settlement.interactions.push(Arc::new(approval)); - } - settlement.interactions.push(Arc::new(transaction)); - Ok(Some(settlement)) - } - - fn account(&self) -> &Account { - &self.account - } - - fn name(&self) -> &'static str { - "ParaSwap" - } -} - -impl ParaswapSolver { - async fn get_price_for_order( - &self, - order: &LimitOrder, - token_info: &HashMap, - ) -> Result { - let (amount, side) = match order.kind { - model::order::OrderKind::Buy => (order.buy_amount, Side::Buy), - model::order::OrderKind::Sell => (order.sell_amount, Side::Sell), - }; - - let price_query = PriceQuery { - src_token: order.sell_token, - dest_token: order.buy_token, - src_decimals: decimals(token_info, &order.sell_token)?, - dest_decimals: decimals(token_info, &order.buy_token)?, - amount, - side, - exclude_dexs: Some(self.disabled_paraswap_dexs.clone()), - }; - let price_response = self.client.price(price_query, true).await?; - Ok(price_response) - } - - fn transaction_query_from( - &self, - external_prices: &ExternalPrices, - order: &LimitOrder, - price_response: &PriceResponse, - token_info: &HashMap, - ) -> Result { - let slippage = self.slippage_calculator.context(external_prices); - let trade_amount = match order.kind { - OrderKind::Sell => TradeAmount::Exact { - src_amount: price_response.src_amount, - dest_amount: slippage - .apply_to_amount_out(order.buy_token, price_response.dest_amount)?, - }, - OrderKind::Buy => TradeAmount::Exact { - src_amount: slippage - .apply_to_amount_in(order.sell_token, price_response.src_amount)?, - dest_amount: price_response.dest_amount, - }, - }; - let query = TransactionBuilderQuery { - src_token: order.sell_token, - dest_token: order.buy_token, - trade_amount, - src_decimals: decimals(token_info, &order.sell_token)?, - dest_decimals: decimals(token_info, &order.buy_token)?, - price_route: price_response.clone().price_route_raw, - user_address: self.account.address(), - }; - Ok(query) - } -} - -fn decimals(token_info: &HashMap, token: &H160) -> Result { - token_info - .get(token) - .and_then(|info| info.decimals) - .ok_or_else(|| anyhow!("decimals for token {:?} not found", token)) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - interactions::allowances::{Approval, MockAllowanceManaging}, - test::account, - }, - contracts::{dummy_contract, WETH9}, - ethcontract::U256, - ethrpc::current_block::BlockInfo, - futures::FutureExt as _, - maplit::hashmap, - mockall::{predicate::*, Sequence}, - model::order::{Order, OrderData, OrderKind}, - reqwest::Client, - shared::{ - ethrpc::create_env_test_transport, - paraswap_api::MockParaswapApi, - token_info::{MockTokenInfoFetching, TokenInfo, TokenInfoFetcher}, - }, - std::collections::HashMap, - tokio::sync::watch, - }; - - #[tokio::test] - async fn test_skips_order_if_unable_to_fetch_decimals() { - let client = Box::new(MockParaswapApi::new()); - let allowance_fetcher = Box::new(MockAllowanceManaging::new()); - let mut token_info = MockTokenInfoFetching::new(); - - token_info - .expect_get_token_infos() - .return_const(HashMap::new()); - - let solver = ParaswapSolver { - account: account(), - client, - token_info: Arc::new(token_info), - allowance_fetcher, - settlement_contract: dummy_contract!(GPv2Settlement, H160::zero()), - disabled_paraswap_dexs: vec![], - slippage_calculator: Default::default(), - }; - - let order = LimitOrder::default(); - let result = solver - .try_settle_order(order, &Default::default(), 1.) - .await; - - // This implicitly checks that we don't call the API is its mock doesn't have - // any expectations and would panic - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_respects_limit_price() { - let mut client = Box::new(MockParaswapApi::new()); - let mut allowance_fetcher = Box::new(MockAllowanceManaging::new()); - let mut token_info = MockTokenInfoFetching::new(); - - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(2); - - client.expect_price().returning(|_, _| { - async { - Ok(PriceResponse { - price_route_raw: Default::default(), - src_amount: 100.into(), - dest_amount: 99.into(), - token_transfer_proxy: H160([0x42; 20]), - gas_cost: 0, - }) - } - .boxed() - }); - client - .expect_transaction() - .returning(|_, _| async { Ok(Default::default()) }.boxed()); - - allowance_fetcher - .expect_get_approval() - .returning(|_| Ok(None)); - - token_info.expect_get_token_infos().returning(move |_| { - hashmap! { - sell_token => TokenInfo { decimals: Some(18), symbol: None }, - buy_token => TokenInfo { decimals: Some(18), symbol: None }, - } - }); - - let solver = ParaswapSolver { - account: account(), - client, - token_info: Arc::new(token_info), - allowance_fetcher, - settlement_contract: dummy_contract!(GPv2Settlement, H160::zero()), - disabled_paraswap_dexs: vec![], - slippage_calculator: Default::default(), - }; - - let order_passing_limit = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 90.into(), - kind: model::order::OrderKind::Sell, - ..Default::default() - }; - let order_violating_limit = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 110.into(), - kind: model::order::OrderKind::Sell, - ..Default::default() - }; - - let result = solver - .try_settle_order(order_passing_limit, &Default::default(), 1.) - .await - .unwrap() - .unwrap(); - assert_eq!(result.sell_token_price, 99.into()); - assert_eq!(result.buy_token_price, 100.into()); - - let result = solver - .try_settle_order(order_violating_limit, &Default::default(), 1.) - .await - .unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn test_sets_allowance_if_necessary() { - let mut client = Box::new(MockParaswapApi::new()); - let mut allowance_fetcher = Box::new(MockAllowanceManaging::new()); - let mut token_info = MockTokenInfoFetching::new(); - - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(2); - let token_transfer_proxy = H160([0x42; 20]); - - client.expect_price().returning(move |_, _| { - async move { - Ok(PriceResponse { - price_route_raw: Default::default(), - src_amount: 100.into(), - dest_amount: 99.into(), - token_transfer_proxy, - gas_cost: 0, - }) - } - .boxed() - }); - client - .expect_transaction() - .returning(|_, _| async { Ok(Default::default()) }.boxed()); - - // On first invocation no prior allowance, then max allowance set. - let mut seq = Sequence::new(); - allowance_fetcher - .expect_get_approval() - .times(1) - .with(eq(ApprovalRequest { - token: sell_token, - spender: token_transfer_proxy, - amount: U256::from(100), - })) - .returning(move |_| { - Ok(Some(Approval { - token: sell_token, - spender: token_transfer_proxy, - })) - }) - .in_sequence(&mut seq); - allowance_fetcher - .expect_get_approval() - .times(1) - .with(eq(ApprovalRequest { - token: sell_token, - spender: token_transfer_proxy, - amount: U256::from(100), - })) - .returning(|_| Ok(None)) - .in_sequence(&mut seq); - - token_info.expect_get_token_infos().returning(move |_| { - hashmap! { - sell_token => TokenInfo { decimals: Some(18), symbol: None }, - buy_token => TokenInfo { decimals: Some(18), symbol: None }, - } - }); - - let solver = ParaswapSolver { - account: account(), - client, - token_info: Arc::new(token_info), - allowance_fetcher, - settlement_contract: dummy_contract!(GPv2Settlement, H160::zero()), - disabled_paraswap_dexs: vec![], - slippage_calculator: Default::default(), - }; - - let order = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 90.into(), - ..Default::default() - }; - - // On first run we have two main interactions (approve + swap) - let result = solver - .try_settle_order(order.clone(), &Default::default(), 1.) - .await - .unwrap() - .unwrap(); - assert_eq!(result.interactions.len(), 2); - - // On second run we have only have one main interactions (swap) - let result = solver - .try_settle_order(order, &Default::default(), 1.) - .await - .unwrap() - .unwrap(); - assert_eq!(result.interactions.len(), 1) - } - - #[tokio::test] - async fn test_sets_slippage() { - let mut client = Box::new(MockParaswapApi::new()); - let mut allowance_fetcher = Box::new(MockAllowanceManaging::new()); - let mut token_info = MockTokenInfoFetching::new(); - - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(2); - - client.expect_price().returning(|_, _| { - async { - Ok(PriceResponse { - price_route_raw: Default::default(), - src_amount: 100.into(), - dest_amount: 99.into(), - token_transfer_proxy: H160([0x42; 20]), - gas_cost: 0, - }) - } - .boxed() - }); - - // Check slippage is applied to PriceResponse - let mut seq = Sequence::new(); - client - .expect_transaction() - .times(1) - .returning(|transaction, _| { - assert_eq!( - transaction.trade_amount, - TradeAmount::Exact { - src_amount: 100.into(), - dest_amount: 89.into(), // 99 - 10% slippage - } - ); - async { Ok(Default::default()) }.boxed() - }) - .in_sequence(&mut seq); - client - .expect_transaction() - .times(1) - .returning(|transaction, _| { - assert_eq!( - transaction.trade_amount, - TradeAmount::Exact { - src_amount: 110.into(), // 100 + 10% slippage - dest_amount: 99.into(), - } - ); - async { Ok(Default::default()) }.boxed() - }) - .in_sequence(&mut seq); - - allowance_fetcher - .expect_get_approval() - .returning(|_| Ok(None)); - - token_info.expect_get_token_infos().returning(move |_| { - hashmap! { - sell_token => TokenInfo { decimals: Some(18), symbol: None }, - buy_token => TokenInfo { decimals: Some(18), symbol: None }, - } - }); - - let solver = ParaswapSolver { - account: account(), - client, - token_info: Arc::new(token_info), - allowance_fetcher, - settlement_contract: dummy_contract!(GPv2Settlement, H160::zero()), - disabled_paraswap_dexs: vec![], - slippage_calculator: SlippageCalculator::from_bps(1000, None), - }; - - let sell_order = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 90.into(), - kind: model::order::OrderKind::Sell, - ..Default::default() - }; - - let result = solver - .try_settle_order(sell_order, &Default::default(), 1.) - .await - .unwrap(); - // Actual assertion is inside the client's `expect_transaction` mock - assert!(result.is_some()); - - let buy_order = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 90.into(), - kind: model::order::OrderKind::Buy, - ..Default::default() - }; - let result = solver - .try_settle_order(buy_order, &Default::default(), 1.) - .await - .unwrap(); - // Actual assertion is inside the client's `expect_transaction` mock - assert!(result.is_some()); - } - - #[tokio::test] - #[ignore] - async fn solve_order_on_paraswap() { - let web3 = Web3::new(create_env_test_transport()); - let settlement = GPv2Settlement::deployed(&web3).await.unwrap(); - let token_info_fetcher = Arc::new(TokenInfoFetcher { web3: web3.clone() }); - - let weth = WETH9::deployed(&web3).await.unwrap(); - let gno = testlib::tokens::GNO; - let (_, block_stream) = watch::channel(BlockInfo::default()); - - let solver = ParaswapSolver::new( - account(), - web3, - settlement, - token_info_fetcher, - vec![], - Client::new(), - None, - "https://apiv5.paraswap.io".into(), - SlippageCalculator::default(), - block_stream, - ); - - let settlement = solver - .try_settle_order( - Order { - data: OrderData { - sell_token: weth.address(), - buy_token: gno, - sell_amount: 1_000_000_000_000_000_000u128.into(), - buy_amount: 1u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - ..Default::default() - } - .into(), - &Default::default(), - 1., - ) - .await - .unwrap() - .unwrap(); - - println!("{settlement:#?}"); - } -} diff --git a/crates/solver/src/solver/risk_computation.rs b/crates/solver/src/solver/risk_computation.rs deleted file mode 100644 index 84697b444c..0000000000 --- a/crates/solver/src/solver/risk_computation.rs +++ /dev/null @@ -1,176 +0,0 @@ -// Suppose a solver generates a solution, and we compute the three variables: -// surplus, fees and gas_cost. Currently, the objective is then simply equal to -// surplus + fees - gas_cost. However, for CIP-20, we now have one additional -// step for each solver. So for each solver we have trained a revert risk model, -// which essentially is a function that computes the probability of the -// settlement not reverting, and is a function of 3 parameters. -// p (num_orders_in_sol, gas_used, gas_price). - -use { - super::SolverType, - anyhow::{Context, Result}, - clap::{Parser, ValueEnum}, - std::{ - collections::HashMap, - fmt::{Display, Formatter}, - ops::Neg, - str::FromStr, - }, -}; - -#[derive(Debug, Default, Clone)] -pub struct RiskCalculator { - pub gas_amount_factor: f64, - pub gas_price_factor: f64, - pub nmb_orders_factor: f64, - pub intercept: f64, -} - -impl RiskCalculator { - pub fn calculate(&self, gas_amount: f64, gas_price: f64, nmb_orders: usize) -> Result { - let exponent = self.intercept.neg() - - self.gas_amount_factor * gas_amount / 1_000_000. - - self.gas_price_factor * gas_price / 10_000_000_000. - - self.nmb_orders_factor * nmb_orders as f64; - let success_probability = 1. / (1. + exponent.exp()); - tracing::trace!( - ?gas_amount, - ?gas_price, - ?nmb_orders, - ?exponent, - ?success_probability, - "risk calculation", - ); - Ok(success_probability) - } -} - -// The code for collecting the data and training the model can be found here: -// https://github.com/cowprotocol/risk_adjusted_rewards -// The data for each solver can be found here. -// https://drive.google.com/drive/u/1/folders/19yoL808qkp_os3BpLIYQahI3mQrNyx5T -#[rustfmt::skip] -const DEFAULT_RISK_PARAMETERS: &str = "\ - Naive,0.5604082285267333,0.00285114179288399,0.06499875450001853,3.3987949311136787;\ - Baseline,-0.24391894879979226,-0.05809501139187965,-0.000013222507455295696,4.27946195371547;\ - OneInch,-0.32674185936325467,-0.05930446215554123,-0.33031769043234466,3.144609301500272;\ - Paraswap,-0.7815504846264341,-0.06965336115721313,0.0701725936991023,3.2617622830143453;\ - ZeroEx,-1.399997494341399,-0.04522233479453635,0.11066085229796373,2.7150950015915676;\ - BalancerSor,-0.7070951919365344,-0.1841886790519467,0.34189609422313544,3.6849833670945027"; - -#[derive(Debug, Parser)] -#[group(skip)] -pub struct Arguments { - /// Parameters for the risk computation for each solver. - /// The format is a list of semicolon separated solver parameters. - /// Each solver parameter is a comma separated list of parameters: - /// [solver name],[gas amount factor],[gas price factor],[number of orders - /// factor],[intercept parameter] - #[clap(long, env, default_value = DEFAULT_RISK_PARAMETERS)] - risk_parameters: RiskParameters, -} - -impl Arguments { - pub fn get_calculator(&self, solver: SolverType) -> Option { - self.risk_parameters.0.get(&solver).cloned() - } -} - -impl Display for Arguments { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let Self { risk_parameters } = self; - - write!(f, "risk_parameters: {:?}", risk_parameters) - } -} - -#[derive(Debug, Clone)] -pub struct RiskParameters(HashMap); - -impl FromStr for RiskParameters { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - // parse user provided parameters (or default if none are provided) - let user_risk_parameters = parse_calculators(s)?; - // parse default parameters and override them with the ones provided by the user - let risk_parameters = parse_calculators(DEFAULT_RISK_PARAMETERS)? - .into_iter() - .map(|(solver, default_risk_parameter)| { - ( - solver, - user_risk_parameters - .get(&solver) - .cloned() - .unwrap_or(default_risk_parameter), - ) - }) - .collect(); - Ok(Self(risk_parameters)) - } -} - -fn parse_calculators(s: &str) -> Result> { - s.split(';') - .map(|part| { - let (solver, parameters) = part - .split_once(',') - .context("malformed solver risk parameters")?; - let mut parameters = parameters.split(','); - let gas_amount_factor = parameters - .next() - .context("missing a parameter for risk")? - .parse()?; - let gas_price_factor = parameters - .next() - .context("missing b parameter for risk")? - .parse()?; - let nmb_orders_factor = parameters - .next() - .context("missing c parameter for risk")? - .parse()?; - let intercept = parameters - .next() - .context("missing x parameter for risk")? - .parse()?; - Ok(( - SolverType::from_str(solver, true).map_err(|message| anyhow::anyhow!(message))?, - RiskCalculator { - gas_amount_factor, - gas_price_factor, - nmb_orders_factor, - intercept, - }, - )) - }) - .collect::>>() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn risk_parameters_test() { - let risk_parameters = RiskParameters::from_str(DEFAULT_RISK_PARAMETERS).unwrap(); - assert_eq!(risk_parameters.0.len(), 6); - } - - #[test] - fn compute_success_probability_test() { - // tx hash 0x201c948ad94d7f93ad2d3c13fa4b6bbd4270533fbfedcb8be60e68c8e709d2b6 - // objective_score = 251547381429604400 - // success_probability ends up being 0.9202405649482063 - let risk_parameters = RiskParameters::from_str(DEFAULT_RISK_PARAMETERS).unwrap(); - let gas_amount = 765096.; - let gas_price = 41398382700.; - let nmb_orders = 1; - let success_probability = risk_parameters - .0 - .get(&SolverType::Paraswap) - .unwrap() - .calculate(gas_amount, gas_price, nmb_orders) - .unwrap(); - assert_eq!(success_probability, 0.9202405649482063); - } -} diff --git a/crates/solver/src/solver/single_order_solver.rs b/crates/solver/src/solver/single_order_solver.rs deleted file mode 100644 index 5bb203178d..0000000000 --- a/crates/solver/src/solver/single_order_solver.rs +++ /dev/null @@ -1,1256 +0,0 @@ -use { - super::SolverInfo, - crate::{ - liquidity::{ - order_converter::OrderConverter, - LimitOrder, - LimitOrderExecution, - LimitOrderId, - }, - metrics::SolverMetrics, - settlement::Settlement, - settlement_rater::SettlementRating, - solver::{Auction, Solver}, - }, - anyhow::{anyhow, Context as _, Result}, - clap::Parser, - ethcontract::Account, - futures::FutureExt, - gas_estimation::GasPrice1559, - model::order::OrderKind, - num::{CheckedDiv, ToPrimitive}, - number::conversions::u256_to_big_rational, - primitive_types::{H160, U256}, - rand::prelude::SliceRandom, - rate_limit::{Error, RateLimiter, Strategy}, - shared::{ - arguments::display_option, - conversions::U256Ext, - external_prices::ExternalPrices, - interaction::Interaction, - }, - std::{ - collections::VecDeque, - fmt::{self, Display, Formatter}, - sync::Arc, - time::Duration, - }, - tokio::sync::mpsc, - tracing::Instrument, -}; - -mod fills; -mod merge; - -/// CLI arguments to configure order prioritization of single order solvers -/// based on an orders price. -#[derive(Debug, Parser, Clone)] -#[group(skip)] -pub struct Arguments { - /// Exponent to turn an order's price ratio into a weight for a weighted - /// prioritization. - #[clap(long, env, default_value = "10.0")] - pub price_priority_exponent: f64, - - /// The lowest possible weight an order can have for the weighted order - /// prioritization. - #[clap(long, env, default_value = "0.01")] - pub price_priority_min_weight: f64, - - /// The highest possible weight an order can have for the weighted order - /// prioritization. - #[clap(long, env, default_value = "10.0")] - pub price_priority_max_weight: f64, - - /// Configures the back off strategy for single order solvers. Requests - /// issued while back off is active get dropped entirely. Expects - /// "= 1.0>,,". - #[clap(long, env)] - pub single_order_solver_rate_limiter: Option, -} - -impl Arguments { - fn order_prioritization(&self) -> OrderPrioritization { - OrderPrioritization { - exponent: self.price_priority_exponent, - min_weight: self.price_priority_min_weight, - max_weight: self.price_priority_max_weight, - } - } - - fn rate_limiter(&self, name: &str) -> Arc { - Arc::new(RateLimiter::from_strategy( - self.single_order_solver_rate_limiter - .clone() - .unwrap_or_default(), - format!("{name}_solver"), - )) - } -} - -impl Display for Arguments { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let Self { - price_priority_exponent, - price_priority_min_weight, - price_priority_max_weight, - single_order_solver_rate_limiter, - } = self; - - writeln!(f, "price_priority_exponent: {}", price_priority_exponent)?; - writeln!( - f, - "price_priority_min_weight: {}", - price_priority_min_weight - )?; - writeln!( - f, - "price_priority_max_weight: {}", - price_priority_max_weight - )?; - display_option( - f, - "single_order_solver_rate_limiter", - single_order_solver_rate_limiter, - )?; - Ok(()) - } -} - -#[cfg_attr(test, mockall::automock)] -#[async_trait::async_trait] -/// Implementations of this trait know how to settle a single limit order (not -/// taking advantage of batching multiple orders together) -pub trait SingleOrderSolving: Send + Sync + 'static { - async fn try_settle_order( - &self, - order: LimitOrder, - external_prices: &ExternalPrices, - gas_price: f64, - ) -> Result, SettlementError>; - - /// Solver's account that should be used to submit settlements. - fn account(&self) -> &Account; - - /// Displayable name of the solver. Defaults to the type name. - fn name(&self) -> &'static str { - std::any::type_name::() - } -} - -struct OrderPrioritization { - exponent: f64, - min_weight: f64, - max_weight: f64, -} - -impl OrderPrioritization { - fn apply_weight_constraints(&self, original_weight: f64) -> f64 { - original_weight - .powf(self.exponent) - .clamp(self.min_weight, self.max_weight) - } -} - -impl Default for OrderPrioritization { - fn default() -> Self { - // Arguments which seem to produce reasonable results for orders between 90% and - // 130% of the market price. - Self { - exponent: 10., - min_weight: 0.01, - max_weight: 10., - } - } -} - -pub struct SingleOrderSolver { - inner: Box, - metrics: Arc, - max_merged_settlements: usize, - max_settlements_per_solver: usize, - order_prioritization_config: OrderPrioritization, - rate_limiter: Arc, - fills: fills::Fills, - settlement_rater: Arc, - ethflow_contract: Option, - order_converter: OrderConverter, -} - -impl SingleOrderSolver { - #[allow(clippy::too_many_arguments)] - pub fn new( - inner: Box, - metrics: Arc, - max_settlements_per_solver: usize, - max_merged_settlements: usize, - arguments: Arguments, - smallest_fill: U256, - settlement_rater: Arc, - ethflow_contract: Option, - order_converter: OrderConverter, - ) -> Self { - let rate_limiter = arguments.rate_limiter(inner.name()); - Self { - inner, - metrics, - max_merged_settlements, - max_settlements_per_solver, - order_prioritization_config: arguments.order_prioritization(), - rate_limiter, - fills: fills::Fills::new(smallest_fill), - settlement_rater, - ethflow_contract, - order_converter, - } - } - - async fn solve_single_order( - &self, - order: LimitOrder, - external_prices: &ExternalPrices, - gas_price: f64, - ) -> SolveResult { - let name = self.inner.name(); - let fill = match self.fills.order(&order, external_prices) { - Ok(fill) => fill, - Err(err) => { - tracing::warn!(?order.id, ?err, "failed to compute fill; skipping order"); - return SolveResult::Failed; - } - }; - - let single = match self - .rate_limiter - .execute( - self.inner - .try_settle_order(fill.clone(), external_prices, gas_price), - |result| matches!(result, Err(SettlementError::RateLimited)), - ) - .await - .unwrap_or_else(|Error::RateLimited| Err(SettlementError::RateLimited)) - { - Ok(value) => { - self.metrics.single_order_solver_succeeded(name); - value - } - Err(SettlementError::RateLimited) => { - self.metrics.single_order_solver_failed(name); - tracing::warn!("rate limited"); - return SolveResult::RateLimited; - } - Err(SettlementError::Benign(err)) => { - // The order was processed but cannot get matched, it shouldn't - // be treated as a failure. - self.metrics.single_order_solver_succeeded(name); - tracing::info!(?err, "benign error"); - return SolveResult::Failed; - } - Err(SettlementError::Retryable(err)) => { - self.metrics.single_order_solver_failed(name); - tracing::warn!(?err, "retryable error"); - return SolveResult::Retryable(order); - } - Err(SettlementError::Other(err)) => { - self.metrics.single_order_solver_failed(name); - tracing::warn!(?err, "failed"); - return SolveResult::Failed; - } - }; - - match single { - Some(settlement) => { - if let Some(order_uid) = order.id.order_uid() { - // Maybe some liquidity appeared that enables a bigger fill. - self.fills.increase_next_try(order_uid); - } - SolveResult::Solved(settlement) - } - None => { - if let Some(order_uid) = order.id.order_uid() { - self.fills.reduce_next_try(order_uid); - } - SolveResult::Failed - } - } - } - - /// Ensures the intermediate settlement simulates and uses the gas estimate - /// from the simulations to determine appropriate fees for the final - /// settlement. - async fn finalize_settlement( - &self, - intermediate: SingleOrderSettlement, - external_prices: &ExternalPrices, - gas_price: f64, - id: usize, - ) -> Result> { - let settlement = intermediate.into_settlement(external_prices, &0.into()); - let settlement = match settlement { - Ok(Some(settlement)) => settlement, - Err(err) => return Err(err), - Ok(None) => anyhow::bail!("settlement did not respect limit price"), - }; - - let simulation = self - .settlement_rater - .rate_settlement( - &SolverInfo { - name: self.name().to_owned(), - account: self.account().clone(), - }, - settlement, - external_prices, - GasPrice1559 { - base_fee_per_gas: gas_price, - // factor in 1 block of maximal gas increase - max_fee_per_gas: gas_price * 1.125, - max_priority_fee_per_gas: 0., - }, - id, - ) - .await - .map_err(|error| anyhow!("rating failed with {:?}", error))?; - - let gas_cost = simulation - .gas_estimate - .checked_mul(U256::from_f64_lossy(gas_price)) - .ok_or_else(|| anyhow::anyhow!("overflow during gas cost computation"))?; - - intermediate.into_settlement(external_prices, &gas_cost) - } -} - -enum SolveResult { - /// Found a solution for the order. - Solved(SingleOrderSettlement), - /// No solution but order could be retried. - Retryable(LimitOrder), - /// No solution and retrying would not help. - Failed, - /// The single solver solver is rate limiting, back off until the next - /// auction (as all single order solves will fail anyway). - RateLimited, -} - -#[async_trait::async_trait] -impl Solver for SingleOrderSolver { - async fn solve( - &self, - Auction { - orders, - balances, - external_prices, - gas_price, - deadline, - .. - }: Auction, - ) -> Result> { - let orders = super::balance_and_convert_orders( - self.ethflow_contract, - &self.order_converter, - balances, - orders, - &external_prices, - ); - let mut orders = - get_prioritized_orders(&orders, &external_prices, &self.order_prioritization_config); - tracing::trace!(?orders, "prioritized orders"); - - let mut settlements = Vec::new(); - let (tx, mut rx) = mpsc::unbounded_channel::(); - - let solve = async { - while let Some(order) = orders.pop_front() { - let span = tracing::info_span!("order", id =? order.id); - match self - .solve_single_order(order, &external_prices, gas_price) - .instrument(span) - .await - { - SolveResult::Failed => continue, - SolveResult::Retryable(order) => orders.push_back(order), - SolveResult::Solved(settlement) => { - let _ = tx.send(settlement); - } - SolveResult::RateLimited => { - tracing::warn!( - solver = %self.name(), - "rate limited; backing off until next auction" - ); - break; - } - } - } - drop(tx); - }; - - let finalize = async { - let mut index = 0; - while let Some(intermediate) = rx.recv().await { - let id = intermediate.order.id.clone(); - let span = tracing::info_span!("order", ?id); - match self - .finalize_settlement(intermediate, &external_prices, gas_price, index) - .instrument(span) - .await - { - Ok(Some(settlement)) => { - settlements.push(settlement); - } - Ok(None) => (), - Err(err) => { - tracing::warn!(?err, ?id, "failed to finalize intermediate settlement"); - } - } - index += 1; - } - }; - - // Subtract a small amount of time to ensure that the driver doesn't reach the - // deadline first. - let timeout = - tokio::time::sleep_until(deadline.checked_sub(Duration::from_secs(1)).unwrap().into()); - - // open new scope for solve->finalize pipeline to make borrow checker happy - { - let solve = solve.fuse(); - futures::pin_mut!(solve); - futures::pin_mut!(finalize); - futures::pin_mut!(timeout); - loop { - tokio::select! { - // if `solve` stops early there are still settlements to be finalized - _ = &mut solve => (), - // all possible settlements have been finalized - _ = &mut finalize => break, - // we reached the timeout and should return the results ASAP - _ = &mut timeout => break, - } - } - } - - self.fills.collect_garbage(); - - // Shuffle so that in the case a buggy solver keeps returning some amount - // of invalid settlements first we have a chance to make progress. - settlements.shuffle(&mut rand::thread_rng()); - // Keep at most this many settlements to not overwhelm the node with too many - // simulations. - settlements.truncate(self.max_settlements_per_solver); - - merge::merge_settlements( - self.max_merged_settlements, - &external_prices, - &mut settlements, - ); - - Ok(settlements) - } - - fn account(&self) -> &Account { - self.inner.account() - } - - fn name(&self) -> &'static str { - self.inner.name() - } -} - -#[derive(Clone, Debug)] -pub struct SingleOrderSettlement { - pub sell_token_price: U256, - pub buy_token_price: U256, - pub interactions: Vec>, - pub order: LimitOrder, - pub executed_amount: U256, -} - -impl SingleOrderSettlement { - pub fn into_settlement( - &self, - prices: &ExternalPrices, - gas_cost: &U256, - ) -> Result> { - let order = &self.order; - let executed_amount = self.executed_amount; - // Compute the expected traded amounts. - let (traded_sell_amount, traded_buy_amount) = match order.kind { - OrderKind::Buy => ( - executed_amount - .checked_mul(self.buy_token_price) - .context("buy value overflow")? - .checked_div(self.sell_token_price) - .context("zero sell token price")?, - executed_amount, - ), - OrderKind::Sell => ( - executed_amount, - executed_amount - .checked_mul(self.sell_token_price) - .context("sell value overflow")? - .checked_ceil_div(&self.buy_token_price) - .context("zero buy token price")?, - ), - }; - - // Compute the surplus fee that needs to be incorporated into the prices - // and fee which will be used for execution. - let (surplus_fee, fee) = if order.solver_determines_fee() { - let fee = number::conversions::big_rational_to_u256( - &prices - .try_get_token_amount( - &number::conversions::u256_to_big_rational(gas_cost), - order.sell_token, - ) - .context("failed to compute solver fee")?, - ) - .context("invalid solver fee amount")?; - - (fee, fee) - } else { - (U256::zero(), order.user_fee) - }; - - // Compute the actual executed amounts accounting for surplus fees. - let (executed_sell_amount, executed_buy_amount) = match order.kind { - OrderKind::Buy => ( - traded_sell_amount - .checked_add(surplus_fee) - .context("underflow computing executed sell amount")?, - traded_buy_amount, - ), - OrderKind::Sell => { - let executed_sell_amount = traded_sell_amount - .checked_add(surplus_fee) - .context("overflow computing executed sell amount")? - .min(order.sell_amount); - let executed_buy_amount = match executed_sell_amount.checked_sub(surplus_fee) { - Some(i) => i, - // The fee is larger than the sell amount so it is not possible to fulfill it. - None => return Ok(None), - } - .checked_mul(traded_buy_amount) - .context("overflow computing executed buy amount")? - .checked_ceil_div(&traded_sell_amount) - .context("zero traded sell amount")?; - - (executed_sell_amount, executed_buy_amount) - } - }; - - // Check that the order's limit price is satisfied accounting for the - // surplus fees. - if self - .order - .sell_amount - .checked_mul(executed_buy_amount) - .context("overflow sell value")? - < self - .order - .buy_amount - .checked_mul(executed_sell_amount) - .context("overflow buy value")? - { - tracing::debug!( - ?surplus_fee, - ?self.sell_token_price, - ?self.buy_token_price, - ?order, - "order limit price not respected after applying surplus fees", - ); - return Ok(None); - } - - let prices = [ - (order.sell_token, executed_buy_amount), - (order.buy_token, executed_sell_amount - surplus_fee), - ]; - let mut settlement = Settlement::new(prices.into_iter().collect()); - let execution = LimitOrderExecution { - filled: match order.kind { - OrderKind::Buy => executed_buy_amount, - OrderKind::Sell => executed_sell_amount - surplus_fee, - }, - fee, - }; - settlement.with_liquidity(order, execution)?; - for interaction in &self.interactions { - settlement - .encoder - .append_to_execution_plan(interaction.clone()); - } - Ok(Some(settlement)) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum SettlementError { - #[error("rate limited")] - RateLimited, - - #[error("intermittent error: {0}")] - Retryable(anyhow::Error), - - /// Benign errors are failures of the solver in finding a solution which - /// are expected in some circumstances and do not denote a failure in the - /// inner working of the single order solver. - #[error("benign error: {0}")] - Benign(anyhow::Error), - - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -/// Prioritizes orders to settle in the auction. First come all the market -/// orders and then all the limit orders. Orders within these groups get -/// prioritized by their price achievability. See [`prioritize_orders`]. -fn get_prioritized_orders( - orders: &[LimitOrder], - prices: &ExternalPrices, - order_prioritization_config: &OrderPrioritization, -) -> VecDeque { - let (market, limit) = orders - .iter() - .filter(|order| !matches!(order.id, LimitOrderId::Liquidity(_))) - .cloned() - .partition(|order| matches!(order.id, LimitOrderId::Market(_))); - - let market = prioritize_orders(market, prices, order_prioritization_config); - let limit = prioritize_orders(limit, prices, order_prioritization_config); - - market.into_iter().chain(limit).collect() -} - -/// Returns the `native_sell_amount / native_buy_amount` of the given order -/// under the current market conditions. The higher the value the more likely it -/// is that this order could get filled. -fn estimate_price_viability(order: &LimitOrder, prices: &ExternalPrices) -> f64 { - let sell_amount = u256_to_big_rational(&order.sell_amount); - let buy_amount = u256_to_big_rational(&order.buy_amount); - let native_sell_amount = prices.get_native_amount(order.sell_token, sell_amount); - let native_buy_amount = prices.get_native_amount(order.buy_token, buy_amount); - native_sell_amount - .checked_div(&native_buy_amount) - .and_then(|ratio| ratio.to_f64()) - .unwrap_or(0.) -} - -/// In case there are too many orders to solve before the auction deadline we -/// want to prioritize orders which are more likely to be matchable. This is -/// implemented by rating each order's viability by comparing the ask price with -/// the current market price. The lower the ask price is compared to the market -/// price the higher the chance the order will get prioritized. -fn prioritize_orders( - mut orders: Vec, - prices: &ExternalPrices, - order_prioritization_config: &OrderPrioritization, -) -> Vec { - if orders.len() <= 1 { - return orders; - } - - let mut rng = rand::thread_rng(); - - // Chose `user_orders.len()` distinct items from `user_orders` weighted by the - // viability of the order. This effectively sorts the orders by viability - // with a slight randomness to not get stuck on bad orders. - let weighted_order = orders.choose_multiple_weighted(&mut rng, orders.len(), |order| { - let price_viability = estimate_price_viability(order, prices); - order_prioritization_config.apply_weight_constraints(price_viability) - }); - match weighted_order { - Ok(weighted_user_orders) => weighted_user_orders.into_iter().cloned().collect(), - Err(err) => { - // if weighted sorting by viability fails we fall back to shuffling randomly - tracing::warn!(?err, "weighted order prioritization failed"); - orders.shuffle(&mut rng); - orders - } - } -} - -// Used by the single order solvers to verify that the response respects the -// order price. We have also observed that a 0x buy order did not respect the -// queried buy amount so verifying just the price or verifying just one -// component of the price (sell amount for buy orders, buy amount for sell -// orders) is not enough. -pub fn execution_respects_order( - order: &LimitOrder, - executed_sell_amount: U256, - executed_buy_amount: U256, -) -> bool { - executed_sell_amount <= order.sell_amount - && if order.partially_fillable { - order.sell_amount.full_mul(executed_buy_amount) - >= executed_sell_amount.full_mul(order.buy_amount) - } else { - executed_buy_amount >= order.buy_amount - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - liquidity::{order_converter::OrderConverter, LimitOrderId, LiquidityOrderId}, - metrics::NoopMetrics, - order_balance_filter::{max_balance, BalancedOrder}, - settlement::TradeExecution, - settlement_rater::MockSettlementRating, - }, - anyhow::anyhow, - ethcontract::Bytes, - maplit::{btreemap, hashmap}, - mockall::Sequence, - model::order::{Order, OrderClass, OrderData, OrderKind, OrderMetadata, OrderUid}, - num::{BigRational, FromPrimitive}, - primitive_types::H160, - shared::{ - http_solver::model::InternalizationStrategy, - price_estimation::gas::SETTLEMENT_OVERHEAD, - }, - std::{collections::HashMap, sync::Arc}, - }; - - fn test_solver(inner: MockSingleOrderSolving) -> SingleOrderSolver { - let mut settlement_rating = MockSettlementRating::new(); - settlement_rating - .expect_rate_settlement() - .returning(|_, _, _, _, _| Ok(Default::default())); - - SingleOrderSolver { - inner: Box::new(inner), - metrics: Arc::new(NoopMetrics::default()), - max_merged_settlements: 5, - max_settlements_per_solver: 5, - order_prioritization_config: Default::default(), - rate_limiter: RateLimiter::test(), - fills: fills::Fills::new(1.into()), - settlement_rater: Arc::new(settlement_rating), - ethflow_contract: None, - order_converter: OrderConverter::test(Default::default()), - } - } - - #[tokio::test] - async fn merges() { - let native = H160::from_low_u64_be(0); - let buy_order = Order { - data: OrderData { - sell_token: H160::from_low_u64_be(1), - buy_token: H160::from_low_u64_be(2), - kind: OrderKind::Buy, - sell_amount: 2.into(), - buy_amount: 1.into(), - ..Default::default() - }, - metadata: OrderMetadata { - uid: OrderUid([0u8; 56]), - ..Default::default() - }, - ..Default::default() - }; - let sell_order = Order { - data: OrderData { - sell_token: H160::from_low_u64_be(3), - buy_token: H160::from_low_u64_be(4), - sell_amount: 7.into(), - buy_amount: 6.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - metadata: OrderMetadata { - uid: OrderUid([1u8; 56]), - ..Default::default() - }, - ..Default::default() - }; - let orders: Vec = vec![buy_order.clone(), sell_order.clone()]; - let balances = max_balance(&orders); - - let mut inner = MockSingleOrderSolving::new(); - inner - .expect_try_settle_order() - .returning(|order, _, _| match order.kind { - OrderKind::Buy => Ok(Some(SingleOrderSettlement { - sell_token_price: 1.into(), - buy_token_price: 2.into(), - interactions: vec![Arc::new(( - H160::from_low_u64_be(3), - 4.into(), - Bytes(vec![5]), - ))], - executed_amount: order.full_execution_amount(), - order, - })), - OrderKind::Sell => Ok(Some(SingleOrderSettlement { - sell_token_price: 6.into(), - buy_token_price: 7.into(), - interactions: vec![Arc::new(( - H160::from_low_u64_be(8), - 9.into(), - Bytes(vec![10]), - ))], - executed_amount: order.full_execution_amount(), - order, - })), - }); - inner - .expect_account() - .return_const(Account::Local(Default::default(), None)); - inner.expect_name().returning(|| ""); - - let solver = test_solver(inner); - let external_prices = ExternalPrices::try_from_auction_prices( - native, - [ - buy_order.data.sell_token, - buy_order.data.buy_token, - sell_order.data.sell_token, - sell_order.data.buy_token, - ] - .into_iter() - .map(|token| (token, U256::from(1))) - .collect(), - ) - .unwrap(); - let settlements = solver - .solve(Auction { - external_prices, - orders, - balances, - ..Default::default() - }) - .await - .unwrap(); - assert_eq!(settlements.len(), 3); - - let merged = settlements.into_iter().nth(2).unwrap().encoder; - let merged = merged.finish(InternalizationStrategy::EncodeAllInteractions); - assert_eq!(merged.tokens.len(), 4); - let token_index = |token: &H160| -> usize { - merged - .tokens - .iter() - .position(|token_| token_ == token) - .unwrap() - }; - let prices = &merged.clearing_prices; - assert_eq!(prices[token_index(&buy_order.data.sell_token)], 1.into()); - assert_eq!(prices[token_index(&buy_order.data.buy_token)], 2.into()); - assert_eq!(prices[token_index(&sell_order.data.sell_token)], 6.into()); - assert_eq!(prices[token_index(&sell_order.data.buy_token)], 7.into()); - assert_eq!(merged.trades.len(), 2); - assert_eq!(merged.interactions.iter().flatten().count(), 2); - } - - #[tokio::test] - async fn retries_retryable() { - let mut inner = MockSingleOrderSolving::new(); - inner.expect_name().return_const(""); - let mut call_count = 0u32; - inner - .expect_try_settle_order() - .times(2) - .returning(move |_, _, _| { - dbg!(); - let result = match call_count { - 0 => Err(SettlementError::Retryable(anyhow!(""))), - 1 => Ok(None), - _ => unreachable!(), - }; - call_count += 1; - result - }); - - let solver = test_solver(inner); - let orders = vec![Order { - data: OrderData { - sell_amount: 1.into(), - buy_amount: 1.into(), - ..Default::default() - }, - ..Default::default() - }]; - let balances = max_balance(&orders); - solver - .solve(Auction { - orders, - balances, - ..Default::default() - }) - .await - .unwrap(); - } - - #[tokio::test] - async fn does_not_retry_unretryable() { - let mut inner = MockSingleOrderSolving::new(); - let mut seq = Sequence::new(); - inner.expect_name().return_const(""); - inner - .expect_try_settle_order() - .times(1) - .returning(|_, _, _| Err(SettlementError::Other(anyhow!("")))) - .in_sequence(&mut seq); - - let solver = test_solver(inner); - let orders = vec![Order { - data: OrderData { - sell_amount: 1.into(), - buy_amount: 1.into(), - ..Default::default() - }, - ..Default::default() - }]; - let balances = max_balance(&orders); - solver - .solve(Auction { - orders, - balances, - ..Default::default() - }) - .await - .unwrap(); - } - - #[tokio::test] - async fn stops_trying_when_rate_limited() { - let mut inner = MockSingleOrderSolving::new(); - let mut seq = Sequence::new(); - inner.expect_name().return_const(""); - inner - .expect_try_settle_order() - .times(1) - .returning(|_, _, _| Err(SettlementError::RateLimited)) - .in_sequence(&mut seq); - - let solver = test_solver(inner); - let orders = vec![ - Order { - data: OrderData { - sell_amount: 1.into(), - buy_amount: 1.into(), - ..Default::default() - }, - ..Default::default() - }; - 3 - ]; - let balances = max_balance(&orders); - solver - .solve(Auction { - orders, - balances, - ..Default::default() - }) - .await - .unwrap(); - } - - #[test] - fn execution_respects_order_() { - let order = LimitOrder { - kind: OrderKind::Sell, - sell_amount: 10.into(), - buy_amount: 10.into(), - ..Default::default() - }; - assert!(execution_respects_order(&order, 10.into(), 11.into(),)); - assert!(!execution_respects_order(&order, 10.into(), 9.into(),)); - // Unexpectedly the executed sell amount is less than the real sell order for a - // fill kill order but we still get enough buy token. - assert!(execution_respects_order(&order, 9.into(), 10.into(),)); - // Price is respected but order is partially filled. - assert!(!execution_respects_order(&order, 9.into(), 9.into(),)); - - let order = LimitOrder { - kind: OrderKind::Buy, - ..order - }; - assert!(execution_respects_order(&order, 9.into(), 10.into(),)); - assert!(!execution_respects_order(&order, 11.into(), 10.into(),)); - // Unexpectedly get more buy amount but sell amount is still respected. - assert!(execution_respects_order(&order, 10.into(), 11.into(),)); - // Price is respected but order is partially filled. - assert!(!execution_respects_order(&order, 9.into(), 9.into(),)); - - let order = LimitOrder { - kind: OrderKind::Sell, - sell_amount: 10.into(), - buy_amount: 20.into(), - partially_fillable: true, - ..Default::default() - }; - assert!(execution_respects_order(&order, 10.into(), 20.into())); - assert!(execution_respects_order(&order, 10.into(), 21.into())); - assert!(!execution_respects_order(&order, 10.into(), 19.into())); - assert!(!execution_respects_order(&order, 11.into(), 23.into())); - assert!(execution_respects_order(&order, 5.into(), 10.into())); - assert!(execution_respects_order(&order, 5.into(), 11.into())); - assert!(!execution_respects_order(&order, 5.into(), 9.into())); - } - - #[ignore] // ignore this test because it could fail due to randomness - #[test] - fn spread_orders_get_prioritized() { - let token = H160::from_low_u64_be; - let amount = U256::from; - let order = |sell_amount: u128, id: LimitOrderId| LimitOrder { - id, - sell_token: token(1), - sell_amount: amount(sell_amount), - buy_token: token(2), - buy_amount: amount(100), - ..Default::default() - }; - let orders = vec![ - order( - 500, - LimitOrderId::Liquidity(LiquidityOrderId::Protocol(OrderUid::from_integer(1))), - ), //liquidity order - order(90, Default::default()), //market order - order(100, Default::default()), //market order - order(130, Default::default()), //market order - ]; - let prices = ExternalPrices::new( - token(0), - hashmap! { - token(1) => BigRational::from_u8(100).unwrap(), - token(2) => BigRational::from_u8(100).unwrap(), - }, - ) - .unwrap(); - - let config = OrderPrioritization::default(); - - const SAMPLES: usize = 1_000; - let mut expected_results = 0; - for _ in 0..SAMPLES { - let prioritized_orders = prioritize_orders(orders.clone(), &prices, &config); - let expected_output = &[orders[3].clone(), orders[2].clone(), orders[1].clone()]; - expected_results += usize::from(prioritized_orders == expected_output); - } - // Using weighted selection should give us some suboptimal orderings even with - // skewed weights. - dbg!(expected_results); - assert!((expected_results as f64) < (SAMPLES as f64 * 0.9)); - } - - #[ignore] // ignore this test because it could fail due to randomness - #[test] - fn tight_orders_get_prioritized() { - let token = H160::from_low_u64_be; - let amount = U256::from; - let order = |sell_amount: u128, id: LimitOrderId| LimitOrder { - id, - sell_token: token(1), - sell_amount: amount(sell_amount), - buy_token: token(2), - buy_amount: amount(100), - ..Default::default() - }; - let orders = vec![ - order(105, Default::default()), //market order - order(103, Default::default()), //market order - order(101, Default::default()), //market order - ]; - let prices = ExternalPrices::new( - token(0), - hashmap! { - token(1) => BigRational::from_u8(100).unwrap(), - token(2) => BigRational::from_u8(100).unwrap(), - }, - ) - .unwrap(); - - let config = OrderPrioritization::default(); - - const SAMPLES: usize = 1_000; - let mut expected_results = 0; - for _ in 0..SAMPLES { - let prioritized_orders = prioritize_orders(orders.clone(), &prices, &config); - expected_results += usize::from(prioritized_orders == orders); - } - // Using weighted selection should give us some suboptimal orderings even with - // skewed weights. - dbg!(expected_results); - assert!((expected_results as f64) < (SAMPLES as f64 * 0.9)); - } - - #[test] - fn partially_fillable_single_order_settlements() { - let native = H160::from_low_u64_be(0); - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(2); - - let converter = OrderConverter::test(native); - let order = |kind: OrderKind| { - converter - .normalize_limit_order(BalancedOrder { - order: Order { - data: OrderData { - sell_token: H160::from_low_u64_be(1), - buy_token: H160::from_low_u64_be(2), - kind, - sell_amount: 100.into(), - buy_amount: 100.into(), - partially_fillable: true, - ..Default::default() - }, - metadata: OrderMetadata { - uid: OrderUid([0u8; 56]), - class: OrderClass::Limit, - ..Default::default() - }, - ..Default::default() - }, - available_sell_token_balance: 100.into(), - }) - .unwrap() - }; - - let base = U256::from(10_u128.pow(18)); - let prices = ExternalPrices::try_from_auction_prices( - native, - btreemap! { - native => base, - sell_token => base * 50000, - buy_token => base * 50000, - }, - ) - .unwrap(); - - let settlement = |order: LimitOrder, in_amount: u128, out_amount: u128| { - SingleOrderSettlement { - sell_token_price: out_amount.into(), - buy_token_price: in_amount.into(), - interactions: vec![], - executed_amount: match order.kind { - OrderKind::Sell => in_amount.into(), - OrderKind::Buy => out_amount.into(), - }, - order, - } - .into_settlement(&prices, &SETTLEMENT_OVERHEAD.into()) - .unwrap() - }; - let trade = |settlement: Settlement| settlement.trade_executions().next().unwrap(); - - // Not enough room for surplus fees. - assert!(settlement(order(OrderKind::Buy), 50, 50).is_none()); - assert!(settlement(order(OrderKind::Buy), 100, 100).is_none()); - assert!(settlement(order(OrderKind::Sell), 50, 50).is_none()); - assert!(settlement(order(OrderKind::Sell), 100, 100).is_none()); - - // Adds surplus fee to executed sell amount. - assert_eq!( - trade(settlement(order(OrderKind::Buy), 40, 50).unwrap()), - TradeExecution { - sell_token, - buy_token, - sell_amount: 42.into(), - buy_amount: 50.into(), - fee_amount: 0.into(), - } - ); - assert_eq!( - trade(settlement(order(OrderKind::Buy), 90, 100).unwrap()), - TradeExecution { - sell_token, - buy_token, - sell_amount: 92.into(), - buy_amount: 100.into(), - fee_amount: 0.into(), - } - ); - assert_eq!( - trade(settlement(order(OrderKind::Sell), 50, 60).unwrap()), - TradeExecution { - sell_token, - buy_token, - sell_amount: 52.into(), - buy_amount: 60.into(), - fee_amount: 0.into(), - } - ); - - // Scale buy amount if insufficient "space" for collecting surplus fees - // in the sell token. - assert_eq!( - trade(settlement(order(OrderKind::Sell), 99, 110).unwrap()), - TradeExecution { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 109.into(), - fee_amount: 0.into(), - } - ); - assert_eq!( - trade(settlement(order(OrderKind::Sell), 100, 110).unwrap()), - TradeExecution { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 108.into(), - fee_amount: 0.into(), - } - ); - - // Fee is larger than total order sell amount. - let settlement = SingleOrderSettlement { - sell_token_price: 1.into(), - buy_token_price: 1.into(), - interactions: vec![], - executed_amount: 100.into(), - order: order(OrderKind::Sell), - }; - let result = settlement.into_settlement(&prices, &1_000_000.into()); - assert!(matches!(result, Ok(None)), "{:?}", result); - } - - #[test] - fn order_priotization_weight_does_not_panic_on_zeros() { - let token = H160::from_low_u64_be; - let amount = U256::from; - let prices = - |prices: HashMap| ExternalPrices::new(token(0), prices).unwrap(); - - assert_eq!( - estimate_price_viability( - &LimitOrder { - sell_token: token(1), - sell_amount: amount(100), - buy_token: token(2), - buy_amount: amount(100), - ..Default::default() - }, - &prices(hashmap! { - token(1) => BigRational::from_u8(1).unwrap(), - token(2) => BigRational::from_u8(0).unwrap(), - }) - ), - 0. - ); - - assert_eq!( - estimate_price_viability( - &LimitOrder { - sell_token: token(1), - sell_amount: amount(100), - buy_token: token(2), - buy_amount: amount(0), - ..Default::default() - }, - &prices(hashmap! { - token(1) => BigRational::from_u8(1).unwrap(), - token(2) => BigRational::from_u8(1).unwrap(), - }) - ), - 0. - ); - } -} diff --git a/crates/solver/src/solver/single_order_solver/fills.rs b/crates/solver/src/solver/single_order_solver/fills.rs deleted file mode 100644 index 537e1c7297..0000000000 --- a/crates/solver/src/solver/single_order_solver/fills.rs +++ /dev/null @@ -1,185 +0,0 @@ -use { - crate::liquidity::LimitOrder, - ethcontract::U256, - model::order::{OrderKind, OrderUid}, - num::BigRational, - shared::external_prices::ExternalPrices, - std::{ - collections::HashMap, - sync::Mutex, - time::{Duration, Instant}, - }, -}; - -/// Manages the search for a fillable amount for all order types but -/// specifically for partially fillable orders. -#[derive(Debug)] -pub struct Fills { - /// Maps which fill amount should be tried next for a given order. For sell - /// orders the amount refers to the `sell` asset and for buy orders it - /// refers to the `buy` asset. - amounts: Mutex>, - /// The smallest value in ETH we consider trying a partially fillable order - /// with. If we move below this threshold we'll restart from 100% fill - /// amount to not eventually converge at 0. - smallest_fill: BigRational, -} - -/// The reason `Fills::order` failed. -#[derive(Debug)] -pub enum Error { - MissingPrice, - /// The smallest fill can't be represented in the order's token. - SmallestFillU256Conversion, - /// The order doesn't have a UID (`LimitOrder`s can be created from non GPv2 - /// orders). - NoOrderUid, - /// The resulting amount would be less than `Fills::smallest_fill`. - LessThanSmallestFill, - /// One of the order's amounts is 0. - ZeroAmount, -} - -impl Fills { - pub fn new(smallest_fill: U256) -> Self { - Self { - amounts: Default::default(), - smallest_fill: number::conversions::u256_to_big_rational(&smallest_fill), - } - } - - /// Returns which dex query should be tried for the given order. Takes - /// information of previous partial fill attempts into account. - pub fn order(&self, order: &LimitOrder, prices: &ExternalPrices) -> Result { - if !order.partially_fillable { - return Ok(order.clone()); - } - - let (token, total_amount) = match order.kind { - OrderKind::Buy => (order.buy_token, order.buy_amount), - OrderKind::Sell => (order.sell_token, order.sell_amount), - }; - - let smallest_fill = prices - .try_get_token_amount(&self.smallest_fill, token) - .ok_or(Error::MissingPrice)?; - let smallest_fill = number::conversions::big_rational_to_u256(&smallest_fill) - .map_err(|_| Error::SmallestFillU256Conversion)?; - - let now = Instant::now(); - - let amount = match self - .amounts - .lock() - .unwrap() - .entry(order.id.order_uid().ok_or(Error::NoOrderUid)?) - { - std::collections::hash_map::Entry::Vacant(entry) => { - entry.insert(CacheEntry { - next_amount: total_amount, - last_requested: now, - total_amount, - }); - total_amount - } - std::collections::hash_map::Entry::Occupied(mut entry) => { - let entry = entry.get_mut(); - entry.last_requested = now; - entry.total_amount = total_amount; - - if entry.next_amount < smallest_fill { - tracing::trace!( - ?smallest_fill, - target =? entry.next_amount, - "target fill got too small; starting over" - ); - entry.next_amount = total_amount; - } else if entry.next_amount > total_amount { - tracing::trace!("partially filled; adjusting to new total amount"); - entry.next_amount = total_amount; - } - - entry.next_amount - } - }; - - if amount < smallest_fill { - return Err(Error::LessThanSmallestFill); - } - - // Scale amounts according to the limit price and the chosen fill. - let (sell_amount, buy_amount) = match order.kind { - OrderKind::Buy => { - let sell_amount = order - .sell_amount - .full_mul(amount) - .checked_div(order.buy_amount.into()) - .ok_or(Error::ZeroAmount)? - .try_into() - .unwrap(); - (sell_amount, amount) - } - OrderKind::Sell => { - let buy_amount = order - .buy_amount - .full_mul(amount) - .checked_div(order.sell_amount.into()) - .ok_or(Error::ZeroAmount)? - .try_into() - .unwrap(); - (amount, buy_amount) - } - }; - - Ok(LimitOrder { - sell_amount, - buy_amount, - ..order.clone() - }) - } - - /// Adjusts the next fill amount that should be tried. Always halves the - /// last tried amount. - // TODO: make use of `price_impact` provided by some APIs to get a more optimal - // next try. - pub fn reduce_next_try(&self, uid: OrderUid) { - self.amounts.lock().unwrap().entry(uid).and_modify(|entry| { - entry.next_amount /= 2; - tracing::trace!(next_try =? entry.next_amount, "decreased next fill amount"); - }); - } - - /// Adjusts the next fill amount that should be tried. Doubles the amount to - /// try. This is useful in case the onchain liquidity changed and now - /// allows for bigger fills. - pub fn increase_next_try(&self, uid: OrderUid) { - self.amounts.lock().unwrap().entry(uid).and_modify(|entry| { - entry.next_amount = entry - .next_amount - .checked_mul(2.into()) - .unwrap_or(entry.total_amount) - .min(entry.total_amount); - tracing::trace!(next_try =? entry.next_amount, "increased next fill amount"); - }); - } - - /// Removes entries that have not been requested for a long time. This - /// allows us to remove orders that got settled by other solvers which - /// we are not able to notice. - pub fn collect_garbage(&self) { - const MAX_AGE: Duration = Duration::from_secs(60 * 10); - let now = Instant::now(); - - self.amounts - .lock() - .unwrap() - .retain(|_, entry| now.duration_since(entry.last_requested) < MAX_AGE) - } -} - -#[derive(Debug)] -struct CacheEntry { - next_amount: U256, - last_requested: Instant, - total_amount: U256, -} diff --git a/crates/solver/src/solver/single_order_solver/merge.rs b/crates/solver/src/solver/single_order_solver/merge.rs deleted file mode 100644 index f7ef741517..0000000000 --- a/crates/solver/src/solver/single_order_solver/merge.rs +++ /dev/null @@ -1,135 +0,0 @@ -use {crate::settlement::Settlement, shared::external_prices::ExternalPrices}; - -// Takes the settlements of a single solver and adds a merged settlement. -pub fn merge_settlements( - max_merged_settlements: usize, - prices: &ExternalPrices, - settlements: &mut Vec, -) { - settlements.sort_by_cached_key(|a| -a.total_surplus(prices)); - - if let Some(settlement) = - merge_at_most_settlements(max_merged_settlements, settlements.clone().into_iter()) - { - settlements.push(settlement); - } -} - -// Goes through the settlements in order and tries to merge a number of them. -// Keeps going on merge error. -fn merge_at_most_settlements( - max_merges: usize, - mut settlements: impl Iterator, -) -> Option { - let mut merged = settlements.next()?; - let mut merge_count = 1; - while merge_count < max_merges { - let next = match settlements.next() { - Some(settlement) => settlement, - None => break, - }; - merged = match merged.clone().merge(next) { - Ok(settlement) => settlement, - Err(err) => { - tracing::debug!("failed to merge settlement: {:?}", err); - continue; - } - }; - merge_count += 1; - } - if merge_count > 1 { - Some(merged) - } else { - None - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::settlement::Trade, - maplit::hashmap, - model::order::{Order, OrderData, OrderKind, OrderMetadata, OrderUid}, - num::{BigRational, One}, - primitive_types::{H160, U256}, - shared::externalprices, - std::collections::HashSet, - }; - - #[test] - fn merges_settlements_with_highest_objective_value() { - let token0 = H160::from_low_u64_be(0); - let token1 = H160::from_low_u64_be(1); - let prices = hashmap! { token0 => 1u32.into(), token1 => 1u32.into()}; - let external_prices = externalprices! { - native_token: token0, - token1 => BigRational::one(), - }; - fn uid(number: u8) -> OrderUid { - OrderUid([number; 56]) - } - - let trade = |sell_amount, uid_: u8| Trade { - executed_amount: 1.into(), - order: Order { - metadata: OrderMetadata { - uid: uid(uid_), - ..Default::default() - }, - data: OrderData { - sell_token: token0, - buy_token: token1, - sell_amount, - buy_amount: 1.into(), - kind: OrderKind::Buy, - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - }; - let settlement = |executed_amount: U256, order_uid: u8| { - Settlement::with_trades(prices.clone(), vec![trade(executed_amount, order_uid)]) - }; - - let mut settlements = vec![ - settlement(1.into(), 1), - settlement(2.into(), 2), - settlement(3.into(), 3), - ]; - merge_settlements(2, &external_prices, &mut settlements); - - assert_eq!(settlements.len(), 4); - assert!(settlements.iter().any(|settlement| { - let uids: HashSet = settlement - .traded_orders() - .map(|order| order.metadata.uid) - .collect(); - uids.len() == 2 && uids.contains(&uid(2)) && uids.contains(&uid(3)) - })); - } - - #[test] - fn merge_continues_on_error() { - let token0 = H160::from_low_u64_be(0); - let token1 = H160::from_low_u64_be(1); - let settlement0 = Settlement::new(hashmap! {token0 => 1.into(), token1 => 2.into()}); - let settlement1 = Settlement::new(hashmap! {token0 => 2.into(), token1 => 2.into()}); - let settlement2 = Settlement::new(hashmap! {token0 => 1.into(), token1 => 2.into()}); - let settlements = vec![settlement0, settlement1, settlement2]; - - // Can't merge 0 with 1 because token0 and token1 clearing prices are different. - let merged = merge_at_most_settlements(2, settlements.into_iter()).unwrap(); - assert_eq!(merged.clearing_price(token0), Some(1.into())); - assert_eq!(merged.clearing_price(token1), Some(2.into())); - } - - #[test] - fn merge_does_nothing_on_max_1_merge() { - let token0 = H160::from_low_u64_be(0); - let settlement = Settlement::new(hashmap! {token0 => 0.into()}); - let settlements = vec![settlement.clone(), settlement]; - assert!(merge_at_most_settlements(1, settlements.into_iter()).is_none()); - } -} diff --git a/crates/solver/src/solver/zeroex_solver.rs b/crates/solver/src/solver/zeroex_solver.rs deleted file mode 100644 index f0ff859a6b..0000000000 --- a/crates/solver/src/solver/zeroex_solver.rs +++ /dev/null @@ -1,520 +0,0 @@ -//! Module containing implementation of the 0x solver. -//! -//! This solver will simply use the 0x API to get a quote for a -//! single GPv2 order and produce a settlement directly against 0x. -//! -//! Please be aware of the following subtlety for buy orders: -//! 0x's API is adding the defined slippage on the sellAmount of a buy order -//! and then returns the surplus in the buy amount to the user. -//! I.e. if the user defines a 5% slippage, they will sell 5% more, and receive -//! 5% more buy-tokens than ordered. Here is on example tx: -//! https://dashboard.tenderly.co/gp-v2/staging/simulator/new?block=12735030&blockIndex=0&from=0xa6ddbd0de6b310819b49f680f65871bee85f517e&gas=8000000&gasPrice=0&value=0&contractAddress=0x3328f5f2cecaf00a2443082b657cedeaf70bfaef&rawFunctionInput=0x13d79a0b000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000029143e200000000000000000000000000000000000000000000000000470de4df820000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000036416d81e590ff67370e4523b9cd3257aa0a853c000000000000000000000000000000000000000000000000000000000291f64800000000000000000000000000000000000000000000000000470de4df8200000000000000000000000000000000000000000000000000000000000060dc5839000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000003dc140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000029143e2000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000410a7f27a6638cc9cdaba8266a15acef4cf7e1e1c9b9b2059391b7230b67bdfeb21f1d3aa45852f527a5040d3d7a190b92764a2c854f334b7eed579b390b85fd3f1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000003800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000def1c0ded9bec7f1a1670819833240f027b25effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000def1c0ded9bec7f1a1670819833240f027b25eff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000128d9627aa400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000002b220e100000000000000000000000000000000000000000000000000470de4df82000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2869584cd000000000000000000000000100000000000000000000000000000000000001100000000000000000000000000000000000000000000003239e38b8a60dc53b70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000&network=1 -//! This behavior has the following risks: The additional sell tokens from the -//! slippage are not provided by the user, hence the additional tokens might be -//! not available in the settlement contract. For smaller amounts this is -//! unlikely, as we always charge the fees also in the sell token, though, the -//! fee's might not always be sufficient. This risk should be covered in a -//! future PR. -//! -//! Sell orders are unproblematic, especially, since the positive slippage is -//! handed back from 0x - -use { - super::single_order_solver::{ - execution_respects_order, - SettlementError, - SingleOrderSettlement, - SingleOrderSolving, - }, - crate::{ - interactions::allowances::{AllowanceManager, AllowanceManaging, ApprovalRequest}, - liquidity::{slippage::SlippageCalculator, LimitOrder}, - }, - anyhow::{ensure, Context, Result}, - contracts::GPv2Settlement, - ethcontract::{Account, H160}, - model::order::OrderKind, - shared::{ - ethrpc::Web3, - external_prices::ExternalPrices, - zeroex_api::{Slippage, SwapQuery, ZeroExApi, ZeroExResponseError}, - }, - std::{ - fmt::{self, Display, Formatter}, - sync::Arc, - }, -}; - -/// A GPv2 solver that matches GP orders to direct 0x swaps. -pub struct ZeroExSolver { - account: Account, - api: Arc, - allowance_fetcher: Box, - excluded_sources: Vec, - slippage_calculator: SlippageCalculator, - settlement: H160, - enable_rfqt: bool, - enable_slippage_protection: bool, -} - -/// Chain ID for Mainnet. -const MAINNET_CHAIN_ID: u64 = 1; - -impl ZeroExSolver { - pub fn new( - account: Account, - web3: Web3, - settlement_contract: GPv2Settlement, - chain_id: u64, - api: Arc, - excluded_sources: Vec, - slippage_calculator: SlippageCalculator, - ) -> Result { - ensure!( - chain_id == MAINNET_CHAIN_ID, - "0x solver only supported on Mainnet", - ); - let allowance_fetcher = AllowanceManager::new(web3, settlement_contract.address()); - Ok(Self { - account, - allowance_fetcher: Box::new(allowance_fetcher), - api, - excluded_sources, - slippage_calculator, - settlement: settlement_contract.address(), - enable_rfqt: false, - enable_slippage_protection: false, - }) - } - - pub fn with_rfqt(mut self, enable: bool) -> Self { - self.enable_rfqt = enable; - self - } - - pub fn with_slippage_protection(mut self, enable: bool) -> Self { - self.enable_slippage_protection = enable; - self - } -} - -#[async_trait::async_trait] -impl SingleOrderSolving for ZeroExSolver { - async fn try_settle_order( - &self, - order: LimitOrder, - external_prices: &ExternalPrices, - _: f64, - ) -> Result, SettlementError> { - let (buy_amount, sell_amount) = match order.kind { - OrderKind::Buy => (Some(order.buy_amount), None), - OrderKind::Sell => (None, Some(order.sell_amount)), - }; - let query = SwapQuery { - sell_token: order.sell_token, - buy_token: order.buy_token, - sell_amount, - buy_amount, - slippage_percentage: Some(Slippage::new( - self.slippage_calculator - .context(external_prices) - .relative_for_order(&order)? - .as_factor(), - )), - taker_address: Some(self.settlement), - excluded_sources: self.excluded_sources.clone(), - intent_on_filling: self.enable_rfqt, - enable_slippage_protection: self.enable_slippage_protection, - }; - let swap = match self.api.get_swap(query, true).await { - Ok(swap) => swap, - Err(ZeroExResponseError::InsufficientLiquidity) => { - tracing::debug!("Couldn't get a quote due to insufficient liquidity"); - return Ok(None); - } - Err(err) => { - return Err(err.into()); - } - }; - - if !execution_respects_order(&order, swap.price.sell_amount, swap.price.buy_amount) { - tracing::debug!("execution does not respect order"); - return Ok(None); - } - - let approval = self - .allowance_fetcher - .get_approval(&ApprovalRequest { - token: order.sell_token, - spender: swap.price.allowance_target, - amount: swap.price.sell_amount, - }) - .await - .context("get_approval")?; - - let mut settlement = SingleOrderSettlement { - sell_token_price: swap.price.buy_amount, - buy_token_price: swap.price.sell_amount, - interactions: Vec::new(), - executed_amount: order.full_execution_amount(), - order, - }; - if let Some(approval) = &approval { - settlement.interactions.push(Arc::new(*approval)); - } - settlement.interactions.push(Arc::new(swap.clone())); - Ok(Some(settlement)) - } - - fn account(&self) -> &Account { - &self.account - } - - fn name(&self) -> &'static str { - "0x" - } -} - -impl From for SettlementError { - fn from(err: ZeroExResponseError) -> Self { - match err { - ZeroExResponseError::RateLimited => Self::RateLimited, - err @ ZeroExResponseError::ServerError(_) => Self::Retryable(err.into()), - err => Self::Other(err.into()), - } - } -} - -impl Display for ZeroExSolver { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "ZeroExSolver") - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{ - interactions::allowances::{Approval, MockAllowanceManaging}, - liquidity::LimitOrder, - test::account, - }, - contracts::{GPv2Settlement, WETH9}, - ethcontract::{futures::FutureExt as _, Web3, H160, U256}, - mockall::{predicate::*, Sequence}, - model::order::{Order, OrderData, OrderKind}, - shared::{ - ethrpc::{create_env_test_transport, create_test_transport}, - zeroex_api::{DefaultZeroExApi, MockZeroExApi, PriceResponse, SwapResponse}, - }, - }; - - #[tokio::test] - #[ignore] - async fn solve_sell_order_on_zeroex() { - let web3 = Web3::new(create_env_test_transport()); - let chain_id = web3.eth().chain_id().await.unwrap().as_u64(); - let settlement = GPv2Settlement::deployed(&web3).await.unwrap(); - - let weth = WETH9::deployed(&web3).await.unwrap(); - let gno = testlib::tokens::GNO; - - let solver = ZeroExSolver::new( - account(), - web3, - settlement, - chain_id, - Arc::new(DefaultZeroExApi::test()), - Default::default(), - SlippageCalculator::default(), - ) - .unwrap(); - let settlement = solver - .try_settle_order( - Order { - data: OrderData { - sell_token: weth.address(), - buy_token: gno, - sell_amount: 1_000_000_000_000_000_000u128.into(), - buy_amount: 2u128.into(), - kind: OrderKind::Sell, - ..Default::default() - }, - ..Default::default() - } - .into(), - &Default::default(), - 1., - ) - .await - .unwrap(); - - println!("{settlement:#?}"); - } - - #[tokio::test] - #[ignore] - async fn solve_buy_order_on_zeroex() { - let web3 = Web3::new(create_env_test_transport()); - let chain_id = web3.eth().chain_id().await.unwrap().as_u64(); - let settlement = GPv2Settlement::deployed(&web3).await.unwrap(); - - let weth = WETH9::deployed(&web3).await.unwrap(); - let gno = testlib::tokens::GNO; - - let solver = ZeroExSolver::new( - account(), - web3, - settlement, - chain_id, - Arc::new(DefaultZeroExApi::test()), - Default::default(), - SlippageCalculator::default(), - ) - .unwrap(); - let settlement = solver - .try_settle_order( - Order { - data: OrderData { - sell_token: weth.address(), - buy_token: gno, - sell_amount: 1_000_000_000_000_000_000u128.into(), - buy_amount: 1_000_000_000_000_000_000u128.into(), - kind: OrderKind::Buy, - ..Default::default() - }, - ..Default::default() - } - .into(), - &Default::default(), - 1., - ) - .await - .unwrap(); - - println!("{settlement:#?}"); - } - - #[tokio::test] - async fn test_satisfies_limit_price_for_orders() { - let mut client = MockZeroExApi::new(); - let mut allowance_fetcher = Box::new(MockAllowanceManaging::new()); - - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(1); - - let allowance_target = shared::addr!("def1c0ded9bec7f1a1670819833240f027b25eff"); - client.expect_get_swap().returning(move |_, _| { - async move { - Ok(SwapResponse { - price: PriceResponse { - sell_amount: U256::from_dec_str("100").unwrap(), - buy_amount: U256::from_dec_str("91").unwrap(), - allowance_target, - price: 0.91_f64, - estimated_gas: Default::default(), - }, - to: shared::addr!("0000000000000000000000000000000000000000"), - data: hex::decode("00").unwrap(), - value: U256::from_dec_str("0").unwrap(), - }) - } - .boxed() - }); - - allowance_fetcher - .expect_get_approval() - .times(2) - .with(eq(ApprovalRequest { - token: sell_token, - spender: allowance_target, - amount: U256::from(100), - })) - .returning(move |_| { - Ok(Some(Approval { - token: sell_token, - spender: allowance_target, - })) - }); - - let solver = ZeroExSolver { - account: account(), - api: Arc::new(client), - allowance_fetcher, - excluded_sources: Default::default(), - slippage_calculator: Default::default(), - settlement: testlib::protocol::SETTLEMENT, - enable_rfqt: false, - enable_slippage_protection: false, - }; - - let buy_order_passing_limit = LimitOrder { - sell_token, - buy_token, - sell_amount: 101.into(), - buy_amount: 91.into(), - kind: model::order::OrderKind::Buy, - ..Default::default() - }; - let buy_order_violating_limit = LimitOrder { - sell_token, - buy_token, - sell_amount: 99.into(), - buy_amount: 91.into(), - kind: model::order::OrderKind::Buy, - ..Default::default() - }; - let sell_order_passing_limit = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 90.into(), - kind: model::order::OrderKind::Sell, - ..Default::default() - }; - let sell_order_violating_limit = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 110.into(), - kind: model::order::OrderKind::Sell, - ..Default::default() - }; - - let result = solver - .try_settle_order(sell_order_passing_limit, &Default::default(), 1.) - .await - .unwrap() - .unwrap(); - assert_eq!(result.sell_token_price, 91.into()); - assert_eq!(result.buy_token_price, 100.into()); - - let result = solver - .try_settle_order(sell_order_violating_limit, &Default::default(), 1.) - .await - .unwrap(); - assert!(result.is_none()); - - let result = solver - .try_settle_order(buy_order_passing_limit, &Default::default(), 1.) - .await - .unwrap() - .unwrap(); - assert_eq!(result.sell_token_price, 91.into()); - assert_eq!(result.buy_token_price, 100.into()); - - let result = solver - .try_settle_order(buy_order_violating_limit, &Default::default(), 1.) - .await - .unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - #[ignore] - async fn returns_error_on_non_mainnet() { - let web3 = Web3::new(create_test_transport( - &std::env::var("NODE_URL_RINKEBY").unwrap(), - )); - let chain_id = web3.eth().chain_id().await.unwrap().as_u64(); - let settlement = GPv2Settlement::deployed(&web3).await.unwrap(); - - assert!(ZeroExSolver::new( - account(), - web3, - settlement, - chain_id, - Arc::new(DefaultZeroExApi::test()), - Default::default(), - SlippageCalculator::default(), - ) - .is_err()) - } - - #[tokio::test] - async fn test_sets_allowance_if_necessary() { - let mut client = MockZeroExApi::new(); - let mut allowance_fetcher = Box::new(MockAllowanceManaging::new()); - - let sell_token = H160::from_low_u64_be(1); - let buy_token = H160::from_low_u64_be(1); - - let allowance_target = shared::addr!("def1c0ded9bec7f1a1670819833240f027b25eff"); - client.expect_get_swap().returning(move |_, _| { - async move { - Ok(SwapResponse { - price: PriceResponse { - sell_amount: U256::from_dec_str("100").unwrap(), - buy_amount: U256::from_dec_str("91").unwrap(), - allowance_target, - price: 13.121_002_575_170_278_f64, - estimated_gas: Default::default(), - }, - to: shared::addr!("0000000000000000000000000000000000000000"), - data: hex::decode("").unwrap(), - value: U256::from_dec_str("0").unwrap(), - }) - } - .boxed() - }); - - // On first invocation no prior allowance, then max allowance set. - let mut seq = Sequence::new(); - allowance_fetcher - .expect_get_approval() - .times(1) - .with(eq(ApprovalRequest { - token: sell_token, - spender: allowance_target, - amount: U256::from(100), - })) - .returning(move |_| { - Ok(Some(Approval { - token: sell_token, - spender: allowance_target, - })) - }) - .in_sequence(&mut seq); - allowance_fetcher - .expect_get_approval() - .times(1) - .returning(|_| Ok(None)) - .in_sequence(&mut seq); - - let solver = ZeroExSolver { - account: account(), - api: Arc::new(client), - allowance_fetcher, - excluded_sources: Default::default(), - slippage_calculator: Default::default(), - settlement: testlib::protocol::SETTLEMENT, - enable_rfqt: false, - enable_slippage_protection: false, - }; - - let order = LimitOrder { - sell_token, - buy_token, - sell_amount: 100.into(), - buy_amount: 90.into(), - ..Default::default() - }; - - // On first run we have two main interactions (approve + swap) - let result = solver - .try_settle_order(order.clone(), &Default::default(), 1.) - .await - .unwrap() - .unwrap(); - assert_eq!(result.interactions.len(), 2); - - // On second run we have only have one main interactions (swap) - let result = solver - .try_settle_order(order, &Default::default(), 1.) - .await - .unwrap() - .unwrap(); - assert_eq!(result.interactions.len(), 1) - } -} diff --git a/crates/solver/src/test.rs b/crates/solver/src/test.rs deleted file mode 100644 index ee10f63771..0000000000 --- a/crates/solver/src/test.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Helper functions for unit tests. -use ethcontract::Account; - -/// Create a dummy account. -pub fn account() -> Account { - Account::Offline( - "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" - .parse() - .unwrap(), - None, - ) -} diff --git a/docker/Dockerfile.binary b/docker/Dockerfile.binary index a13971f302..6f0fbf41e8 100644 --- a/docker/Dockerfile.binary +++ b/docker/Dockerfile.binary @@ -18,7 +18,6 @@ COPY --from=cargo-build /src/target/release/autopilot /usr/local/bin/autopilot COPY --from=cargo-build /src/target/release/driver /usr/local/bin/driver COPY --from=cargo-build /src/target/release/orderbook /usr/local/bin/orderbook COPY --from=cargo-build /src/target/release/refunder /usr/local/bin/refunder -COPY --from=cargo-build /src/target/release/solver /usr/local/bin/solver COPY --from=cargo-build /src/target/release/solvers /usr/local/bin/solvers CMD echo "Specify binary..."