diff --git a/crates/autopilot/src/arguments.rs b/crates/autopilot/src/arguments.rs index 4b60bdfee1..0224991454 100644 --- a/crates/autopilot/src/arguments.rs +++ b/crates/autopilot/src/arguments.rs @@ -9,6 +9,7 @@ use { std::{ net::SocketAddr, num::{NonZeroUsize, ParseFloatError}, + str::FromStr, time::Duration, }, url::Url, @@ -208,6 +209,10 @@ pub struct Arguments { )] pub solve_deadline: Duration, + /// Describes how the protocol fee should be calculated. + #[clap(flatten)] + pub fee_policy: FeePolicy, + /// Time interval in days between each cleanup operation of the /// `order_events` database table. #[clap(long, env, default_value = "1", value_parser = duration_from_days)] @@ -284,6 +289,7 @@ impl std::fmt::Display for Arguments { writeln!(f, "score_cap: {}", self.score_cap)?; display_option(f, "shadow", &self.shadow)?; writeln!(f, "solve_deadline: {:?}", self.solve_deadline)?; + writeln!(f, "fee_policy: {:?}", self.fee_policy)?; writeln!( f, "order_events_cleanup_interval: {:?}", @@ -298,6 +304,73 @@ impl std::fmt::Display for Arguments { } } +#[derive(clap::Parser, Debug, Clone)] +pub struct FeePolicy { + /// Type of fee policy to use. Examples: + /// + /// - 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 = "priceImprovement:0.0:1.0")] + 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, +} + +#[derive(clap::Parser, Debug, Clone)] +pub enum FeePolicyKind { + /// How much of the order's price improvement over max(limit price, + /// best_bid) should be taken as a protocol fee. + PriceImprovement { factor: f64, max_volume_factor: f64 }, + /// How much of the order's volume should be taken as a protocol fee. + Volume { factor: f64 }, +} + +impl FromStr for FeePolicyKind { + type Err = String; + + fn from_str(s: &str) -> Result { + let mut parts = s.split(':'); + let kind = parts.next().ok_or("missing fee policy kind")?; + match kind { + "priceImprovement" => { + let factor = parts + .next() + .ok_or("missing price improvement factor")? + .parse::() + .map_err(|e| format!("invalid price improvement factor: {}", e))?; + let max_volume_factor = parts + .next() + .ok_or("missing max volume factor")? + .parse::() + .map_err(|e| format!("invalid max volume factor: {}", e))?; + Ok(Self::PriceImprovement { + factor, + max_volume_factor, + }) + } + "volume" => { + let factor = parts + .next() + .ok_or("missing volume factor")? + .parse::() + .map_err(|e| format!("invalid volume factor: {}", e))?; + Ok(Self::Volume { factor }) + } + _ => Err(format!("invalid fee policy kind: {}", kind)), + } + } +} + fn duration_from_days(s: &str) -> Result { let days = s.parse::()?; Ok(Duration::from_secs_f64(days * 86_400.0)) diff --git a/crates/autopilot/src/driver_model.rs b/crates/autopilot/src/driver_model.rs index 5dd07e472a..d032889efb 100644 --- a/crates/autopilot/src/driver_model.rs +++ b/crates/autopilot/src/driver_model.rs @@ -49,6 +49,7 @@ pub mod quote { pub mod solve { use { + crate::arguments, chrono::{DateTime, Utc}, model::{ app_data::AppDataHash, @@ -87,7 +88,7 @@ pub mod solve { } #[serde_as] - #[derive(Clone, Debug, Serialize, Deserialize)] + #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Order { pub uid: OrderUid, @@ -116,6 +117,9 @@ pub mod solve { pub app_data: AppDataHash, #[serde(flatten)] pub signature: Signature, + /// The types of fees that will be collected by the protocol. + /// Multiple fees are applied in the order they are listed + pub fee_policies: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -137,6 +141,51 @@ pub mod solve { pub call_data: Vec, } + #[derive(Clone, Debug, Serialize)] + #[serde(rename_all = "camelCase")] + pub enum FeePolicy { + /// If the order receives more than expected (positive deviation from + /// quoted amounts) pay the protocol a factor of the achieved + /// improvement. The fee is taken in `sell` token for `buy` + /// orders and in `buy` token for `sell` orders. + #[serde(rename_all = "camelCase")] + PriceImprovement { + /// Factor of price improvement the protocol charges as a fee. + /// Price improvement is the difference between executed price and + /// limit price or quoted price (whichever is better) + /// + /// E.g. if a user received 2000USDC for 1ETH while having been + /// quoted 1990USDC, their price improvement is 10USDC. + /// A factor of 0.5 requires the solver to pay 5USDC to + /// the protocol for settling this order. + factor: f64, + /// Cap protocol fee with a percentage of the order's volume. + max_volume_factor: f64, + }, + /// How much of the order's volume should be taken as a protocol fee. + /// The fee is taken in `sell` token for `sell` orders and in `buy` + /// token for `buy` orders. + #[serde(rename_all = "camelCase")] + Volume { + /// Percentage of the order's volume should be taken as a protocol + /// fee. + factor: f64, + }, + } + + pub fn fee_policy_to_dto(fee_policy: &arguments::FeePolicy) -> FeePolicy { + match fee_policy.fee_policy_kind { + arguments::FeePolicyKind::PriceImprovement { + factor: price_improvement_factor, + max_volume_factor, + } => FeePolicy::PriceImprovement { + factor: price_improvement_factor, + max_volume_factor, + }, + arguments::FeePolicyKind::Volume { factor } => FeePolicy::Volume { factor }, + } + } + #[serde_as] #[derive(Clone, Debug, Default, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs index 5daaf3dea5..2abd64ae19 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -634,6 +634,7 @@ pub async fn run(args: Arguments) { max_settlement_transaction_wait: args.max_settlement_transaction_wait, solve_deadline: args.solve_deadline, in_flight_orders: Default::default(), + fee_policy: args.fee_policy, }; run.run_forever().await; unreachable!("run loop exited"); @@ -695,6 +696,7 @@ async fn shadow_mode(args: Arguments) -> ! { trusted_tokens, args.score_cap, args.solve_deadline, + args.fee_policy, ); shadow.run_forever().await; diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index 176f12c532..0c5ca73773 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -1,5 +1,6 @@ use { crate::{ + arguments, database::{ competition::{Competition, ExecutedFee, OrderExecution}, Postgres, @@ -8,7 +9,7 @@ use { driver_model::{ reveal::{self, Request}, settle, - solve::{self, Class, TradedAmounts}, + solve::{self, fee_policy_to_dto, Class, TradedAmounts}, }, solvable_orders::SolvableOrdersCache, }, @@ -56,6 +57,7 @@ pub struct RunLoop { pub max_settlement_transaction_wait: Duration, pub solve_deadline: Duration, pub in_flight_orders: Arc>, + pub fee_policy: arguments::FeePolicy, } impl RunLoop { @@ -310,6 +312,7 @@ impl RunLoop { &self.market_makable_token_list.all(), self.score_cap, self.solve_deadline, + self.fee_policy.clone(), ); let request = &request; @@ -492,6 +495,7 @@ pub fn solve_request( trusted_tokens: &HashSet, score_cap: U256, time_limit: Duration, + fee_policy: arguments::FeePolicy, ) -> solve::Request { solve::Request { id, @@ -517,6 +521,17 @@ pub fn solve_request( .collect() }; let order_is_untouched = remaining_order.executed_amount.is_zero(); + + let fee_policies = match order.metadata.class { + OrderClass::Market => vec![], + OrderClass::Liquidity => vec![], + // todo https://github.com/cowprotocol/services/issues/2092 + // skip protocol fee for limit orders with in-market price + + // todo https://github.com/cowprotocol/services/issues/2115 + // skip protocol fee for TWAP limit orders + OrderClass::Limit(_) => vec![fee_policy_to_dto(&fee_policy)], + }; solve::Order { uid: order.metadata.uid, sell_token: order.data.sell_token, @@ -542,6 +557,7 @@ pub fn solve_request( class, app_data: order.data.app_data, signature: order.signature.clone(), + fee_policies, } }) .collect(), diff --git a/crates/autopilot/src/shadow.rs b/crates/autopilot/src/shadow.rs index 1c2c9c74f2..b60186a9ea 100644 --- a/crates/autopilot/src/shadow.rs +++ b/crates/autopilot/src/shadow.rs @@ -9,8 +9,12 @@ use { crate::{ + arguments::FeePolicy, driver_api::Driver, - driver_model::{reveal, solve}, + driver_model::{ + reveal, + solve::{self}, + }, protocol, run_loop, }, @@ -43,6 +47,7 @@ pub struct RunLoop { block: u64, score_cap: U256, solve_deadline: Duration, + fee_policy: FeePolicy, } impl RunLoop { @@ -52,6 +57,7 @@ impl RunLoop { trusted_tokens: AutoUpdatingTokenList, score_cap: U256, solve_deadline: Duration, + fee_policy: FeePolicy, ) -> Self { Self { orderbook, @@ -61,6 +67,7 @@ impl RunLoop { block: 0, score_cap, solve_deadline, + fee_policy, } } @@ -193,6 +200,7 @@ impl RunLoop { &self.trusted_tokens.all(), self.score_cap, self.solve_deadline, + self.fee_policy.clone(), ); let request = &request; diff --git a/crates/driver/src/domain/competition/order/fees.rs b/crates/driver/src/domain/competition/order/fees.rs new file mode 100644 index 0000000000..6ef53cd075 --- /dev/null +++ b/crates/driver/src/domain/competition/order/fees.rs @@ -0,0 +1,28 @@ +#[derive(Clone, Debug)] +pub enum FeePolicy { + /// If the order receives more than expected (positive deviation from quoted + /// amounts) pay the protocol a factor of the achieved improvement. + /// The fee is taken in `sell` token for `buy` orders and in `buy` + /// token for `sell` orders. + PriceImprovement { + /// Factor of price improvement the protocol charges as a fee. + /// Price improvement is the difference between executed price and + /// limit price or quoted price (whichever is better) + /// + /// E.g. if a user received 2000USDC for 1ETH while having been quoted + /// 1990USDC, their price improvement is 10USDC. A factor of 0.5 + /// requires the solver to pay 5USDC to the protocol for + /// settling this order. + factor: f64, + /// Cap protocol fee with a percentage of the order's volume. + max_volume_factor: f64, + }, + /// How much of the order's volume should be taken as a protocol fee. + /// The fee is taken in `sell` token for `sell` orders and in `buy` + /// token for `buy` orders. + Volume { + /// Percentage of the order's volume should be taken as a protocol + /// fee. + factor: f64, + }, +} diff --git a/crates/driver/src/domain/competition/order/mod.rs b/crates/driver/src/domain/competition/order/mod.rs index 5ac564979f..4135ab54bf 100644 --- a/crates/driver/src/domain/competition/order/mod.rs +++ b/crates/driver/src/domain/competition/order/mod.rs @@ -1,4 +1,3 @@ -pub use signature::Signature; use { super::auction, crate::{ @@ -9,7 +8,9 @@ use { bigdecimal::Zero, num::CheckedDiv, }; +pub use {fees::FeePolicy, signature::Signature}; +pub mod fees; pub mod signature; /// An order in the auction. @@ -39,6 +40,10 @@ pub struct Order { pub sell_token_balance: SellTokenBalance, pub buy_token_balance: BuyTokenBalance, pub signature: Signature, + /// The types of fees the protocol collects from the winning solver. + /// Unless otherwise configured, the driver modifies solutions to take + /// sufficient fee in the form of positive slippage. + pub fee_policies: Vec, } /// An amount denominated in the sell token of an [`Order`]. @@ -453,6 +458,7 @@ mod tests { data: Default::default(), signer: Default::default(), }, + fee_policies: Default::default(), }; assert_eq!( diff --git a/crates/driver/src/domain/quote.rs b/crates/driver/src/domain/quote.rs index b0623c786e..67146641bb 100644 --- a/crates/driver/src/domain/quote.rs +++ b/crates/driver/src/domain/quote.rs @@ -135,6 +135,7 @@ impl Order { data: Default::default(), signer: Default::default(), }, + fee_policies: Default::default(), }], [ auction::Token { diff --git a/crates/driver/src/infra/api/routes/solve/dto/auction.rs b/crates/driver/src/infra/api/routes/solve/dto/auction.rs index 6cd945ed75..f2a193cc00 100644 --- a/crates/driver/src/infra/api/routes/solve/dto/auction.rs +++ b/crates/driver/src/infra/api/routes/solve/dto/auction.rs @@ -115,6 +115,22 @@ impl Auction { data: order.signature.into(), signer: order.owner.into(), }, + fee_policies: order + .fee_policies + .into_iter() + .map(|policy| match policy { + FeePolicy::PriceImprovement { + factor, + max_volume_factor, + } => competition::order::FeePolicy::PriceImprovement { + factor, + max_volume_factor, + }, + FeePolicy::Volume { factor } => { + competition::order::FeePolicy::Volume { factor } + } + }) + .collect(), }) .collect(), self.tokens.into_iter().map(|token| { @@ -234,6 +250,7 @@ struct Order { signing_scheme: SigningScheme, #[serde_as(as = "serialize::Hex")] signature: Vec, + fee_policies: Vec, } #[derive(Debug, Deserialize)] @@ -287,3 +304,12 @@ enum Class { Limit, Liquidity, } + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +enum FeePolicy { + #[serde(rename_all = "camelCase")] + PriceImprovement { factor: f64, max_volume_factor: f64 }, + #[serde(rename_all = "camelCase")] + Volume { factor: f64 }, +} diff --git a/crates/driver/src/tests/setup/driver.rs b/crates/driver/src/tests/setup/driver.rs index d1961ec2f0..f9d246567f 100644 --- a/crates/driver/src/tests/setup/driver.rs +++ b/crates/driver/src/tests/setup/driver.rs @@ -99,7 +99,18 @@ pub fn solve_req(test: &Test) -> serde_json::Value { }, "appData": "0x0000000000000000000000000000000000000000000000000000000000000000", "signingScheme": "eip712", - "signature": format!("0x{}", hex::encode(quote.order_signature(&test.blockchain))) + "signature": format!("0x{}", hex::encode(quote.order_signature(&test.blockchain))), + "feePolicies": [{ + "priceImprovement": { + "factor": 0.5, + "maxVolumeFactor": 0.06, + } + }, + { + "volume": { + "factor": 0.1, + } + }], })); } for fulfillment in test.fulfillments.iter() {