From 358d48361064992829069a342f5a65917401e029 Mon Sep 17 00:00:00 2001 From: Mateo-mro <160488334+Mateo-mro@users.noreply.github.com> Date: Tue, 26 Mar 2024 10:30:58 +0100 Subject: [PATCH] Make the partner fee max cap configurable in autopilot (#2559) # Description We should make configurable in the autopilot the maximum partner fee amount. # Changes - Introduce a new configuration parameter in the autopilot for the partner fee max cap with a default value 0.01 - Enforce the configured partner fee max cap ## How to test 1. e2e test 2. unit test ## Related Issues Fixes #2554 --- crates/autopilot/src/arguments.rs | 11 ++++ crates/autopilot/src/domain/fee/mod.rs | 46 ++++++++++---- crates/autopilot/src/domain/mod.rs | 2 +- crates/autopilot/src/run.rs | 5 +- crates/autopilot/src/solvable_orders.rs | 6 +- crates/e2e/tests/e2e/protocol_fee.rs | 79 +++++++++++++++++++++---- 6 files changed, 118 insertions(+), 31 deletions(-) diff --git a/crates/autopilot/src/arguments.rs b/crates/autopilot/src/arguments.rs index 396d25c7ae..2419fbecce 100644 --- a/crates/autopilot/src/arguments.rs +++ b/crates/autopilot/src/arguments.rs @@ -206,6 +206,11 @@ pub struct Arguments { #[clap(long, env, use_value_delimiter = true)] pub fee_policies: Vec, + /// Maximum partner fee allow. If the partner fee specified is greater than + /// this maximum, the partner fee will be capped + #[clap(long, env, default_value = "0.01")] + pub fee_policy_max_partner_fee: FeeFactor, + /// Arguments for uploading information to S3. #[clap(flatten)] pub s3: infra::persistence::cli::S3, @@ -253,6 +258,7 @@ impl std::fmt::Display for Arguments { shadow, solve_deadline, fee_policies, + fee_policy_max_partner_fee, order_events_cleanup_interval, order_events_cleanup_threshold, db_url, @@ -314,6 +320,11 @@ impl std::fmt::Display for Arguments { display_option(f, "shadow", shadow)?; writeln!(f, "solve_deadline: {:?}", solve_deadline)?; writeln!(f, "fee_policies: {:?}", fee_policies)?; + writeln!( + f, + "fee_policy_max_partner_fee: {:?}", + fee_policy_max_partner_fee + )?; writeln!( f, "order_events_cleanup_interval: {:?}", diff --git a/crates/autopilot/src/domain/fee/mod.rs b/crates/autopilot/src/domain/fee/mod.rs index ab586f5c15..2b214bb72e 100644 --- a/crates/autopilot/src/domain/fee/mod.rs +++ b/crates/autopilot/src/domain/fee/mod.rs @@ -10,7 +10,7 @@ use { crate::{ arguments, boundary::{self}, - domain, + domain::{self}, }, app_data::Validator, derive_more::Into, @@ -42,18 +42,39 @@ pub struct ProtocolFee { order_class: OrderClass, } -impl ProtocolFee { - pub fn new(fee_policy_args: arguments::FeePolicy) -> Self { +impl From for ProtocolFee { + fn from(value: arguments::FeePolicy) -> Self { Self { - policy: fee_policy_args.fee_policy_kind.into(), - order_class: fee_policy_args.fee_policy_order_class.into(), + policy: value.fee_policy_kind.into(), + order_class: value.fee_policy_order_class.into(), + } + } +} + +pub struct ProtocolFees { + fee_policies: Vec, + max_partner_fee: FeeFactor, +} + +impl ProtocolFees { + pub fn new( + fee_policies: &[arguments::FeePolicy], + fee_policy_max_partner_fee: FeeFactor, + ) -> Self { + Self { + fee_policies: fee_policies + .iter() + .cloned() + .map(ProtocolFee::from) + .collect(), + max_partner_fee: fee_policy_max_partner_fee, } } /// Converts an order from the boundary layer to the domain layer, applying /// protocol fees if necessary. pub fn apply( - protocol_fees: &[ProtocolFee], + protocol_fees: &ProtocolFees, order: boundary::Order, quote: &domain::Quote, ) -> domain::Order { @@ -69,9 +90,11 @@ impl ProtocolFee { { if let Some(partner_fee) = validated_app_data.protocol.partner_fee { let fee_policy = vec![Policy::Volume { - factor: FeeFactor::partner_fee_capped_from( + factor: FeeFactor::try_from_capped( partner_fee.bps.into_f64() / 10_000.0, - ), + protocol_fees.max_partner_fee.into(), + ) + .unwrap(), }]; return boundary::order::to_domain(order, fee_policy); } @@ -88,6 +111,7 @@ impl ProtocolFee { fee: quote.fee, }; let protocol_fees = protocol_fees + .fee_policies .iter() // TODO: support multiple fee policies .find_map(|fee_policy| { @@ -149,9 +173,9 @@ pub enum Policy { pub struct FeeFactor(f64); impl FeeFactor { - /// Convert a partner fee into a `Factor` capping its value - pub fn partner_fee_capped_from(value: f64) -> Self { - Self(value.max(0.0).min(0.01)) + /// Convert a fee into a `FeeFactor` capping its value + pub fn try_from_capped(value: f64, cap: f64) -> anyhow::Result { + value.max(0.0).min(cap).try_into() } } diff --git a/crates/autopilot/src/domain/mod.rs b/crates/autopilot/src/domain/mod.rs index f06eb9a6b0..32147c619e 100644 --- a/crates/autopilot/src/domain/mod.rs +++ b/crates/autopilot/src/domain/mod.rs @@ -10,6 +10,6 @@ pub use { Auction, AuctionWithId, }, - fee::ProtocolFee, + fee::ProtocolFees, quote::Quote, }; diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs index 5833064f9b..40df29626e 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -560,10 +560,7 @@ pub async fn run(args: Arguments) { args.limit_order_price_factor .try_into() .expect("limit order price factor can't be converted to BigDecimal"), - args.fee_policies - .into_iter() - .map(domain::ProtocolFee::new) - .collect(), + domain::ProtocolFees::new(&args.fee_policies, args.fee_policy_max_partner_fee), ); solvable_orders_cache .update(block) diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index b976bea046..083b29e7e7 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_fees: Vec, + protocol_fees: domain::ProtocolFees, } type Balances = HashMap; @@ -104,7 +104,7 @@ impl SolvableOrdersCache { update_interval: Duration, weth: H160, limit_order_price_factor: BigDecimal, - protocol_fees: Vec, + protocol_fees: domain::ProtocolFees, ) -> Arc { let self_ = Arc::new(Self { min_order_validity_period, @@ -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(domain::ProtocolFee::apply(&self.protocol_fees, order, quote)) + Some(domain::ProtocolFees::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 cb923ecf4c..d578510a38 100644 --- a/crates/e2e/tests/e2e/protocol_fee.rs +++ b/crates/e2e/tests/e2e/protocol_fee.rs @@ -38,6 +38,12 @@ async fn local_node_partner_fee_sell_order() { run_test(partner_fee_sell_order_test).await; } +#[tokio::test] +#[ignore] +async fn local_node_partner_fee_with_not_default_cap_sell_order_test() { + run_test(partner_fee_with_not_default_cap_sell_order_test).await; +} + #[tokio::test] #[ignore] async fn local_node_surplus_fee_buy_order() { @@ -91,7 +97,7 @@ async fn surplus_fee_sell_order_test(web3: Web3) { // 1480603400674076736) = 1461589542731026166 DAI execute_test( web3.clone(), - vec![protocol_fee], + vec![ProtocolFeesConfig(vec![protocol_fee])], OrderKind::Sell, None, 1480603400674076736u128.into(), @@ -133,7 +139,7 @@ async fn surplus_fee_sell_order_capped_test(web3: Web3) { // 1000150353094783059) = 987306456662572858 DAI execute_test( web3.clone(), - protocol_fees, + vec![ProtocolFeesConfig(protocol_fees)], OrderKind::Sell, None, 1000150353094783059u128.into(), @@ -166,7 +172,7 @@ async fn volume_fee_sell_order_test(web3: Web3) { // 1000150353094783059) = 987306456662572858 DAI execute_test( web3.clone(), - vec![protocol_fee], + vec![ProtocolFeesConfig(vec![protocol_fee])], OrderKind::Sell, None, 1000150353094783059u128.into(), @@ -201,7 +207,7 @@ async fn partner_fee_sell_order_test(web3: Web3) { // 100165388404261365) = 98879067931768848 DAI execute_test( web3.clone(), - vec![protocol_fee], + vec![ProtocolFeesConfig(vec![protocol_fee])], OrderKind::Sell, Some(OrderCreationAppData::Full { full: json!({ @@ -221,6 +227,55 @@ async fn partner_fee_sell_order_test(web3: Web3) { .await; } +async fn partner_fee_with_not_default_cap_sell_order_test(web3: Web3) { + // Fee policy to be overwritten by the partner fee + capped to 0.02 + let fee_policy = FeePolicyKind::PriceImprovement { + 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 + // + // With protocol fee: + // Expected executed_surplus_fee is 167058994203399 + + // 0.02*(10000000000000000000 - 167058994203399) = 200163717814319331 + // + // Final execution is 10000000000000000000 GNO for 9772701276038844388 DAI, with + // executed_surplus_fee = 200163717814319331 GNO + // + // Settlement contract balance after execution = 200163717814319331 GNO = + // 200163717814319331 GNO * 9673987121735421787 / (10000000000000000000 - + // 200163717814319331) = 197593222235191520 DAI + execute_test( + web3.clone(), + vec![ + ProtocolFeesConfig(vec![protocol_fee]).to_string(), + "--fee-policy-max-partner-fee=0.02".to_string(), + ], + OrderKind::Sell, + Some(OrderCreationAppData::Full { + full: json!({ + "version": "1.1.0", + "metadata": { + "partnerFee": { + "bps":1000, + "recipient": "0xb6BAd41ae76A11D10f7b0E664C5007b908bC77C9", + } + } + }) + .to_string(), + }), + 200163717814319331u128.into(), + 197593222235191520u128.into(), + ) + .await; +} + async fn surplus_fee_buy_order_test(web3: Web3) { let fee_policy = FeePolicyKind::Surplus { factor: 0.3, @@ -246,7 +301,7 @@ async fn surplus_fee_buy_order_test(web3: Web3) { // Settlement contract balance after execution = executed_surplus_fee GNO execute_test( web3.clone(), - vec![protocol_fee], + vec![ProtocolFeesConfig(vec![protocol_fee])], OrderKind::Buy, None, 1488043031123213136u128.into(), @@ -275,7 +330,7 @@ async fn surplus_fee_buy_order_capped_test(web3: Web3) { // Settlement contract balance after execution = executed_surplus_fee GNO execute_test( web3.clone(), - vec![protocol_fee], + vec![ProtocolFeesConfig(vec![protocol_fee])], OrderKind::Buy, None, 504208401617866820u128.into(), @@ -301,7 +356,7 @@ async fn volume_fee_buy_order_test(web3: Web3) { // Settlement contract balance after execution = executed_surplus_fee GNO execute_test( web3.clone(), - vec![protocol_fee], + vec![ProtocolFeesConfig(vec![protocol_fee])], OrderKind::Buy, None, 504208401617866820u128.into(), @@ -343,7 +398,7 @@ async fn price_improvement_fee_sell_order_test(web3: Web3) { // 205312824093583) = 202676203868731 DAI execute_test( web3.clone(), - vec![protocol_fee], + vec![ProtocolFeesConfig(vec![protocol_fee])], OrderKind::Sell, None, 205312824093583u128.into(), @@ -362,7 +417,7 @@ fn is_approximately_equal(executed_value: U256, expected_value: U256) -> bool { async fn execute_test( web3: Web3, - protocol_fees: Vec, + autopilot_config: Vec, order_kind: OrderKind, app_data: Option, expected_surplus_fee: U256, @@ -435,12 +490,12 @@ async fn execute_test( endpoint: solver_endpoint, }], ); - let autopilot_args = vec![ + let mut config = 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); + config.extend(autopilot_config.iter().map(ToString::to_string)); + services.start_autopilot(None, config); services .start_api(vec![ "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(),