From ae05328674036cdce5fa5669c9df351637ac6c66 Mon Sep 17 00:00:00 2001 From: Mateo-mro <160488334+Mateo-mro@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:34:35 +0200 Subject: [PATCH] Allow multiple fee policies (#2595) # Description Support for multiple fee policies # Changes - Allow multiple fee policies in autopilot and driver - Apply fee policies one by one, where each applied policy creates a new solution as a base to apply next fee policy ## How to test 1. e2e test 2. unit tests ## Related Issues Fixes #2495 --------- Co-authored-by: sunce86 --- crates/autopilot/src/arguments.rs | 6 + crates/autopilot/src/domain/fee/mod.rs | 142 +++++--- crates/autopilot/src/run.rs | 1 + crates/driver/src/domain/competition/mod.rs | 2 +- .../src/domain/competition/solution/fee.rs | 51 +-- .../src/domain/competition/solution/mod.rs | 27 +- .../domain/competition/solution/scoring.rs | 105 ++++-- .../src/domain/competition/solution/trade.rs | 50 ++- crates/driver/src/domain/eth/mod.rs | 53 ++- crates/driver/src/infra/solver/dto/auction.rs | 40 +-- crates/driver/src/tests/cases/mod.rs | 7 + .../driver/src/tests/cases/protocol_fees.rs | 305 ++++++++++++++++-- crates/driver/src/tests/setup/driver.rs | 10 +- crates/driver/src/tests/setup/mod.rs | 8 +- crates/driver/src/tests/setup/solver.rs | 76 +++-- crates/e2e/tests/e2e/protocol_fee.rs | 61 ++-- 16 files changed, 720 insertions(+), 224 deletions(-) diff --git a/crates/autopilot/src/arguments.rs b/crates/autopilot/src/arguments.rs index bcf240f498..f592c7a176 100644 --- a/crates/autopilot/src/arguments.rs +++ b/crates/autopilot/src/arguments.rs @@ -202,6 +202,10 @@ pub struct Arguments { #[clap(long, env, use_value_delimiter = true)] pub fee_policies: Vec, + /// Enables multiple fees + #[clap(long, env, action = clap::ArgAction::Set, default_value = "false")] + pub enable_multiple_fees: bool, + /// 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")] @@ -258,6 +262,7 @@ impl std::fmt::Display for Arguments { shadow, solve_deadline, fee_policies, + enable_multiple_fees, fee_policy_max_partner_fee, order_events_cleanup_interval, order_events_cleanup_threshold, @@ -325,6 +330,7 @@ impl std::fmt::Display for Arguments { "protocol_fee_exempt_addresses: {:?}", protocol_fee_exempt_addresses )?; + writeln!(f, "enable_multiple_fees: {:?}", enable_multiple_fees)?; 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 17acdc9495..f30bfdf63e 100644 --- a/crates/autopilot/src/domain/fee/mod.rs +++ b/crates/autopilot/src/domain/fee/mod.rs @@ -59,6 +59,7 @@ pub struct ProtocolFees { /// List of addresses which are exempt from the protocol /// fees protocol_fee_exempt_addresses: ProtocolFeeExemptAddresses, + enable_protocol_fees: bool, } impl ProtocolFees { @@ -66,6 +67,7 @@ impl ProtocolFees { fee_policies: &[arguments::FeePolicy], fee_policy_max_partner_fee: FeeFactor, protocol_fee_exempt_addresses: &[H160], + enable_protocol_fees: bool, ) -> Self { Self { fee_policies: fee_policies @@ -78,32 +80,39 @@ impl ProtocolFees { .cloned() .collect::>(), max_partner_fee: fee_policy_max_partner_fee, + enable_protocol_fees, } } /// 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 { - // If the partner fee is specified, it overwrites the current volume fee policy - if let Some(validated_app_data) = order + let partner_fee = order .metadata .full_app_data .as_ref() - .map(|full_app_data| Validator::new(usize::MAX).validate(full_app_data.as_bytes())) - .transpose() - .ok() - .flatten() + .and_then(|full_app_data| { + Validator::new(usize::MAX) + .validate(full_app_data.as_bytes()) + .ok()? + .protocol + .partner_fee + .map(|partner_fee| Policy::Volume { + factor: FeeFactor::try_from_capped( + partner_fee.bps.into_f64() / 10_000.0, + self.max_partner_fee.into(), + ) + .unwrap(), + }) + }) + .into_iter() + .collect::>(); + + if self + .protocol_fee_exempt_addresses + .contains(&order.metadata.owner) { - if let Some(partner_fee) = validated_app_data.protocol.partner_fee { - let fee_policy = vec![Policy::Volume { - factor: FeeFactor::try_from_capped( - partner_fee.bps.into_f64() / 10_000.0, - self.max_partner_fee.into(), - ) - .unwrap(), - }]; - return boundary::order::to_domain(order, fee_policy); - } + return boundary::order::to_domain(order, partner_fee); } let order_ = boundary::Amounts { @@ -116,35 +125,84 @@ impl ProtocolFees { buy: quote.buy_amount, fee: quote.fee, }; - let protocol_fees = if self - .protocol_fee_exempt_addresses - .contains(&order.metadata.owner) - { - vec![] + + if self.enable_protocol_fees { + self.apply_multiple_policies(order, quote, order_, quote_, partner_fee) } 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() - }; + self.apply_single_policy(order, quote, order_, quote_, partner_fee) + } + } + + fn apply_single_policy( + &self, + order: boundary::Order, + quote: &domain::Quote, + order_: boundary::Amounts, + quote_: boundary::Amounts, + partner_fees: Vec, + ) -> domain::Order { + if let Some(partner_fee) = partner_fees.first() { + return boundary::order::to_domain(order, vec![*partner_fee]); + } + let protocol_fees = self + .fee_policies + .iter() + .find_map(|fee_policy| { + Self::protocol_fee_into_policy(&order, &order_, "e_, fee_policy) + }) + .and_then(|policy| Self::variant_fee_apply(&order, quote, policy)) + .into_iter() + .collect_vec(); + boundary::order::to_domain(order, protocol_fees) + } + + fn apply_multiple_policies( + &self, + order: boundary::Order, + quote: &domain::Quote, + order_: boundary::Amounts, + quote_: boundary::Amounts, + partner_fees: Vec, + ) -> domain::Order { + let protocol_fees = self + .fee_policies + .iter() + .filter_map(|fee_policy| { + Self::protocol_fee_into_policy(&order, &order_, "e_, fee_policy) + }) + .flat_map(|policy| Self::variant_fee_apply(&order, quote, policy)) + .chain(partner_fees) + .collect::>(); boundary::order::to_domain(order, protocol_fees) } + + fn variant_fee_apply( + order: &boundary::Order, + quote: &domain::Quote, + policy: &policy::Policy, + ) -> Option { + match policy { + policy::Policy::Surplus(variant) => variant.apply(order), + policy::Policy::PriceImprovement(variant) => variant.apply(order, quote), + policy::Policy::Volume(variant) => variant.apply(order), + } + } + + fn protocol_fee_into_policy<'a>( + order: &boundary::Order, + order_: &boundary::Amounts, + quote_: &boundary::Amounts, + protocol_fee: &'a ProtocolFee, + ) -> Option<&'a policy::Policy> { + let outside_market_price = + boundary::is_order_outside_market_price(order_, quote_, order.data.kind); + match (outside_market_price, &protocol_fee.order_class) { + (_, OrderClass::Any) => Some(&protocol_fee.policy), + (true, OrderClass::Limit) => Some(&protocol_fee.policy), + (false, OrderClass::Market) => Some(&protocol_fee.policy), + _ => None, + } + } } #[derive(Debug, Copy, Clone, PartialEq)] diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs index d096f303bc..3c21ba65d5 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -562,6 +562,7 @@ pub async fn run(args: Arguments) { &args.fee_policies, args.fee_policy_max_partner_fee, args.protocol_fee_exempt_addresses.as_slice(), + args.enable_multiple_fees, ), ); diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index d43449b586..05112498b0 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -378,7 +378,7 @@ pub struct Amounts { pub buy: eth::TokenAmount, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct PriceLimits { pub sell: eth::TokenAmount, pub buy: eth::TokenAmount, diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 34d35707dc..09721ce69d 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -33,15 +33,29 @@ use { order::{FeePolicy, Side}, PriceLimits, }, - eth, + eth::{self}, }, bigdecimal::Zero, }; impl Fulfillment { + /// Applies the protocol fees to the existing fulfillment creating a new + /// one. + pub fn with_protocol_fees(&self, prices: ClearingPrices) -> Result { + let mut current_fulfillment = self.clone(); + for protocol_fee in &self.order().protocol_fees { + current_fulfillment = current_fulfillment.with_protocol_fee(prices, protocol_fee)?; + } + Ok(current_fulfillment) + } + /// Applies the protocol fee to the existing fulfillment creating a new one. - pub fn with_protocol_fee(&self, prices: ClearingPrices) -> Result { - let protocol_fee = self.protocol_fee_in_sell_token(prices)?; + fn with_protocol_fee( + &self, + prices: ClearingPrices, + protocol_fee: &FeePolicy, + ) -> Result { + let protocol_fee = self.protocol_fee_in_sell_token(prices, protocol_fee)?; // Increase the fee by the protocol fee let fee = match self.surplus_fee() { @@ -74,17 +88,16 @@ impl Fulfillment { } /// Computed protocol fee in surplus token. - fn protocol_fee(&self, prices: ClearingPrices) -> Result { - // TODO: support multiple fee policies - if self.order().protocol_fees.len() > 1 { - return Err(Error::MultipleFeePolicies); - } - - match self.order().protocol_fees.first() { - Some(FeePolicy::Surplus { + fn protocol_fee( + &self, + prices: ClearingPrices, + protocol_fee: &FeePolicy, + ) -> Result { + match protocol_fee { + FeePolicy::Surplus { factor, max_volume_factor, - }) => self.calculate_fee( + } => self.calculate_fee( PriceLimits { sell: self.order().sell.amount, buy: self.order().buy.amount, @@ -93,11 +106,11 @@ impl Fulfillment { *factor, *max_volume_factor, ), - Some(FeePolicy::PriceImprovement { + FeePolicy::PriceImprovement { factor, max_volume_factor, quote, - }) => { + } => { let price_limits = adjust_quote_to_order_limits( Order { sell_amount: self.order().sell.amount.0, @@ -112,8 +125,7 @@ impl Fulfillment { )?; self.calculate_fee(price_limits, prices, *factor, *max_volume_factor) } - Some(FeePolicy::Volume { factor }) => self.fee_from_volume(prices, *factor), - None => Ok(0.into()), + FeePolicy::Volume { factor } => self.fee_from_volume(prices, *factor), } } @@ -175,11 +187,12 @@ impl Fulfillment { fn protocol_fee_in_sell_token( &self, prices: ClearingPrices, + protocol_fee: &FeePolicy, ) -> Result { let fee_in_sell_token = match self.order().side { - Side::Buy => self.protocol_fee(prices)?, + Side::Buy => self.protocol_fee(prices, protocol_fee)?, Side::Sell => self - .protocol_fee(prices)? + .protocol_fee(prices, protocol_fee)? .0 .checked_mul(prices.buy) .ok_or(Math::Overflow)? @@ -270,8 +283,6 @@ pub fn adjust_quote_to_order_limits(order: Order, quote: Quote) -> Result { @@ -152,29 +151,7 @@ impl Solution { .get(&trade.order().buy.token.wrap(self.weth)) .ok_or(error::Scoring::InvalidClearingPrices)?, }; - let custom_prices = scoring::CustomClearingPrices { - sell: match trade.order().side { - order::Side::Sell => trade - .executed() - .0 - .checked_mul(uniform_prices.sell) - .ok_or(error::Math::Overflow)? - .checked_ceil_div(&uniform_prices.buy) - .ok_or(error::Math::DivisionByZero)?, - order::Side::Buy => trade.executed().0, - }, - buy: match trade.order().side { - order::Side::Sell => trade.executed().0 + trade.fee().0, - order::Side::Buy => { - (trade.executed().0) - .checked_mul(uniform_prices.buy) - .ok_or(error::Math::Overflow)? - .checked_div(uniform_prices.sell) - .ok_or(error::Math::DivisionByZero)? - + trade.fee().0 - } - }, - }; + let custom_prices = trade.calculate_custom_prices(&uniform_prices)?; trades.push(scoring::Trade::new( trade.order().sell, trade.order().buy, diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index fb184ce65d..848d0375f8 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -15,14 +15,18 @@ use { domain::{ competition::{ auction, - order::fees::Quote, - solution::{fee, fee::adjust_quote_to_order_limits}, + order::{fees::Quote, FeePolicy}, + solution::{ + error, + fee::{self, adjust_quote_to_order_limits}, + }, PriceLimits, }, - eth::{self}, + eth::{self, TokenAmount}, }, util::conv::u256::U256Ext, }, + bigdecimal::Zero, }; /// Scoring contains trades with values as they are expected by the settlement @@ -66,7 +70,7 @@ pub struct Trade { side: Side, executed: order::TargetAmount, custom_price: CustomClearingPrices, - policies: Vec, + policies: Vec, } impl Trade { @@ -76,7 +80,7 @@ impl Trade { side: Side, executed: order::TargetAmount, custom_price: CustomClearingPrices, - policies: Vec, + policies: Vec, ) -> Self { Self { sell, @@ -158,17 +162,74 @@ impl Trade { Ok(price.in_eth(surplus.amount)) } - /// Protocol fee is defined by fee policies attached to the order. + /// Protocol fees is defined by fee policies attached to the order. /// /// Denominated in SURPLUS token - fn protocol_fee(&self) -> Result { - // TODO: support multiple fee policies - if self.policies.len() > 1 { - return Err(Error::MultipleFeePolicies); + fn protocol_fees(&self) -> Result { + let mut current_trade = self.clone(); + let mut amount = TokenAmount::default(); + for (i, protocol_fee) in self.policies.iter().enumerate().rev() { + let fee = current_trade.protocol_fee(protocol_fee)?; + // Do not need to calculate the last custom prices because in the last iteration + // the prices are not used anymore to calculate the protocol fee + amount += fee; + if !i.is_zero() { + current_trade.custom_price = self + .calculate_custom_prices(amount) + .map_err(|e| Error::CustomPrice(e.to_string()))?; + } } - let protocol_fee = |policy: &order::FeePolicy| match policy { - order::FeePolicy::Surplus { + Ok(eth::Asset { + token: self.surplus_token(), + amount, + }) + } + + /// Derive new custom prices (given the current custom prices) to exclude + /// the protocol fee from the trade. + /// + /// For this to work properly, `executed` amount is used to build actual + /// traded amounts. Note how the clearing prices actually represent the + /// traded amounts of a trade (as seen from the user perspective). + pub fn calculate_custom_prices( + &self, + protocol_fee: TokenAmount, + ) -> Result { + Ok(match self.side { + Side::Sell => CustomClearingPrices { + sell: self + .executed + .0 + .checked_mul(self.custom_price.sell) + .ok_or(Math::Overflow)? + .checked_div(self.custom_price.buy) + .ok_or(Math::DivisionByZero)? + .checked_add(protocol_fee.0) + .ok_or(Math::Overflow)?, + buy: self.executed.0, + }, + Side::Buy => CustomClearingPrices { + sell: self.executed.0, + buy: self + .executed + .0 + .checked_mul(self.custom_price.buy) + .ok_or(Math::Overflow)? + .checked_div(self.custom_price.sell) + .ok_or(Math::DivisionByZero)? + .checked_sub(protocol_fee.0) + .ok_or(Math::Negative)?, + }, + }) + } + + /// Protocol fee is defined by a fee policy attached to the order. + /// + /// Denominated in SURPLUS token + fn protocol_fee(&self, fee_policy: &FeePolicy) -> Result { + match fee_policy { + FeePolicy::Surplus { factor, max_volume_factor, } => { @@ -177,9 +238,9 @@ impl Trade { self.surplus_fee(surplus, *factor)?.amount, self.volume_fee(*max_volume_factor)?.amount, ); - Ok::(fee) + Ok::(fee) } - order::FeePolicy::PriceImprovement { + FeePolicy::PriceImprovement { factor, max_volume_factor, quote, @@ -191,14 +252,8 @@ impl Trade { ); Ok(fee) } - order::FeePolicy::Volume { factor } => Ok(self.volume_fee(*factor)?.amount), - }; - - let protocol_fee = self.policies.first().map(protocol_fee).transpose(); - Ok(eth::Asset { - token: self.surplus_token(), - amount: protocol_fee?.unwrap_or(0.into()), - }) + FeePolicy::Volume { factor } => Ok(self.volume_fee(*factor)?.amount), + } } fn price_improvement(&self, quote: &Quote) -> Result { @@ -330,7 +385,7 @@ impl Trade { /// /// Denominated in NATIVE token fn native_protocol_fee(&self, prices: &auction::Prices) -> Result { - let protocol_fee = self.protocol_fee()?; + let protocol_fee = self.protocol_fees()?; let price = prices .get(&protocol_fee.token) .ok_or(Error::MissingPrice(protocol_fee.token))?; @@ -360,10 +415,10 @@ pub struct CustomClearingPrices { #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("multiple fee policies are not supported yet")] - MultipleFeePolicies, #[error("missing native price for token {0:?}")] MissingPrice(eth::TokenAddress), #[error(transparent)] Math(#[from] Math), + #[error("failed to calculate custom price {0:?}")] + CustomPrice(String), } diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 86ddc296bb..e1b48b4293 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -1,11 +1,15 @@ use { super::error::Math, - crate::domain::{ - competition::{ - self, - order::{self, Side}, + crate::{ + domain::{ + competition::{ + self, + order::{self, Side}, + solution::{error, scoring}, + }, + eth::{self}, }, - eth, + util::conv::u256::U256Ext, }, }; @@ -76,10 +80,46 @@ impl Fulfillment { } } + /// Custom prices are calculated using the uniform clearing prices and the + /// fee. So that custom prices represent the actual traded amounts as + /// seen from the user perspective: the amount going in/out of their wallet + pub fn calculate_custom_prices( + &self, + uniform_prices: &ClearingPrices, + ) -> Result { + Ok(scoring::CustomClearingPrices { + sell: match self.order().side { + Side::Sell => self + .executed() + .0 + .checked_mul(uniform_prices.sell) + .ok_or(Math::Overflow)? + .checked_ceil_div(&uniform_prices.buy) + .ok_or(Math::DivisionByZero)?, + Side::Buy => self.executed().0, + }, + buy: match self.order().side { + Side::Sell => self.executed().0 + self.fee().0, + Side::Buy => { + (self.executed().0) + .checked_mul(uniform_prices.buy) + .ok_or(Math::Overflow)? + .checked_div(uniform_prices.sell) + .ok_or(Math::DivisionByZero)? + + self.fee().0 + } + }, + }) + } + pub fn order(&self) -> &competition::Order { &self.order } + pub fn side(&self) -> Side { + self.order.side + } + pub fn executed(&self) -> order::TargetAmount { self.executed } diff --git a/crates/driver/src/domain/eth/mod.rs b/crates/driver/src/domain/eth/mod.rs index 4442dd1a65..6469fe7798 100644 --- a/crates/driver/src/domain/eth/mod.rs +++ b/crates/driver/src/domain/eth/mod.rs @@ -1,7 +1,10 @@ use { crate::util::Bytes, itertools::Itertools, - std::collections::{HashMap, HashSet}, + std::{ + collections::{HashMap, HashSet}, + ops::{Div, Mul, Sub}, + }, }; pub mod allowance; @@ -197,6 +200,54 @@ impl TokenAmount { } } +impl Sub for TokenAmount { + type Output = TokenAmount; + + fn sub(self, rhs: Self) -> Self::Output { + self.0.sub(rhs.0).into() + } +} + +impl num::CheckedSub for TokenAmount { + fn checked_sub(&self, other: &Self) -> Option { + self.0.checked_sub(other.0).map(Into::into) + } +} + +impl Mul for TokenAmount { + type Output = TokenAmount; + + fn mul(self, rhs: Self) -> Self::Output { + self.0.mul(rhs.0).into() + } +} + +impl num::CheckedMul for TokenAmount { + fn checked_mul(&self, other: &Self) -> Option { + self.0.checked_mul(other.0).map(Into::into) + } +} + +impl num::CheckedAdd for TokenAmount { + fn checked_add(&self, other: &Self) -> Option { + self.0.checked_add(other.0).map(Into::into) + } +} + +impl Div for TokenAmount { + type Output = TokenAmount; + + fn div(self, rhs: Self) -> Self::Output { + self.0.div(rhs.0).into() + } +} + +impl num::CheckedDiv for TokenAmount { + fn checked_div(&self, other: &Self) -> Option { + self.0.checked_div(other.0).map(Into::into) + } +} + impl From for TokenAmount { fn from(value: U256) -> Self { Self(value) diff --git a/crates/driver/src/infra/solver/dto/auction.rs b/crates/driver/src/infra/solver/dto/auction.rs index 01963cb483..698d8ca75a 100644 --- a/crates/driver/src/infra/solver/dto/auction.rs +++ b/crates/driver/src/infra/solver/dto/auction.rs @@ -84,28 +84,28 @@ impl Auction { // // https://github.com/cowprotocol/services/issues/2440 if fee_handler == FeeHandler::Driver { - if let Some(fees::FeePolicy::Volume { factor }) = - order.protocol_fees.first() - { - match order.side { - Side::Buy => { - // reduce sell amount by factor - available.sell.amount = available - .sell - .amount - .apply_factor(1.0 / (1.0 + factor)) - .unwrap_or_default(); - } - Side::Sell => { - // increase buy amount by factor - available.buy.amount = available - .buy - .amount - .apply_factor(1.0 / (1.0 - factor)) - .unwrap_or_default(); + order.protocol_fees.iter().for_each(|protocol_fee| { + if let fees::FeePolicy::Volume { factor } = protocol_fee { + match order.side { + Side::Buy => { + // reduce sell amount by factor + available.sell.amount = available + .sell + .amount + .apply_factor(1.0 / (1.0 + factor)) + .unwrap_or_default(); + } + Side::Sell => { + // increase buy amount by factor + available.buy.amount = available + .buy + .amount + .apply_factor(1.0 / (1.0 - factor)) + .unwrap_or_default(); + } } } - } + }) } Order { uid: order.uid.into(), diff --git a/crates/driver/src/tests/cases/mod.rs b/crates/driver/src/tests/cases/mod.rs index fe12cad12c..8a1829d103 100644 --- a/crates/driver/src/tests/cases/mod.rs +++ b/crates/driver/src/tests/cases/mod.rs @@ -109,3 +109,10 @@ impl EtherExt for i32 { Ether(BigRational::from_i32(self).unwrap()) } } + +impl EtherExt for f64 { + fn ether(self) -> Ether { + assert!(self >= 0.0, "Ether supports non-negative values only"); + Ether(BigRational::from_f64(self).unwrap()) + } +} diff --git a/crates/driver/src/tests/cases/protocol_fees.rs b/crates/driver/src/tests/cases/protocol_fees.rs index 43e571fb02..0a8a11cd51 100644 --- a/crates/driver/src/tests/cases/protocol_fees.rs +++ b/crates/driver/src/tests/cases/protocol_fees.rs @@ -37,7 +37,7 @@ struct Order { struct TestCase { order: Order, - fee_policy: Policy, + fee_policy: Vec, execution: Execution, expected_score: eth::U256, fee_handler: FeeHandler, @@ -111,6 +111,241 @@ async fn protocol_fee_test_case(test_case: TestCase) { result.orders(&[order]); } +#[tokio::test] +#[ignore] +async fn triple_surplus_protocol_fee_buy_order_not_capped() { + let fee_policy_surplus = Policy::Surplus { + factor: 0.5, + // high enough so we don't get capped by volume fee + max_volume_factor: 1.0, + }; + let test_case = TestCase { + fee_policy: vec![ + fee_policy_surplus.clone(), + fee_policy_surplus.clone(), + fee_policy_surplus, + ], + order: Order { + sell_amount: 50.ether().into_wei(), + buy_amount: 40.ether().into_wei(), + side: order::Side::Buy, + }, + execution: Execution { + // 20 ETH surplus in sell token (after network fee) + // The protocol fees are applied one by one to the orders, altering the order pricing + // For this example: + // -> First fee policy: + // 20 ETH surplus (50 ETH - 30 ETH), surplus policy: 50 %, fee 10 ETH + // -> Second fee policy: + // New sell amount in the Order: 40 ETH + 10 ETH (fee) = 50 ETH + // New surplus: 10 ETH, fee 5 ETH + // -> Third fee policy: + // New sell amount in the Order: 50 ETH + 5 ETH (fee) = 55 ETH + // New surplus: 5 ETH, fee 2.5 ETH + // Total fee: 17.5 ETH + // Out of the 20 ETH of surplus, 17.5 ETH goes to fees and 2.5 ETH goes to the trader + solver: Amounts { + sell: 30.ether().into_wei(), + buy: 40.ether().into_wei(), + }, + driver: Amounts { + sell: 47.5.ether().into_wei(), + buy: 40.ether().into_wei(), + }, + }, + expected_score: 20.ether().into_wei(), + fee_handler: FeeHandler::Driver, + }; + + protocol_fee_test_case(test_case).await; +} + +#[tokio::test] +#[ignore] +async fn triple_surplus_protocol_fee_sell_order_not_capped() { + let fee_policy = Policy::Surplus { + factor: 0.5, + // high enough so we don't get capped by volume fee + max_volume_factor: 0.9, + }; + let test_case = TestCase { + fee_policy: vec![fee_policy.clone(), fee_policy.clone(), fee_policy], + order: Order { + sell_amount: 50.ether().into_wei(), + buy_amount: 40.ether().into_wei(), + side: order::Side::Sell, + }, + execution: Execution { + // 20 ETH surplus in sell token (after network fee) + // The protocol fees are applied one by one to the orders, altering the order pricing + // For this example: + // -> First fee policy: + // 20 ETH surplus (60 ETH - 40 ETH), surplus policy: 50 %, fee 10 ETH + // -> Second fee policy: + // New buy amount in the Order: 40 ETH + 10 ETH (fee) = 50 ETH + // -> Third fee policy: + // New buy amount in the Order: 50 ETH + 5 ETH (fee) = 55 ETH + // New surplus: 5 ETH, fee 2.5 ETH + // Total fee: 17.5 ETH + // Out of the 20 ETH of surplus, 17.5 ETH goes to fees and 2.5 ETH goes to the trader + solver: Amounts { + sell: 50.ether().into_wei(), + buy: 60.ether().into_wei(), + }, + driver: Amounts { + sell: 50.ether().into_wei(), + buy: 42.5.ether().into_wei(), + }, + }, + expected_score: 20.ether().into_wei(), + fee_handler: FeeHandler::Driver, + }; + protocol_fee_test_case(test_case).await; +} + +#[tokio::test] +#[ignore] +async fn surplus_and_volume_protocol_fee_buy_order_not_capped() { + let fee_policy_surplus = Policy::Surplus { + factor: 0.5, + // high enough so we don't get capped by volume fee + max_volume_factor: 1.0, + }; + let fee_policy_volume = Policy::Volume { factor: 0.25 }; + let test_case = TestCase { + fee_policy: vec![fee_policy_volume, fee_policy_surplus], + order: Order { + sell_amount: 60.ether().into_wei(), + buy_amount: 40.ether().into_wei(), + side: order::Side::Buy, + }, + execution: Execution { + // -> First fee policy: + // 25% of the solver proposed sell volume is kept by the protocol + // solver executes at the adjusted limit price ( 50 / (1 + 0.25) = 40 ) + // Fee = 50 ETH (limit price) - 40 ETH => 10 ETH + // -> Second fee policy: + // New buy amount in the Order: 40 ETH + 10 ETH (fee) = 50 ETH + // New surplus: 10 ETH, fee 5 ETH + // Total fee: 15 ETH + solver: Amounts { + sell: 40.ether().into_wei(), + buy: 40.ether().into_wei(), + }, + // driver executes at limit price + driver: Amounts { + sell: 55.ether().into_wei(), + buy: 40.ether().into_wei(), + }, + }, + expected_score: 20.ether().into_wei(), + fee_handler: FeeHandler::Driver, + }; + protocol_fee_test_case(test_case).await; +} + +#[tokio::test] +#[ignore] +async fn surplus_and_price_improvement_protocol_fee_sell_order_not_capped() { + let fee_policy_surplus = Policy::Surplus { + factor: 0.5, + // high enough so we don't get capped by volume fee + max_volume_factor: 0.9, + }; + let fee_policy_price_improvement = Policy::PriceImprovement { + factor: 0.5, + // high enough so we don't get capped by volume fee + max_volume_factor: 0.9, + quote: Quote { + sell: 50.ether().into_wei(), + buy: 50.ether().into_wei(), + network_fee: 5.ether().into_wei(), // 50 sell for 45 buy + }, + }; + let test_case = TestCase { + fee_policy: vec![fee_policy_price_improvement, fee_policy_surplus], + order: Order { + sell_amount: 50.ether().into_wei(), + // Demanding to receive less than quoted (in-market) + buy_amount: 25.ether().into_wei(), + side: order::Side::Sell, + }, + execution: Execution { + // -> First fee policy: + // Quote is 50 sell for 45 buy, which is equal to 20 sell for 18 buy + // Solver returns 20 sell for 30 buy, so the price improvement is 12 in buy token + // Receive 12 ETH more than quoted, half of which gets captured by the protocol + // Fee = 12 ETH * 0.5 => 6 ETH + // -> Second fee policy: + // Order is 50 sell for 25 buy, which is equal to 20 sell for 10 buy + // New buy amount in the Order: 10 ETH + 6 ETH (fee) = 16 ETH + // New surplus: 30 ETH - 16 ETH = 14 ETH, fee 7 ETH + // Total fee: 6 ETH + 7 ETH = 13 ETH + solver: Amounts { + sell: 20.ether().into_wei(), + buy: 30.ether().into_wei(), + }, + driver: Amounts { + sell: 20.ether().into_wei(), + // 30 ETH - 13 ETH = 17 ETH + buy: 17.ether().into_wei(), + }, + }, + expected_score: 20.ether().into_wei(), + fee_handler: FeeHandler::Driver, + }; + protocol_fee_test_case(test_case).await; +} + +#[tokio::test] +#[ignore] +async fn surplus_and_price_improvement_fee_buy_in_market_order_not_capped() { + let fee_policy_surplus = Policy::Surplus { + factor: 0.5, + // high enough so we don't get capped by volume fee + max_volume_factor: 1.0, + }; + let fee_policy_price_improvement = Policy::PriceImprovement { + factor: 0.5, + // high enough so we don't get capped by volume fee + max_volume_factor: 1.0, + quote: Quote { + sell: 49.ether().into_wei(), + buy: 40.ether().into_wei(), + network_fee: 1.ether().into_wei(), + }, + }; + let test_case = TestCase { + fee_policy: vec![fee_policy_price_improvement, fee_policy_surplus], + order: Order { + // Demanding to sell more than quoted (in-market) + sell_amount: 60.ether().into_wei(), + buy_amount: 40.ether().into_wei(), + side: order::Side::Buy, + }, + execution: Execution { + // -> First fee policy: + // Receive 10 ETH more than quoted, half of which gets captured by the protocol (10 ETH) + // Fee = 10 ETH * 0.5 => 5 ETH + // -> Second fee policy: + // New buy amount in the Order: 40 ETH + 5 ETH (fee) = 45 ETH + // New surplus: 5 ETH, fee 2.5 ETH + // Total fee: 7.5 ETH + solver: Amounts { + sell: 40.ether().into_wei(), + buy: 40.ether().into_wei(), + }, + driver: Amounts { + sell: 52.5.ether().into_wei(), + buy: 40.ether().into_wei(), + }, + }, + expected_score: 20.ether().into_wei(), + fee_handler: FeeHandler::Driver, + }; + protocol_fee_test_case(test_case).await; +} + #[tokio::test] #[ignore] async fn surplus_protocol_fee_buy_order_not_capped() { @@ -120,7 +355,7 @@ async fn surplus_protocol_fee_buy_order_not_capped() { max_volume_factor: 1.0, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 40.ether().into_wei(), @@ -153,7 +388,7 @@ async fn protocol_fee_calculated_on_the_solver_side() { max_volume_factor: 1.0, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 40.ether().into_wei(), @@ -185,7 +420,7 @@ async fn surplus_protocol_fee_sell_order_not_capped() { max_volume_factor: 0.9, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 40.ether().into_wei(), @@ -217,7 +452,7 @@ async fn surplus_protocol_fee_partial_buy_order_not_capped() { max_volume_factor: 1.0, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 50.ether().into_wei(), @@ -251,7 +486,7 @@ async fn surplus_protocol_fee_partial_sell_order_not_capped() { max_volume_factor: 0.9, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 50.ether().into_wei(), @@ -283,7 +518,7 @@ async fn surplus_protocol_fee_buy_order_capped() { max_volume_factor: 0.1, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 40.ether().into_wei(), @@ -315,7 +550,7 @@ async fn surplus_protocol_fee_sell_order_capped() { max_volume_factor: 0.1, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 40.ether().into_wei(), @@ -347,7 +582,7 @@ async fn surplus_protocol_fee_partial_buy_order_capped() { max_volume_factor: 0.2, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 50.ether().into_wei(), @@ -379,7 +614,7 @@ async fn surplus_protocol_fee_partial_sell_order_capped() { max_volume_factor: 0.1, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 50.ether().into_wei(), @@ -407,7 +642,7 @@ async fn surplus_protocol_fee_partial_sell_order_capped() { async fn volume_protocol_fee_buy_order() { let fee_policy = Policy::Volume { factor: 0.5 }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 40.ether().into_wei(), @@ -435,7 +670,7 @@ async fn volume_protocol_fee_buy_order() { async fn volume_protocol_fee_buy_order_at_limit_price() { let fee_policy = Policy::Volume { factor: 0.25 }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 40.ether().into_wei(), @@ -465,7 +700,7 @@ async fn volume_protocol_fee_buy_order_at_limit_price() { async fn volume_protocol_fee_sell_order() { let fee_policy = Policy::Volume { factor: 0.1 }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 40.ether().into_wei(), @@ -493,7 +728,7 @@ async fn volume_protocol_fee_sell_order() { async fn volume_protocol_fee_sell_order_at_limit_price() { let fee_policy = Policy::Volume { factor: 0.2 }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 40.ether().into_wei(), @@ -523,7 +758,7 @@ async fn volume_protocol_fee_sell_order_at_limit_price() { async fn volume_protocol_fee_partial_buy_order() { let fee_policy = Policy::Volume { factor: 0.5 }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 50.ether().into_wei(), @@ -551,7 +786,7 @@ async fn volume_protocol_fee_partial_buy_order() { async fn volume_protocol_fee_partial_buy_order_at_limit_price() { let fee_policy = Policy::Volume { factor: 0.25 }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 50.ether().into_wei(), @@ -582,7 +817,7 @@ async fn volume_protocol_fee_partial_buy_order_at_limit_price() { async fn volume_protocol_fee_partial_sell_order() { let fee_policy = Policy::Volume { factor: 0.1 }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 50.ether().into_wei(), @@ -610,7 +845,7 @@ async fn volume_protocol_fee_partial_sell_order() { async fn volume_protocol_fee_partial_sell_order_at_limit_price() { let fee_policy = Policy::Volume { factor: 0.2 }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), buy_amount: 50.ether().into_wei(), @@ -650,7 +885,7 @@ async fn price_improvement_fee_buy_in_market_order_not_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { // Demanding to sell more than quoted (in-market) sell_amount: 60.ether().into_wei(), @@ -688,7 +923,7 @@ async fn price_improvement_fee_sell_in_market_order_not_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), // Demanding to receive less than quoted (in-market) @@ -726,7 +961,7 @@ async fn price_improvement_fee_buy_out_of_market_order_not_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { // Demanding to sell less than quoted (out-market) sell_amount: 50.ether().into_wei(), @@ -764,7 +999,7 @@ async fn price_improvement_fee_sell_out_of_market_order_not_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), // Demanding to receive more than quoted (out-market) @@ -802,7 +1037,7 @@ async fn price_improvement_fee_buy_in_market_order_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { // Demanding to sell more than quoted (in-market) sell_amount: 60.ether().into_wei(), @@ -840,7 +1075,7 @@ async fn price_improvement_fee_sell_in_market_order_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), // Demanding to receive less than quoted (in-market) @@ -878,7 +1113,7 @@ async fn price_improvement_fee_buy_out_of_market_order_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { // Demanding to sell less than quoted (out-market) sell_amount: 50.ether().into_wei(), @@ -916,7 +1151,7 @@ async fn price_improvement_fee_sell_out_of_market_order_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), // Demanding to receive more than quoted (out-market) @@ -954,7 +1189,7 @@ async fn price_improvement_fee_partial_buy_in_market_order_not_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { // Demanding to sell more than quoted (in-market) sell_amount: 50.ether().into_wei(), @@ -992,7 +1227,7 @@ async fn price_improvement_fee_partial_sell_in_market_order_not_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), // Demanding to receive less than quoted (in-market) @@ -1032,7 +1267,7 @@ async fn price_improvement_fee_partial_buy_out_of_market_order_not_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { // Demanding to sell less than quoted (out-market) sell_amount: 50.ether().into_wei(), @@ -1070,7 +1305,7 @@ async fn price_improvement_fee_partial_sell_out_of_market_order_not_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), // Demanding to receive more than quoted (out-market) @@ -1108,7 +1343,7 @@ async fn price_improvement_fee_partial_buy_in_market_order_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { // Demanding to sell more than quoted (in-market) sell_amount: 75.ether().into_wei(), @@ -1146,7 +1381,7 @@ async fn price_improvement_fee_partial_sell_in_market_order_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), // Demanding to receive less than quoted (in-market) @@ -1184,7 +1419,7 @@ async fn price_improvement_fee_partial_buy_out_of_market_order_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { // Demanding to sell less than quoted (out-market) sell_amount: 50.ether().into_wei(), @@ -1222,7 +1457,7 @@ async fn price_improvement_fee_partial_sell_out_of_market_order_capped() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), // Demanding to receive more than quoted (out-market) @@ -1260,7 +1495,7 @@ async fn price_improvement_fee_sell_no_improvement() { }, }; let test_case = TestCase { - fee_policy, + fee_policy: vec![fee_policy], order: Order { sell_amount: 50.ether().into_wei(), // Demanding to receive less than quoted (in-market) diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index d40b31ddfe..2fab758d15 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -71,7 +71,15 @@ pub fn solve_req(test: &Test) -> serde_json::Value { "protocolFees": match quote.order.kind { order::Kind::Market => json!([]), order::Kind::Liquidity => json!([]), - order::Kind::Limit { .. } => json!([quote.order.fee_policy.to_json_value()]), + order::Kind::Limit { .. } => { + let fee_policies_json: Vec = quote + .order + .fee_policy + .iter() + .map(|policy| policy.to_json_value()) + .collect(); + json!(fee_policies_json) + } }, "validTo": u32::try_from(time::now().timestamp()).unwrap() + quote.order.valid_for.0, "kind": match quote.order.side { diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index a4beceac8c..554345d961 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -119,7 +119,7 @@ pub struct Order { /// Should the trader account be funded with enough tokens to place this /// order? True by default. pub funded: bool, - pub fee_policy: fee::Policy, + pub fee_policy: Vec, } impl Order { @@ -202,7 +202,7 @@ impl Order { } } - pub fn fee_policy(self, fee_policy: fee::Policy) -> Self { + pub fn fee_policy(self, fee_policy: Vec) -> Self { Self { fee_policy, ..self } } @@ -267,10 +267,10 @@ impl Default for Order { expected_amounts: Default::default(), filtered: Default::default(), funded: true, - fee_policy: fee::Policy::Surplus { + fee_policy: vec![fee::Policy::Surplus { factor: 0.0, max_volume_factor: 0.06, - }, + }], } } } diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index 9f791e7bed..e5f47c0eec 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -62,38 +62,54 @@ impl Solver { order::Side::Buy if config.quote => { "22300745198530623141535718272648361505980416".to_owned() } - order::Side::Buy => match quote.order.fee_policy { - // If the fees are handler in the driver, for volume based fee, we artificially - // reduce the limit sell amount for buy orders before sending to solvers. This - // allows driver to withhold volume based fee and not violate original limit - // prices. - fee::Policy::Volume { factor } if config.fee_handler == FeeHandler::Driver => { - eth::TokenAmount(quote.sell_amount()) - .apply_factor(1.0 / (1.0 + factor)) - .unwrap() - .0 - .to_string() + order::Side::Buy => { + let mut current_sell_amount = quote.sell_amount(); + for fee_policy in "e.order.fee_policy { + match fee_policy { + // If the fees are handled in the driver, for volume based fee, we + // artificially reduce the limit sell amount + // for buy orders before sending to solvers. This + // allows driver to withhold volume based fee and not violate original + // limit prices. + fee::Policy::Volume { factor } + if config.fee_handler == FeeHandler::Driver => + { + current_sell_amount = eth::TokenAmount(current_sell_amount) + .apply_factor(1.0 / (1.0 + factor)) + .unwrap() + .0; + } + _ => {} + } } - _ => quote.sell_amount().to_string(), - }, + current_sell_amount.to_string() + } _ => quote.sell_amount().to_string(), }; let buy_amount = match quote.order.side { order::Side::Sell if config.quote => "1".to_owned(), - order::Side::Sell => match quote.order.fee_policy { - // If the fees are handler in the driver, for volume based fee, we artificially - // increase the limit buy amount for sell orders before sending to solvers. This - // allows driver to withhold volume based fee and not violate original limit - // prices. - fee::Policy::Volume { factor } if config.fee_handler == FeeHandler::Driver => { - eth::TokenAmount(quote.buy_amount()) - .apply_factor(1.0 / (1.0 - factor)) - .unwrap() - .0 - .to_string() + order::Side::Sell => { + let mut current_buy_amount = quote.buy_amount(); + for fee_policy in "e.order.fee_policy { + match fee_policy { + // If the fees are handled in the driver, for volume based fee, we + // artificially increase the limit buy + // amount for sell orders before sending to solvers. This + // allows driver to withhold volume based fee and not violate original + // limit prices. + fee::Policy::Volume { factor } + if config.fee_handler == FeeHandler::Driver => + { + current_buy_amount = eth::TokenAmount(current_buy_amount) + .apply_factor(1.0 / (1.0 - factor)) + .unwrap() + .0; + } + _ => {} + } } - _ => quote.buy_amount().to_string(), - }, + current_buy_amount.to_string() + } _ => quote.buy_amount().to_string(), }; @@ -123,7 +139,13 @@ impl Solver { order::Kind::Market => json!([]), order::Kind::Liquidity => json!([]), order::Kind::Limit { .. } => { - json!([quote.order.fee_policy.to_json_value()]) + let fee_policies_json: Vec = quote + .order + .fee_policy + .iter() + .map(|policy| policy.to_json_value()) + .collect(); + json!(fee_policies_json) } }, ); diff --git a/crates/e2e/tests/e2e/protocol_fee.rs b/crates/e2e/tests/e2e/protocol_fee.rs index 7248d7457a..0eeb1eb8d8 100644 --- a/crates/e2e/tests/e2e/protocol_fee.rs +++ b/crates/e2e/tests/e2e/protocol_fee.rs @@ -121,6 +121,7 @@ async fn combined_protocol_fees(web3: Web3) { "--protocol-fee-exempt-addresses={:?}", trader_exempt.address() ), + "--enable-multiple-fees=true".to_string(), ]; let services = Services::new(onchain.contracts()).await; services @@ -184,7 +185,8 @@ async fn combined_protocol_fees(web3: Web3) { ); let partner_fee_order = OrderCreation { sell_amount, - buy_amount: to_wei(5), + // to make sure the order is out-of-market + buy_amount: partner_fee_quote.quote.buy_amount * 3 / 2, app_data: partner_fee_app_data.clone(), ..sell_order_from_quote(&partner_fee_quote) } @@ -211,6 +213,9 @@ async fn combined_protocol_fees(web3: Web3) { onchain .mint_token_to_weth_uni_v2_pool(&limit_order_token, to_wei(1000)) .await; + onchain + .mint_token_to_weth_uni_v2_pool(&partner_fee_order_token, to_wei(1000)) + .await; tracing::info!("Waiting for liquidity state to update"); wait_for_condition(TIMEOUT, || async { @@ -229,19 +234,26 @@ async fn combined_protocol_fees(web3: Web3) { new_market_order_quote.quote.buy_amount != market_quote_before.quote.buy_amount }) .await - .unwrap(); - - let [market_quote_after, limit_quote_after] = - futures::future::try_join_all([&market_order_token, &limit_order_token].map(|token| { - get_quote( - &services, - onchain.contracts().weth.address(), - token.address(), - OrderKind::Sell, - sell_amount, - quote_valid_to, - ) - })) + .expect("Timeout waiting for eviction of the cached liquidity"); + + let [market_quote_after, limit_quote_after, partner_fee_quote_after] = + futures::future::try_join_all( + [ + &market_order_token, + &limit_order_token, + &partner_fee_order_token, + ] + .map(|token| { + get_quote( + &services, + onchain.contracts().weth.address(), + token.address(), + OrderKind::Sell, + sell_amount, + quote_valid_to, + ) + }), + ) .await .unwrap() .try_into() @@ -283,7 +295,9 @@ async fn combined_protocol_fees(web3: Web3) { .into_iter() .all(std::convert::identity) }; - wait_for_condition(TIMEOUT, metadata_updated).await.unwrap(); + wait_for_condition(TIMEOUT, metadata_updated) + .await + .expect("Timeout waiting for the orders to trade"); tracing::info!("Checking executions..."); let market_price_improvement_order = services @@ -301,12 +315,18 @@ async fn combined_protocol_fees(web3: Web3) { let partner_fee_order = services.get_order(&partner_fee_order_uid).await.unwrap(); let partner_fee_executed_surplus_fee_in_buy_token = - surplus_fee_in_buy_token(&partner_fee_order, &partner_fee_quote.quote); + surplus_fee_in_buy_token(&partner_fee_order, &partner_fee_quote_after.quote); assert!( // see `--fee-policy-max-partner-fee` autopilot config argument, which is 0.02 partner_fee_executed_surplus_fee_in_buy_token >= partner_fee_quote.quote.buy_amount * 2 / 100 ); + let limit_quote_diff = partner_fee_quote_after + .quote + .buy_amount + .saturating_sub(partner_fee_order.data.buy_amount); + // see `limit_surplus_policy.factor`, which is 0.3 + assert!(partner_fee_executed_surplus_fee_in_buy_token >= limit_quote_diff * 3 / 10); let limit_surplus_order = services.get_order(&limit_surplus_order_uid).await.unwrap(); let limit_executed_surplus_fee_in_buy_token = @@ -342,7 +362,7 @@ async fn combined_protocol_fees(web3: Web3) { .await .unwrap() .try_into() - .expect("Expected exactly three elements"); + .expect("Expected exactly four elements"); assert_approximately_eq!( market_executed_surplus_fee_in_buy_token, market_order_token_balance @@ -414,7 +434,10 @@ async fn volume_fee_buy_order_test(web3: Web3) { // applied policy_order_class: FeePolicyOrderClass::Any, }; - let protocol_fees_config = ProtocolFeesConfig(vec![protocol_fee]).to_string(); + // Protocol fee set twice to test that only one policy will apply if the + // autopilot is not configured to support multiple fees + let protocol_fees_config = + ProtocolFeesConfig(vec![protocol_fee.clone(), protocol_fee]).to_string(); let mut onchain = OnchainComponents::deploy(web3.clone()).await; @@ -543,11 +566,13 @@ async fn volume_fee_buy_order_test(web3: Web3) { struct ProtocolFeesConfig(Vec); +#[derive(Clone)] struct ProtocolFee { policy: FeePolicyKind, policy_order_class: FeePolicyOrderClass, } +#[derive(Clone)] enum FeePolicyOrderClass { Market, Limit,