From ac6580a2870099afc150c8e1527f1977dd659c74 Mon Sep 17 00:00:00 2001 From: ilya Date: Mon, 25 Mar 2024 16:04:53 +0000 Subject: [PATCH] In/Out of market order protocol fee config (#2550) # Description Separate protocol fees config for in and out of market orders. ## How to test `protocol_fee.rs` tests work with in-market orders. Removing `in_market_fee_policy_kind` makes the tests fail. ## Related Issues Fixes #2548 --- crates/autopilot/src/arguments.rs | 107 +++++++++-------- crates/autopilot/src/domain/fee/mod.rs | 62 ++++++++-- crates/autopilot/src/domain/fee/policy.rs | 31 +---- crates/autopilot/src/run.rs | 5 +- crates/autopilot/src/solvable_orders.rs | 8 +- crates/e2e/tests/e2e/protocol_fee.rs | 136 +++++++++++++++++----- 6 files changed, 232 insertions(+), 117 deletions(-) diff --git a/crates/autopilot/src/arguments.rs b/crates/autopilot/src/arguments.rs index 448acef837..396d25c7ae 100644 --- a/crates/autopilot/src/arguments.rs +++ b/crates/autopilot/src/arguments.rs @@ -1,6 +1,7 @@ use { crate::{domain::fee::FeeFactor, infra}, anyhow::Context, + clap::ValueEnum, primitive_types::{H160, U256}, shared::{ arguments::{display_list, display_option, ExternalSolver}, @@ -201,9 +202,9 @@ pub struct Arguments { )] pub solve_deadline: Duration, - /// Describes how the protocol fee should be calculated. - #[clap(flatten)] - pub fee_policy: FeePolicy, + /// Describes how the protocol fees should be calculated. + #[clap(long, env, use_value_delimiter = true)] + pub fee_policies: Vec, /// Arguments for uploading information to S3. #[clap(flatten)] @@ -251,7 +252,7 @@ impl std::fmt::Display for Arguments { score_cap, shadow, solve_deadline, - fee_policy, + fee_policies, order_events_cleanup_interval, order_events_cleanup_threshold, db_url, @@ -312,7 +313,7 @@ impl std::fmt::Display for Arguments { writeln!(f, "score_cap: {}", score_cap)?; display_option(f, "shadow", shadow)?; writeln!(f, "solve_deadline: {:?}", solve_deadline)?; - writeln!(f, "fee_policy: {:?}", fee_policy)?; + writeln!(f, "fee_policies: {:?}", fee_policies)?; writeln!( f, "order_events_cleanup_interval: {:?}", @@ -340,32 +341,26 @@ impl std::fmt::Display for Arguments { } } -#[derive(clap::Parser, Debug, Clone)] +/// A fee policy to be used for orders base on it's class. +/// Examples: +/// - Surplus with a high enough cap for limit orders +/// surplus:0.5:0.9:limit +/// +/// - Surplus with cap for market orders: +/// surplus:0.5:0.06:market +/// +/// - Price improvement with a high enough cap for any order class +/// price_improvement:0.5:0.9:any +/// +/// - Price improvement with cap for limit orders: +/// price_improvement:0.5:0.06:limit +/// +/// - Volume based fee for any order class: +/// volume:0.1:any +#[derive(Debug, Clone)] pub struct FeePolicy { - /// Type of fee policy to use. Examples: - /// - /// - Surplus without cap - /// surplus:0.5:1.0 - /// - /// - Surplus with cap: - /// surplus:0.5:0.06 - /// - /// - Price improvement without cap: - /// price_improvement:0.5:1.0 - /// - /// - Price improvement with cap: - /// price_improvement:0.5:0.06 - /// - /// - Volume based: - /// volume:0.1 - #[clap(long, env, default_value = "surplus:0.0:0.9")] pub fee_policy_kind: FeePolicyKind, - - /// Should protocol fees be collected or skipped for orders whose - /// limit price at order creation time suggests they can be immediately - /// filled. - #[clap(long, env, action = clap::ArgAction::Set, default_value = "true")] - pub fee_policy_skip_market_orders: bool, + pub fee_policy_order_class: FeePolicyOrderClass, } #[derive(clap::Parser, Debug, Clone)] @@ -386,13 +381,23 @@ pub enum FeePolicyKind { Volume { factor: FeeFactor }, } -impl FromStr for FeePolicyKind { +#[derive(clap::Parser, clap::ValueEnum, Clone, Debug)] +pub enum FeePolicyOrderClass { + /// If a fee policy needs to be applied to in-market orders. + Market, + /// If a fee policy needs to be applied to limit orders. + Limit, + /// If a fee policy needs to be applied regardless of the order class. + Any, +} + +impl FromStr for FeePolicy { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let mut parts = s.split(':'); let kind = parts.next().context("missing fee policy kind")?; - match kind { + let fee_policy_kind = match kind { "surplus" => { let factor = parts .next() @@ -404,7 +409,7 @@ impl FromStr for FeePolicyKind { .context("missing max volume factor")? .parse::() .map_err(|e| anyhow::anyhow!("invalid max volume factor: {}", e))?; - Ok(Self::Surplus { + Ok(FeePolicyKind::Surplus { factor: factor.try_into()?, max_volume_factor: max_volume_factor.try_into()?, }) @@ -422,7 +427,7 @@ impl FromStr for FeePolicyKind { .map_err(|e| { anyhow::anyhow!("invalid price improvement max volume factor: {}", e) })?; - Ok(Self::PriceImprovement { + Ok(FeePolicyKind::PriceImprovement { factor: factor.try_into()?, max_volume_factor: max_volume_factor.try_into()?, }) @@ -433,12 +438,22 @@ impl FromStr for FeePolicyKind { .context("missing volume factor")? .parse::() .map_err(|e| anyhow::anyhow!("invalid volume factor: {}", e))?; - Ok(Self::Volume { + Ok(FeePolicyKind::Volume { factor: factor.try_into()?, }) } _ => Err(anyhow::anyhow!("invalid fee policy kind: {}", kind)), - } + }?; + let fee_policy_order_class = FeePolicyOrderClass::from_str( + parts.next().context("missing fee policy order class")?, + true, + ) + .map_err(|e| anyhow::anyhow!("invalid fee policy order class: {}", e))?; + + Ok(FeePolicy { + fee_policy_kind, + fee_policy_order_class, + }) } } @@ -449,20 +464,20 @@ mod test { #[test] fn test_fee_factor_limits() { let policies = vec![ - "volume:1.0", - "volume:-1.0", - "surplus:1.0:0.5", - "surplus:0.5:1.0", - "surplus:0.5:-1.0", - "surplus:-1.0:0.5", - "priceImprovement:1.0:0.5", - "priceImprovement:-1.0:0.5", - "priceImprovement:0.5:1.0", - "priceImprovement:0.5:-1.0", + "volume:1.0:market", + "volume:-1.0:limit", + "surplus:1.0:0.5:any", + "surplus:0.5:1.0:limit", + "surplus:0.5:-1.0:market", + "surplus:-1.0:0.5:limit", + "priceImprovement:1.0:0.5:market", + "priceImprovement:-1.0:0.5:any", + "priceImprovement:0.5:1.0:market", + "priceImprovement:0.5:-1.0:limit", ]; for policy in policies { - assert!(FeePolicyKind::from_str(policy) + assert!(FeePolicy::from_str(policy) .err() .unwrap() .to_string() diff --git a/crates/autopilot/src/domain/fee/mod.rs b/crates/autopilot/src/domain/fee/mod.rs index 679f878a9e..ab586f5c15 100644 --- a/crates/autopilot/src/domain/fee/mod.rs +++ b/crates/autopilot/src/domain/fee/mod.rs @@ -20,21 +20,43 @@ use { std::str::FromStr, }; +enum OrderClass { + Market, + Limit, + Any, +} + +impl From for OrderClass { + fn from(value: arguments::FeePolicyOrderClass) -> Self { + match value { + arguments::FeePolicyOrderClass::Market => Self::Market, + arguments::FeePolicyOrderClass::Limit => Self::Limit, + arguments::FeePolicyOrderClass::Any => Self::Any, + } + } +} + /// Constructs fee policies based on the current configuration. pub struct ProtocolFee { policy: policy::Policy, + order_class: OrderClass, } impl ProtocolFee { pub fn new(fee_policy_args: arguments::FeePolicy) -> Self { Self { - policy: fee_policy_args.into(), + policy: fee_policy_args.fee_policy_kind.into(), + order_class: fee_policy_args.fee_policy_order_class.into(), } } /// Converts an order from the boundary layer to the domain layer, applying /// protocol fees if necessary. - pub fn apply(&self, order: boundary::Order, quote: &domain::Quote) -> domain::Order { + pub fn apply( + protocol_fees: &[ProtocolFee], + order: boundary::Order, + quote: &domain::Quote, + ) -> domain::Order { // If the partner fee is specified, it overwrites the current volume fee policy if let Some(validated_app_data) = order .metadata @@ -55,13 +77,35 @@ impl ProtocolFee { } } - let protocol_fees = match &self.policy { - policy::Policy::Surplus(variant) => variant.apply(&order, quote), - policy::Policy::PriceImprovement(variant) => variant.apply(&order, quote), - policy::Policy::Volume(variant) => variant.apply(&order), - } - .into_iter() - .collect_vec(); + let order_ = boundary::Amounts { + sell: order.data.sell_amount, + buy: order.data.buy_amount, + fee: order.data.fee_amount, + }; + let quote_ = boundary::Amounts { + sell: quote.sell_amount, + buy: quote.buy_amount, + fee: quote.fee, + }; + let protocol_fees = protocol_fees + .iter() + // TODO: support multiple fee policies + .find_map(|fee_policy| { + let outside_market_price = boundary::is_order_outside_market_price(&order_, "e_); + match (outside_market_price, &fee_policy.order_class) { + (_, OrderClass::Any) => Some(&fee_policy.policy), + (true, OrderClass::Limit) => Some(&fee_policy.policy), + (false, OrderClass::Market) => Some(&fee_policy.policy), + _ => None, + } + }) + .and_then(|policy| match policy { + policy::Policy::Surplus(variant) => variant.apply(&order), + policy::Policy::PriceImprovement(variant) => variant.apply(&order, quote), + policy::Policy::Volume(variant) => variant.apply(&order), + }) + .into_iter() + .collect_vec(); boundary::order::to_domain(order, protocol_fees) } } diff --git a/crates/autopilot/src/domain/fee/policy.rs b/crates/autopilot/src/domain/fee/policy.rs index 5c79861630..7649e1dbdc 100644 --- a/crates/autopilot/src/domain/fee/policy.rs +++ b/crates/autopilot/src/domain/fee/policy.rs @@ -13,7 +13,6 @@ pub enum Policy { pub struct Surplus { factor: FeeFactor, max_volume_factor: FeeFactor, - skip_market_orders: bool, } pub struct PriceImprovement { @@ -25,16 +24,15 @@ pub struct Volume { factor: FeeFactor, } -impl From for Policy { - fn from(policy_arg: arguments::FeePolicy) -> Self { - match policy_arg.fee_policy_kind { +impl From for Policy { + fn from(policy_arg: arguments::FeePolicyKind) -> Self { + match policy_arg { arguments::FeePolicyKind::Surplus { factor, max_volume_factor, } => Policy::Surplus(Surplus { factor, max_volume_factor, - skip_market_orders: policy_arg.fee_policy_skip_market_orders, }), arguments::FeePolicyKind::PriceImprovement { factor, @@ -49,11 +47,7 @@ impl From for Policy { } impl Surplus { - pub fn apply( - &self, - order: &boundary::Order, - quote: &domain::Quote, - ) -> Option { + pub fn apply(&self, order: &boundary::Order) -> Option { match order.metadata.class { boundary::OrderClass::Market => None, boundary::OrderClass::Liquidity => None, @@ -62,22 +56,7 @@ impl Surplus { factor: self.factor, max_volume_factor: self.max_volume_factor, }; - if !self.skip_market_orders { - Some(policy) - } else { - let order_ = boundary::Amounts { - sell: order.data.sell_amount, - buy: order.data.buy_amount, - fee: order.data.fee_amount, - }; - let quote_ = boundary::Amounts { - sell: quote.sell_amount, - buy: quote.buy_amount, - fee: quote.fee, - }; - - boundary::is_order_outside_market_price(&order_, "e_).then_some(policy) - } + Some(policy) } } } diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs index c042429be9..5833064f9b 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -560,7 +560,10 @@ pub async fn run(args: Arguments) { args.limit_order_price_factor .try_into() .expect("limit order price factor can't be converted to BigDecimal"), - domain::ProtocolFee::new(args.fee_policy.clone()), + args.fee_policies + .into_iter() + .map(domain::ProtocolFee::new) + .collect(), ); solvable_orders_cache .update(block) diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index cefe424946..b976bea046 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -80,7 +80,7 @@ pub struct SolvableOrdersCache { metrics: &'static Metrics, weth: H160, limit_order_price_factor: BigDecimal, - protocol_fee: domain::ProtocolFee, + protocol_fees: Vec, } type Balances = HashMap; @@ -104,7 +104,7 @@ impl SolvableOrdersCache { update_interval: Duration, weth: H160, limit_order_price_factor: BigDecimal, - protocol_fee: domain::ProtocolFee, + protocol_fees: Vec, ) -> Arc { let self_ = Arc::new(Self { min_order_validity_period, @@ -121,7 +121,7 @@ impl SolvableOrdersCache { metrics: Metrics::instance(observe::metrics::get_storage_registry()).unwrap(), weth, limit_order_price_factor, - protocol_fee, + protocol_fees, }); tokio::task::spawn( update_task(Arc::downgrade(&self_), update_interval, current_block) @@ -242,7 +242,7 @@ impl SolvableOrdersCache { .into_iter() .filter_map(|order| { if let Some(quote) = db_solvable_orders.quotes.get(&order.metadata.uid.into()) { - Some(self.protocol_fee.apply(order, quote)) + Some(domain::ProtocolFee::apply(&self.protocol_fees, order, quote)) } else { tracing::warn!(order_uid = %order.metadata.uid, "order is skipped, quote is missing"); None diff --git a/crates/e2e/tests/e2e/protocol_fee.rs b/crates/e2e/tests/e2e/protocol_fee.rs index adb9c692b0..cb923ecf4c 100644 --- a/crates/e2e/tests/e2e/protocol_fee.rs +++ b/crates/e2e/tests/e2e/protocol_fee.rs @@ -67,6 +67,10 @@ async fn surplus_fee_sell_order_test(web3: Web3) { factor: 0.3, max_volume_factor: 0.9, }; + let protocol_fee = ProtocolFee { + policy: fee_policy, + policy_order_class: FeePolicyOrderClass::Market, + }; // Without protocol fee: // Expected execution is 10000000000000000000 GNO for // 9871415430342266811 DAI, with executed_surplus_fee = 167058994203399 GNO @@ -87,7 +91,7 @@ async fn surplus_fee_sell_order_test(web3: Web3) { // 1480603400674076736) = 1461589542731026166 DAI execute_test( web3.clone(), - fee_policy, + vec![protocol_fee], OrderKind::Sell, None, 1480603400674076736u128.into(), @@ -101,6 +105,18 @@ async fn surplus_fee_sell_order_capped_test(web3: Web3) { factor: 0.9, max_volume_factor: 0.1, }; + let protocol_fees = vec![ + ProtocolFee { + policy: fee_policy.clone(), + policy_order_class: FeePolicyOrderClass::Market, + }, + // Tests ability of providing multiple fees in the config. In fact, the Market one is used + // only since the order is in-market. + ProtocolFee { + policy: fee_policy, + policy_order_class: FeePolicyOrderClass::Limit, + }, + ]; // Without protocol fee: // Expected execution is 10000000000000000000 GNO for // 9871415430342266811 DAI, with executed_surplus_fee = 167058994203399 GNO @@ -117,7 +133,7 @@ async fn surplus_fee_sell_order_capped_test(web3: Web3) { // 1000150353094783059) = 987306456662572858 DAI execute_test( web3.clone(), - fee_policy, + protocol_fees, OrderKind::Sell, None, 1000150353094783059u128.into(), @@ -128,6 +144,12 @@ async fn surplus_fee_sell_order_capped_test(web3: Web3) { async fn volume_fee_sell_order_test(web3: Web3) { let fee_policy = FeePolicyKind::Volume { factor: 0.1 }; + // Tests ability of providing `any` fee policy order class config. In fact, the + // Market config could be used since the order is in-market. + let protocol_fee = ProtocolFee { + policy: fee_policy, + policy_order_class: FeePolicyOrderClass::Any, + }; // Without protocol fee: // Expected execution is 10000000000000000000 GNO for // 9871415430342266811 DAI, with executed_surplus_fee = 167058994203399 GNO @@ -144,7 +166,7 @@ async fn volume_fee_sell_order_test(web3: Web3) { // 1000150353094783059) = 987306456662572858 DAI execute_test( web3.clone(), - fee_policy, + vec![protocol_fee], OrderKind::Sell, None, 1000150353094783059u128.into(), @@ -159,6 +181,10 @@ async fn partner_fee_sell_order_test(web3: Web3) { factor: 0.5, max_volume_factor: 0.9, }; + let protocol_fee = ProtocolFee { + policy: fee_policy, + policy_order_class: FeePolicyOrderClass::Market, + }; // Without protocol fee: // Expected execution is 10000000000000000000 GNO for // 9871415430342266811 DAI, with executed_surplus_fee = 167058994203399 GNO @@ -175,7 +201,7 @@ async fn partner_fee_sell_order_test(web3: Web3) { // 100165388404261365) = 98879067931768848 DAI execute_test( web3.clone(), - fee_policy, + vec![protocol_fee], OrderKind::Sell, Some(OrderCreationAppData::Full { full: json!({ @@ -200,6 +226,10 @@ async fn surplus_fee_buy_order_test(web3: Web3) { factor: 0.3, max_volume_factor: 0.9, }; + let protocol_fee = ProtocolFee { + policy: fee_policy, + policy_order_class: FeePolicyOrderClass::Market, + }; // Without protocol fee: // Expected execution is 5040413426236634210 GNO for 5000000000000000000 DAI, // with executed_surplus_fee = 167058994203399 GNO @@ -216,7 +246,7 @@ async fn surplus_fee_buy_order_test(web3: Web3) { // Settlement contract balance after execution = executed_surplus_fee GNO execute_test( web3.clone(), - fee_policy, + vec![protocol_fee], OrderKind::Buy, None, 1488043031123213136u128.into(), @@ -230,6 +260,10 @@ async fn surplus_fee_buy_order_capped_test(web3: Web3) { factor: 0.9, max_volume_factor: 0.1, }; + let protocol_fee = ProtocolFee { + policy: fee_policy, + policy_order_class: FeePolicyOrderClass::Market, + }; // Without protocol fee: // Expected execution is 5040413426236634210 GNO for 5000000000000000000 DAI, // with executed_surplus_fee = 167058994203399 GNO @@ -241,7 +275,7 @@ async fn surplus_fee_buy_order_capped_test(web3: Web3) { // Settlement contract balance after execution = executed_surplus_fee GNO execute_test( web3.clone(), - fee_policy, + vec![protocol_fee], OrderKind::Buy, None, 504208401617866820u128.into(), @@ -252,6 +286,10 @@ async fn surplus_fee_buy_order_capped_test(web3: Web3) { async fn volume_fee_buy_order_test(web3: Web3) { let fee_policy = FeePolicyKind::Volume { factor: 0.1 }; + let protocol_fee = ProtocolFee { + policy: fee_policy, + policy_order_class: FeePolicyOrderClass::Market, + }; // Without protocol fee: // Expected execution is 5040413426236634210 GNO for 5000000000000000000 DAI, // with executed_surplus_fee = 167058994203399 GNO @@ -263,7 +301,7 @@ async fn volume_fee_buy_order_test(web3: Web3) { // Settlement contract balance after execution = executed_surplus_fee GNO execute_test( web3.clone(), - fee_policy, + vec![protocol_fee], OrderKind::Buy, None, 504208401617866820u128.into(), @@ -277,6 +315,10 @@ async fn price_improvement_fee_sell_order_test(web3: Web3) { factor: 0.3, max_volume_factor: 0.9, }; + let protocol_fee = ProtocolFee { + policy: fee_policy, + policy_order_class: FeePolicyOrderClass::Market, + }; // Without protocol fee: // Expected execution is 10000000000000000000 GNO for // 9871415430342266811 DAI, with executed_surplus_fee = 167058994203399 GNO @@ -301,7 +343,7 @@ async fn price_improvement_fee_sell_order_test(web3: Web3) { // 205312824093583) = 202676203868731 DAI execute_test( web3.clone(), - fee_policy, + vec![protocol_fee], OrderKind::Sell, None, 205312824093583u128.into(), @@ -320,7 +362,7 @@ fn is_approximately_equal(executed_value: U256, expected_value: U256) -> bool { async fn execute_test( web3: Web3, - fee_policy: FeePolicyKind, + protocol_fees: Vec, order_kind: OrderKind, app_data: Option, expected_surplus_fee: U256, @@ -393,15 +435,12 @@ async fn execute_test( endpoint: solver_endpoint, }], ); - services.start_autopilot( - None, - vec![ - "--drivers=test_solver|http://localhost:11088/test_solver".to_string(), - "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(), - "--fee-policy-skip-market-orders=false".to_string(), - fee_policy.to_string(), - ], - ); + let autopilot_args = vec![ + "--drivers=test_solver|http://localhost:11088/test_solver".to_string(), + "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(), + ProtocolFeesConfig(protocol_fees).to_string(), + ]; + services.start_autopilot(None, autopilot_args); services .start_api(vec![ "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(), @@ -461,6 +500,30 @@ async fn execute_test( )); } +struct ProtocolFeesConfig(Vec); + +struct ProtocolFee { + policy: FeePolicyKind, + policy_order_class: FeePolicyOrderClass, +} + +enum FeePolicyOrderClass { + Market, + Limit, + Any, +} + +impl std::fmt::Display for FeePolicyOrderClass { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FeePolicyOrderClass::Market => write!(f, "market"), + FeePolicyOrderClass::Limit => write!(f, "limit"), + FeePolicyOrderClass::Any => write!(f, "any"), + } + } +} + +#[derive(Clone)] enum FeePolicyKind { /// How much of the order's surplus should be taken as a protocol fee. Surplus { factor: f64, max_volume_factor: f64 }, @@ -472,30 +535,41 @@ enum FeePolicyKind { PriceImprovement { factor: f64, max_volume_factor: f64 }, } -impl std::fmt::Display for FeePolicyKind { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { +impl std::fmt::Display for ProtocolFee { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let order_class_str = &self.policy_order_class.to_string(); + match &self.policy { FeePolicyKind::Surplus { factor, max_volume_factor, } => write!( f, - "--fee-policy-kind=surplus:{}:{}", - factor, max_volume_factor + "surplus:{}:{}:{}", + factor, max_volume_factor, order_class_str ), FeePolicyKind::Volume { factor } => { - write!(f, "--fee-policy-kind=volume:{}", factor) + write!(f, "volume:{}:{}", factor, order_class_str) } FeePolicyKind::PriceImprovement { factor, max_volume_factor, - } => { - write!( - f, - "--fee-policy-kind=priceImprovement:{}:{}", - factor, max_volume_factor - ) - } + } => write!( + f, + "priceImprovement:{}:{}:{}", + factor, max_volume_factor, order_class_str + ), } } } + +impl std::fmt::Display for ProtocolFeesConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let fees_str = self + .0 + .iter() + .map(|fee| fee.to_string()) + .collect::>() + .join(","); + write!(f, "--fee-policies={}", fees_str) + } +}