diff --git a/crates/autopilot/src/arguments.rs b/crates/autopilot/src/arguments.rs index 8fc3d9c5ce..bcf240f498 100644 --- a/crates/autopilot/src/arguments.rs +++ b/crates/autopilot/src/arguments.rs @@ -207,6 +207,11 @@ pub struct Arguments { #[clap(long, env, default_value = "0.01")] pub fee_policy_max_partner_fee: FeeFactor, + /// List of addresses which are exempt from the protocol + /// fees + #[clap(long, env, use_value_delimiter = true)] + pub protocol_fee_exempt_addresses: Vec, + /// Arguments for uploading information to S3. #[clap(flatten)] pub s3: infra::persistence::cli::S3, @@ -262,6 +267,7 @@ impl std::fmt::Display for Arguments { auction_update_interval, max_settlement_transaction_wait, s3, + protocol_fee_exempt_addresses, } = self; write!(f, "{}", shared)?; @@ -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, + "protocol_fee_exempt_addresses: {:?}", + protocol_fee_exempt_addresses + )?; writeln!( f, "fee_policy_max_partner_fee: {:?}", diff --git a/crates/autopilot/src/domain/fee/mod.rs b/crates/autopilot/src/domain/fee/mod.rs index 38ab745eb4..17acdc9495 100644 --- a/crates/autopilot/src/domain/fee/mod.rs +++ b/crates/autopilot/src/domain/fee/mod.rs @@ -8,16 +8,16 @@ mod policy; use { crate::{ - arguments, + arguments::{self}, boundary::{self}, domain::{self}, }, app_data::Validator, derive_more::Into, itertools::Itertools, - primitive_types::U256, + primitive_types::{H160, U256}, prometheus::core::Number, - std::str::FromStr, + std::{collections::HashSet, str::FromStr}, }; enum OrderClass { @@ -51,15 +51,21 @@ impl From for ProtocolFee { } } +pub type ProtocolFeeExemptAddresses = HashSet; + pub struct ProtocolFees { fee_policies: Vec, max_partner_fee: FeeFactor, + /// List of addresses which are exempt from the protocol + /// fees + protocol_fee_exempt_addresses: ProtocolFeeExemptAddresses, } impl ProtocolFees { pub fn new( fee_policies: &[arguments::FeePolicy], fee_policy_max_partner_fee: FeeFactor, + protocol_fee_exempt_addresses: &[H160], ) -> Self { Self { fee_policies: fee_policies @@ -67,17 +73,17 @@ impl ProtocolFees { .cloned() .map(ProtocolFee::from) .collect(), + protocol_fee_exempt_addresses: protocol_fee_exempt_addresses + .iter() + .cloned() + .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: &ProtocolFees, - order: boundary::Order, - quote: &domain::Quote, - ) -> domain::Order { + pub fn apply(&self, 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 @@ -92,7 +98,7 @@ impl ProtocolFees { let fee_policy = vec![Policy::Volume { factor: FeeFactor::try_from_capped( partner_fee.bps.into_f64() / 10_000.0, - protocol_fees.max_partner_fee.into(), + self.max_partner_fee.into(), ) .unwrap(), }]; @@ -110,26 +116,33 @@ impl ProtocolFees { buy: quote.buy_amount, fee: quote.fee, }; - let protocol_fees = protocol_fees - .fee_policies - .iter() - // TODO: support multiple fee policies - .find_map(|fee_policy| { - let outside_market_price = boundary::is_order_outside_market_price(&order_, "e_, order.data.kind); - 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(); + let protocol_fees = if self + .protocol_fee_exempt_addresses + .contains(&order.metadata.owner) + { + vec![] + } else { + self + .fee_policies + .iter() + // TODO: support multiple fee policies + .find_map(|fee_policy| { + let outside_market_price = boundary::is_order_outside_market_price(&order_, "e_, order.data.kind); + 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/run.rs b/crates/autopilot/src/run.rs index f45fb0925e..d096f303bc 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -558,7 +558,11 @@ 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::ProtocolFees::new(&args.fee_policies, args.fee_policy_max_partner_fee), + domain::ProtocolFees::new( + &args.fee_policies, + args.fee_policy_max_partner_fee, + args.protocol_fee_exempt_addresses.as_slice(), + ), ); let liveness = Arc::new(Liveness::new(args.max_auction_age)); diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index 23b53bd76a..0777c156e0 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -1,6 +1,6 @@ use { crate::{ - domain, + domain::{self}, infra::{self, banned}, }, anyhow::Result, @@ -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::ProtocolFees::apply(&self.protocol_fees, order, quote)) + Some(self.protocol_fees.apply(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 1f8ee3b699..d59d954add 100644 --- a/crates/e2e/tests/e2e/protocol_fee.rs +++ b/crates/e2e/tests/e2e/protocol_fee.rs @@ -64,15 +64,17 @@ async fn combined_protocol_fees(web3: Web3) { let mut onchain = OnchainComponents::deploy(web3.clone()).await; let [solver] = onchain.make_solvers(to_wei(200)).await; - let [trader] = onchain.make_accounts(to_wei(200)).await; - let [limit_order_token, market_order_token, partner_fee_order_token] = onchain - .deploy_tokens_with_weth_uni_v2_pools(to_wei(20), to_wei(20)) - .await; + let [trader, trader_exempt] = onchain.make_accounts(to_wei(200)).await; + let [limit_order_token, market_order_token, partner_fee_order_token, fee_exempt_token] = + onchain + .deploy_tokens_with_weth_uni_v2_pools(to_wei(20), to_wei(20)) + .await; for token in &[ &limit_order_token, &market_order_token, &partner_fee_order_token, + &fee_exempt_token, ] { token.mint(solver.address(), to_wei(1000)).await; tx!( @@ -82,24 +84,28 @@ async fn combined_protocol_fees(web3: Web3) { to_wei(1000) ) ); + for trader in &[&trader, &trader_exempt] { + tx!( + trader.account(), + token.approve(onchain.contracts().uniswap_v2_router.address(), to_wei(100)) + ); + } + } + + for trader in &[&trader, &trader_exempt] { tx!( trader.account(), - token.approve(onchain.contracts().uniswap_v2_router.address(), to_wei(100)) + onchain + .contracts() + .weth + .approve(onchain.contracts().allowance, to_wei(100)) + ); + tx_value!( + trader.account(), + to_wei(100), + onchain.contracts().weth.deposit() ); } - - tx!( - trader.account(), - onchain - .contracts() - .weth - .approve(onchain.contracts().allowance, to_wei(100)) - ); - tx_value!( - trader.account(), - to_wei(100), - onchain.contracts().weth.deposit() - ); tx!( solver.account(), onchain @@ -111,6 +117,10 @@ async fn combined_protocol_fees(web3: Web3) { let autopilot_config = vec![ ProtocolFeesConfig(vec![limit_surplus_policy, market_price_improvement_policy]).to_string(), "--fee-policy-max-partner-fee=0.02".to_string(), + format!( + "--protocol-fee-exempt-addresses={:?}", + trader_exempt.address() + ), ]; let services = Services::new(onchain.contracts()).await; services @@ -126,12 +136,13 @@ async fn combined_protocol_fees(web3: Web3) { tracing::info!("Acquiring quotes."); let quote_valid_to = model::time::now_in_epoch_seconds() + 300; let sell_amount = to_wei(10); - let [limit_quote_before, market_quote_before, partner_fee_quote] = + let [limit_quote_before, market_quote_before, partner_fee_quote, fee_exempt_quote] = futures::future::try_join_all( [ &limit_order_token, &market_order_token, &partner_fee_order_token, + &fee_exempt_token, ] .map(|token| { get_quote( @@ -147,7 +158,7 @@ async fn combined_protocol_fees(web3: Web3) { .await .unwrap() .try_into() - .expect("Expected exactly three elements"); + .expect("Expected exactly four elements"); let market_price_improvement_order = OrderCreation { sell_amount, @@ -182,6 +193,16 @@ async fn combined_protocol_fees(web3: Web3) { &onchain.contracts().domain_separator, SecretKeyRef::from(&SecretKey::from_slice(trader.private_key()).unwrap()), ); + let fee_exempt_token_order = OrderCreation { + sell_amount, + buy_amount: to_wei(5), + ..sell_order_from_quote(&fee_exempt_quote) + } + .sign( + EcdsaSigningScheme::Eip712, + &onchain.contracts().domain_separator, + SecretKeyRef::from(&SecretKey::from_slice(trader_exempt.private_key()).unwrap()), + ); tracing::info!("Rebalancing AMM pools for market & limit order."); onchain @@ -226,19 +247,20 @@ async fn combined_protocol_fees(web3: Web3) { .try_into() .expect("Expected exactly two elements"); - let [market_price_improvement_uid, limit_surplus_order_uid, partner_fee_order_uid] = + let [market_price_improvement_uid, limit_surplus_order_uid, partner_fee_order_uid, fee_exempt_token_order_uid] = futures::future::try_join_all( [ &market_price_improvement_order, &limit_surplus_order, &partner_fee_order, + &fee_exempt_token_order, ] .map(|order| services.create_order(order)), ) .await .unwrap() .try_into() - .expect("Expected exactly three elements"); + .expect("Expected exactly four elements"); tracing::info!("Waiting for orders to trade."); let metadata_updated = || async { @@ -248,6 +270,7 @@ async fn combined_protocol_fees(web3: Web3) { &market_price_improvement_uid, &limit_surplus_order_uid, &partner_fee_order_uid, + &fee_exempt_token_order_uid, ] .map(|uid| async { services @@ -295,6 +318,13 @@ async fn combined_protocol_fees(web3: Web3) { // see `limit_surplus_policy.factor`, which is 0.3 assert!(limit_executed_surplus_fee_in_buy_token >= limit_quote_diff * 3 / 10); + let fee_exempt_order = services + .get_order(&fee_exempt_token_order_uid) + .await + .unwrap(); + let fee_exempt_surplus_fee_in_buy_token = + surplus_fee_in_buy_token(&fee_exempt_order, &fee_exempt_quote.quote); + let balance_after = market_order_token .balance_of(onchain.contracts().gp_settlement.address()) .call() @@ -315,6 +345,13 @@ async fn combined_protocol_fees(web3: Web3) { .await .unwrap(); assert_approximately_eq!(partner_fee_executed_surplus_fee_in_buy_token, balance_after); + + let balance_after = fee_exempt_token + .balance_of(onchain.contracts().gp_settlement.address()) + .call() + .await + .unwrap(); + assert_approximately_eq!(fee_exempt_surplus_fee_in_buy_token, balance_after); } async fn get_quote(