diff --git a/crates/autopilot/src/arguments.rs b/crates/autopilot/src/arguments.rs index 782657811b..e9edc5b274 100644 --- a/crates/autopilot/src/arguments.rs +++ b/crates/autopilot/src/arguments.rs @@ -349,6 +349,12 @@ pub struct FeePolicy { /// - 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:1.0")] @@ -362,16 +368,23 @@ pub struct FeePolicy { } impl FeePolicy { - pub fn to_domain(self) -> domain::fee::Policy { + pub fn to_policy_builder(&self) -> domain::fee::PolicyBuilder { match self.fee_policy_kind { FeePolicyKind::Surplus { factor, max_volume_factor, - } => domain::fee::Policy::Surplus { + } => domain::fee::PolicyBuilder::Surplus { factor, max_volume_factor, }, - FeePolicyKind::Volume { factor } => domain::fee::Policy::Volume { factor }, + FeePolicyKind::PriceImprovement { + factor, + max_volume_factor, + } => domain::fee::PolicyBuilder::PriceImprovement { + factor, + max_volume_factor, + }, + FeePolicyKind::Volume { factor } => domain::fee::PolicyBuilder::Volume { factor }, } } } @@ -380,6 +393,9 @@ impl FeePolicy { pub enum FeePolicyKind { /// How much of the order's surplus should be taken as a protocol fee. Surplus { factor: f64, max_volume_factor: f64 }, + /// How much of the order's price improvement 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 }, } @@ -407,6 +423,22 @@ impl FromStr for FeePolicyKind { max_volume_factor, }) } + "price-improvement" => { + 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 price-improvement max volume factor")? + .parse::() + .map_err(|e| format!("invalid price-improvement max volume factor: {}", e))?; + Ok(Self::PriceImprovement { + factor, + max_volume_factor, + }) + } "volume" => { let factor = parts .next() diff --git a/crates/autopilot/src/database/fee_policies.rs b/crates/autopilot/src/database/fee_policies.rs index 0796d6ba6c..a06259e353 100644 --- a/crates/autopilot/src/database/fee_policies.rs +++ b/crates/autopilot/src/database/fee_policies.rs @@ -9,7 +9,7 @@ pub async fn insert_batch( ) -> Result<(), sqlx::Error> { let mut query_builder = QueryBuilder::new( "INSERT INTO fee_policies (auction_id, order_uid, kind, surplus_factor, \ - max_volume_factor, volume_factor) ", + max_volume_factor, volume_factor, quote_sell_amount, quote_buy_amount) ", ); query_builder.push_values(fee_policies, |mut b, fee_policy| { @@ -18,7 +18,9 @@ pub async fn insert_batch( .push_bind(fee_policy.kind) .push_bind(fee_policy.surplus_factor) .push_bind(fee_policy.max_volume_factor) - .push_bind(fee_policy.volume_factor); + .push_bind(fee_policy.volume_factor) + .push_bind(fee_policy.quote_sell_amount) + .push_bind(fee_policy.quote_buy_amount); }); query_builder.build().execute(ex).await.map(|_| ()) @@ -46,7 +48,7 @@ pub async fn fetch( #[cfg(test)] mod tests { - use {super::*, database::byte_array::ByteArray, sqlx::Connection}; + use {super::*, bigdecimal::BigDecimal, database::byte_array::ByteArray, sqlx::Connection}; #[tokio::test] #[ignore] @@ -66,6 +68,8 @@ mod tests { surplus_factor: Some(0.1), max_volume_factor: Some(1.0), volume_factor: None, + quote_sell_amount: None, + quote_buy_amount: None, }; // surplus fee policy with caps let fee_policy_2 = dto::FeePolicy { @@ -75,6 +79,8 @@ mod tests { surplus_factor: Some(0.2), max_volume_factor: Some(0.05), volume_factor: None, + quote_sell_amount: None, + quote_buy_amount: None, }; // volume based fee policy let fee_policy_3 = dto::FeePolicy { @@ -84,6 +90,19 @@ mod tests { surplus_factor: None, max_volume_factor: None, volume_factor: Some(0.06), + quote_sell_amount: None, + quote_buy_amount: None, + }; + // price improvement fee policy + let fee_policy_4 = dto::FeePolicy { + auction_id, + order_uid, + kind: dto::fee_policy::FeePolicyKind::PriceImprovement, + surplus_factor: Some(0.3), + max_volume_factor: Some(0.07), + volume_factor: None, + quote_sell_amount: Some(BigDecimal::new(100.into(), 1)), + quote_buy_amount: Some(BigDecimal::new(200.into(), 1)), }; insert_batch( &mut db, @@ -91,12 +110,16 @@ mod tests { fee_policy_1.clone(), fee_policy_2.clone(), fee_policy_3.clone(), + fee_policy_4.clone(), ], ) .await .unwrap(); let output = fetch(&mut db, 1, order_uid).await.unwrap(); - assert_eq!(output, vec![fee_policy_1, fee_policy_2, fee_policy_3]); + assert_eq!( + output, + vec![fee_policy_1, fee_policy_2, fee_policy_3, fee_policy_4] + ); } } diff --git a/crates/autopilot/src/domain/fee/mod.rs b/crates/autopilot/src/domain/fee/mod.rs index 02222cc13c..13b06976a5 100644 --- a/crates/autopilot/src/domain/fee/mod.rs +++ b/crates/autopilot/src/domain/fee/mod.rs @@ -4,65 +4,64 @@ //! we define the way to calculate the protocol fee based on the configuration //! parameters. -use crate::{ - boundary::{self}, - domain, +use { + crate::{ + boundary::{self}, + domain, + }, + primitive_types::U256, }; /// Constructs fee policies based on the current configuration. #[derive(Debug)] pub struct ProtocolFee { - policy: Policy, + policy_builder: PolicyBuilder, fee_policy_skip_market_orders: bool, } impl ProtocolFee { - pub fn new(policy: Policy, fee_policy_skip_market_orders: bool) -> Self { + pub fn new(policy: PolicyBuilder, fee_policy_skip_market_orders: bool) -> Self { Self { - policy, + policy_builder: policy, fee_policy_skip_market_orders, } } - /// Get policies for order. - pub fn get(&self, order: &boundary::Order, quote: Option<&domain::Quote>) -> Vec { - match order.metadata.class { + /// Converts an order from the boundary layer to the domain layer, applying + /// protocol fees if necessary. + pub fn to_order(&self, order: boundary::Order, quote: &domain::Quote) -> domain::Order { + let protocol_fees = match order.metadata.class { boundary::OrderClass::Market => { if self.fee_policy_skip_market_orders { vec![] } else { - vec![self.policy] + vec![self.policy_builder.build_with(quote)] } } boundary::OrderClass::Liquidity => vec![], boundary::OrderClass::Limit => { if !self.fee_policy_skip_market_orders { - return vec![self.policy]; - } - - // if the quote is missing, we can't determine if the order is outside the - // market price so we protect the user and not charge a fee - let Some(quote) = quote else { - return 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, - }; - if boundary::is_order_outside_market_price(&order_, "e) { - vec![self.policy] + vec![self.policy_builder.build_with(quote)] } else { - 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, + }; + if boundary::is_order_outside_market_price(&order_, "e_) { + vec![self.policy_builder.build_with(quote)] + } else { + vec![] + } } } - } + }; + boundary::order::to_domain(order, protocol_fees) } } @@ -83,6 +82,14 @@ pub enum Policy { /// Cap protocol fee with a percentage of the order's volume. max_volume_factor: f64, }, + /// A price improvement corresponds to a situation where the order is + /// executed at a better price than the top quote. The protocol fee in such + /// case is calculated from a cut of this price improvement. + PriceImprovement { + factor: f64, + max_volume_factor: f64, + quote: Quote, + }, /// 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. @@ -92,3 +99,48 @@ pub enum Policy { factor: f64, }, } + +#[derive(Debug)] +pub enum PolicyBuilder { + Surplus { factor: f64, max_volume_factor: f64 }, + PriceImprovement { factor: f64, max_volume_factor: f64 }, + Volume { factor: f64 }, +} + +impl PolicyBuilder { + pub fn build_with(&self, quote: &domain::Quote) -> Policy { + match self { + PolicyBuilder::Surplus { + factor, + max_volume_factor, + } => Policy::Surplus { + factor: *factor, + max_volume_factor: *max_volume_factor, + }, + PolicyBuilder::PriceImprovement { + factor, + max_volume_factor, + } => Policy::PriceImprovement { + factor: *factor, + max_volume_factor: *max_volume_factor, + quote: quote.clone().into(), + }, + PolicyBuilder::Volume { factor } => Policy::Volume { factor: *factor }, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Quote { + pub sell_amount: U256, + pub buy_amount: U256, +} + +impl From for Quote { + fn from(value: domain::Quote) -> Self { + Self { + sell_amount: value.sell_amount, + buy_amount: value.buy_amount, + } + } +} diff --git a/crates/autopilot/src/infra/persistence/dto/fee_policy.rs b/crates/autopilot/src/infra/persistence/dto/fee_policy.rs index bfbd52ec2e..16c0e3e3dc 100644 --- a/crates/autopilot/src/infra/persistence/dto/fee_policy.rs +++ b/crates/autopilot/src/infra/persistence/dto/fee_policy.rs @@ -1,4 +1,8 @@ -use crate::{boundary, domain}; +use { + crate::{boundary, domain}, + bigdecimal::BigDecimal, + number::conversions::{big_decimal_to_u256, u256_to_big_decimal}, +}; #[derive(Debug, Clone, PartialEq, sqlx::FromRow)] pub struct FeePolicy { @@ -8,6 +12,8 @@ pub struct FeePolicy { pub surplus_factor: Option, pub max_volume_factor: Option, pub volume_factor: Option, + pub quote_sell_amount: Option, + pub quote_buy_amount: Option, } impl FeePolicy { @@ -27,6 +33,8 @@ impl FeePolicy { surplus_factor: Some(factor), max_volume_factor: Some(max_volume_factor), volume_factor: None, + quote_sell_amount: None, + quote_buy_amount: None, }, domain::fee::Policy::Volume { factor } => Self { auction_id, @@ -35,6 +43,22 @@ impl FeePolicy { surplus_factor: None, max_volume_factor: None, volume_factor: Some(factor), + quote_sell_amount: None, + quote_buy_amount: None, + }, + domain::fee::Policy::PriceImprovement { + factor, + max_volume_factor, + quote, + } => Self { + auction_id, + order_uid: boundary::database::byte_array::ByteArray(order_uid.0), + kind: FeePolicyKind::Surplus, + surplus_factor: Some(factor), + max_volume_factor: Some(max_volume_factor), + volume_factor: None, + quote_sell_amount: Some(u256_to_big_decimal("e.sell_amount)), + quote_buy_amount: Some(u256_to_big_decimal("e.buy_amount)), }, } } @@ -50,6 +74,20 @@ impl From for domain::fee::Policy { FeePolicyKind::Volume => domain::fee::Policy::Volume { factor: row.volume_factor.expect("missing volume factor"), }, + FeePolicyKind::PriceImprovement => domain::fee::Policy::PriceImprovement { + factor: row.surplus_factor.expect("missing surplus factor"), + max_volume_factor: row.max_volume_factor.expect("missing max volume factor"), + quote: domain::fee::Quote { + sell_amount: big_decimal_to_u256( + &row.quote_sell_amount.expect("missing sell amount"), + ) + .expect("sell amount is not a valid eth::U256"), + buy_amount: big_decimal_to_u256( + &row.quote_buy_amount.expect("missing buy amount"), + ) + .expect("buy amount is not a valid eth::U256"), + }, + }, } } } @@ -59,4 +97,5 @@ impl From for domain::fee::Policy { pub enum FeePolicyKind { Surplus, Volume, + PriceImprovement, } diff --git a/crates/autopilot/src/infra/persistence/dto/order.rs b/crates/autopilot/src/infra/persistence/dto/order.rs index bbb3a9efbe..4c171d31c0 100644 --- a/crates/autopilot/src/infra/persistence/dto/order.rs +++ b/crates/autopilot/src/infra/persistence/dto/order.rs @@ -268,9 +268,23 @@ pub enum FeePolicy { #[serde(rename_all = "camelCase")] Surplus { factor: f64, max_volume_factor: f64 }, #[serde(rename_all = "camelCase")] + PriceImprovement { + factor: f64, + max_volume_factor: f64, + quote: Quote, + }, + #[serde(rename_all = "camelCase")] Volume { factor: f64 }, } +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Quote { + pub sell_amount: U256, + pub buy_amount: U256, +} + impl From for FeePolicy { fn from(policy: domain::fee::Policy) -> Self { match policy { @@ -281,6 +295,18 @@ impl From for FeePolicy { factor, max_volume_factor, }, + domain::fee::Policy::PriceImprovement { + factor, + max_volume_factor, + quote, + } => Self::PriceImprovement { + factor, + max_volume_factor, + quote: Quote { + sell_amount: quote.sell_amount, + buy_amount: quote.buy_amount, + }, + }, domain::fee::Policy::Volume { factor } => Self::Volume { factor }, } } @@ -296,6 +322,18 @@ impl From for domain::fee::Policy { factor, max_volume_factor, }, + FeePolicy::PriceImprovement { + factor, + max_volume_factor, + quote, + } => Self::PriceImprovement { + factor, + max_volume_factor, + quote: domain::fee::Quote { + sell_amount: quote.sell_amount, + buy_amount: quote.buy_amount, + }, + }, FeePolicy::Volume { factor } => Self::Volume { factor }, } } diff --git a/crates/autopilot/src/run.rs b/crates/autopilot/src/run.rs index 056e7a3eec..1e1a1f2d0f 100644 --- a/crates/autopilot/src/run.rs +++ b/crates/autopilot/src/run.rs @@ -568,7 +568,7 @@ pub async fn run(args: Arguments) { .try_into() .expect("limit order price factor can't be converted to BigDecimal"), domain::ProtocolFee::new( - args.fee_policy.clone().to_domain(), + args.fee_policy.clone().to_policy_builder(), args.fee_policy.fee_policy_skip_market_orders, ), ); diff --git a/crates/autopilot/src/solvable_orders.rs b/crates/autopilot/src/solvable_orders.rs index c46aa9e7fa..bb4f4d20dd 100644 --- a/crates/autopilot/src/solvable_orders.rs +++ b/crates/autopilot/src/solvable_orders.rs @@ -1,5 +1,5 @@ use { - crate::{boundary, domain, infra}, + crate::{domain, infra}, anyhow::Result, bigdecimal::BigDecimal, database::order_events::OrderEventLabel, @@ -236,10 +236,13 @@ impl SolvableOrdersCache { latest_settlement_block: db_solvable_orders.latest_settlement_block, orders: orders .into_iter() - .map(|order| { - let quote = db_solvable_orders.quotes.get(&order.metadata.uid.into()); - let protocol_fees = self.protocol_fee.get(&order, quote); - boundary::order::to_domain(order, protocol_fees) + .filter_map(|order| { + if let Some(quote) = db_solvable_orders.quotes.get(&order.metadata.uid.into()) { + Some(self.protocol_fee.to_order(order, quote)) + } else { + tracing::warn!(order_uid = %order.metadata.uid, "order is skipped, quote is missing"); + None + } }) .collect(), prices, diff --git a/crates/driver/openapi.yml b/crates/driver/openapi.yml index 53ce926de3..0dc9ce6673 100644 --- a/crates/driver/openapi.yml +++ b/crates/driver/openapi.yml @@ -413,6 +413,7 @@ components: type: object oneOf: - $ref: "#/components/schemas/SurplusFee" + - $ref: "#/components/schemas/PriceImprovement" - $ref: "#/components/schemas/VolumeFee" SurplusFee: description: | @@ -430,6 +431,24 @@ components: description: The factor of the user surplus that the protocol will request from the solver after settling the order type: number example: 0.5 + PriceImprovement: + description: | + A cut from the price improvement over the best quote is taken as a protocol fee. + type: object + properties: + kind: + type: string + enum: [ "priceImprovement" ] + maxVolumeFactor: + description: Never charge more than that percentage of the order volume. + type: number + example: 0.1 + factor: + description: The factor of the user surplus that the protocol will request from the solver after settling the order + type: number + example: 0.5 + quote: + $ref: "#/components/schemas/Quote" VolumeFee: type: object properties: @@ -440,6 +459,13 @@ components: description: The fraction of the order's volume that the protocol will request from the solver after settling the order. type: number example: 0.5 + Quote: + type: object + properties: + sell_amount: + $ref: "#/components/schemas/TokenAmount" + buy_amount: + $ref: "#/components/schemas/TokenAmount" Error: description: Response on API errors. type: object diff --git a/crates/driver/src/domain/competition/order/fees.rs b/crates/driver/src/domain/competition/order/fees.rs index 0419a35c69..d745b5a7d9 100644 --- a/crates/driver/src/domain/competition/order/fees.rs +++ b/crates/driver/src/domain/competition/order/fees.rs @@ -1,3 +1,5 @@ +use crate::domain::eth; + #[derive(Clone, Debug)] pub enum FeePolicy { /// If the order receives more than limit price, take the protocol fee as a @@ -15,6 +17,14 @@ pub enum FeePolicy { /// Cap protocol fee with a percentage of the order's volume. max_volume_factor: f64, }, + /// A price improvement corresponds to a situation where the order is + /// executed at a better price than the top quote. The protocol fee in such + /// case is calculated from a cut of this price improvement. + PriceImprovement { + factor: f64, + max_volume_factor: f64, + quote: Quote, + }, /// 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. @@ -24,3 +34,9 @@ pub enum FeePolicy { factor: f64, }, } + +#[derive(Clone, Debug)] +pub struct Quote { + pub sell_amount: eth::U256, + pub buy_amount: eth::U256, +} diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index f9a468a9bf..644195f419 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -78,21 +78,52 @@ impl Fulfillment { Some(FeePolicy::Surplus { factor, max_volume_factor, + }) => self.calculate_fee( + self.order().sell.amount.0, + self.order().buy.amount.0, + prices, + *factor, + *max_volume_factor, + ), + Some(FeePolicy::PriceImprovement { + factor, + max_volume_factor, + quote, }) => { - let fee_from_surplus = self.fee_from_surplus(prices, *factor)?; - let fee_from_volume = self.fee_from_volume(prices, *max_volume_factor)?; - // take the smaller of the two - tracing::debug!(uid=?self.order().uid, fee_from_surplus=?fee_from_surplus, fee_from_volume=?fee_from_volume, protocol_fee=?(std::cmp::min(fee_from_surplus, fee_from_volume)), executed=?self.executed(), surplus_fee=?self.surplus_fee(), "calculated protocol fee"); - Ok(std::cmp::min(fee_from_surplus, fee_from_volume)) + let sell_amount = quote.sell_amount.max(self.order().sell.amount.0); + let buy_amount = quote.buy_amount.min(self.order().buy.amount.0); + self.calculate_fee(sell_amount, buy_amount, prices, *factor, *max_volume_factor) } Some(FeePolicy::Volume { factor }) => self.fee_from_volume(prices, *factor), None => Ok(0.into()), } } - fn fee_from_surplus(&self, prices: ClearingPrices, factor: f64) -> Result { - let sell_amount = self.order().sell.amount.0; - let buy_amount = self.order().buy.amount.0; + /// Computes protocol fee compared to the given reference amounts taken from + /// the order or a quote. + fn calculate_fee( + &self, + reference_sell_amount: eth::U256, + reference_buy_amount: eth::U256, + prices: ClearingPrices, + factor: f64, + max_volume_factor: f64, + ) -> Result { + let fee_from_surplus = + self.fee_from_surplus(reference_sell_amount, reference_buy_amount, prices, factor)?; + let fee_from_volume = self.fee_from_volume(prices, max_volume_factor)?; + // take the smaller of the two + tracing::debug!(uid=?self.order().uid, fee_from_surplus=?fee_from_surplus, fee_from_volume=?fee_from_volume, protocol_fee=?(std::cmp::min(fee_from_surplus, fee_from_volume)), executed=?self.executed(), surplus_fee=?self.surplus_fee(), "calculated protocol fee"); + Ok(std::cmp::min(fee_from_surplus, fee_from_volume)) + } + + fn fee_from_surplus( + &self, + sell_amount: eth::U256, + buy_amount: eth::U256, + prices: ClearingPrices, + factor: f64, + ) -> Result { let executed = self.executed().0; let executed_sell_amount = match self.order().side { Side::Buy => { 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 c863e01c04..31280bb07a 100644 --- a/crates/driver/src/infra/api/routes/solve/dto/auction.rs +++ b/crates/driver/src/infra/api/routes/solve/dto/auction.rs @@ -123,6 +123,18 @@ impl Auction { factor, max_volume_factor, }, + FeePolicy::PriceImprovement { + factor, + max_volume_factor, + quote, + } => competition::order::FeePolicy::PriceImprovement { + factor, + max_volume_factor, + quote: competition::order::fees::Quote { + sell_amount: quote.sell_amount, + buy_amount: quote.buy_amount, + }, + }, FeePolicy::Volume { factor } => { competition::order::FeePolicy::Volume { factor } } @@ -306,5 +318,18 @@ enum FeePolicy { #[serde(rename_all = "camelCase")] Surplus { factor: f64, max_volume_factor: f64 }, #[serde(rename_all = "camelCase")] + PriceImprovement { + factor: f64, + max_volume_factor: f64, + quote: Quote, + }, + #[serde(rename_all = "camelCase")] Volume { factor: f64 }, } + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Quote { + pub sell_amount: eth::U256, + pub buy_amount: eth::U256, +} diff --git a/crates/e2e/tests/e2e/protocol_fee.rs b/crates/e2e/tests/e2e/protocol_fee.rs index 0016077ccf..122b01b95a 100644 --- a/crates/e2e/tests/e2e/protocol_fee.rs +++ b/crates/e2e/tests/e2e/protocol_fee.rs @@ -49,6 +49,30 @@ async fn local_node_volume_fee_buy_order() { run_test(volume_fee_buy_order_test).await; } +#[tokio::test] +#[ignore] +async fn local_node_price_improvement_fee_sell_order() { + run_test(price_improvement_fee_sell_order_test).await; +} + +#[tokio::test] +#[ignore] +async fn local_node_price_improvement_fee_sell_order_capped() { + run_test(price_improvement_fee_sell_order_capped_test).await; +} + +#[tokio::test] +#[ignore] +async fn local_node_price_improvement_fee_buy_order() { + run_test(price_improvement_fee_buy_order_test).await; +} + +#[tokio::test] +#[ignore] +async fn local_node_price_improvement_fee_buy_order_capped() { + run_test(price_improvement_fee_buy_order_capped_test).await; +} + async fn surplus_fee_sell_order_test(web3: Web3) { let fee_policy = FeePolicyKind::Surplus { factor: 0.3, @@ -206,6 +230,120 @@ async fn volume_fee_buy_order_test(web3: Web3) { .await; } +async fn price_improvement_fee_sell_order_test(web3: Web3) { + let fee_policy = FeePolicyKind::PriceImprovement { + factor: 0.3, + max_volume_factor: 1.0, + }; + // Without protocol fee: + // Expected execution is 10000000000000000000 GNO for + // 9871415430342266811 DAI, with executed_surplus_fee = 167058994203399 GNO + // + // With protocol fee: + // surplus [DAI] = 9871415430342266811 DAI - 5000000000000000000 DAI = + // 4871415430342266811 DAI + // + // protocol fee = 0.3*surplus = 1461424629102680043 DAI = + // 1461424629102680043 DAI / 9871415430342266811 * + // (10000000000000000000 - 167058994203399) = 1480436341679873337 GNO + // + // final execution is 10000000000000000000 GNO for 8409990801239586768 DAI, with + // executed_surplus_fee = 1480603400674076736 GNO + // + // Settlement contract balance after execution = 1480603400674076736 GNO = + // 1480603400674076736 GNO * 8409990801239586768 / (10000000000000000000 - + // 1480603400674076736) = 1461589542731026166 DAI + execute_test( + web3.clone(), + fee_policy, + OrderKind::Sell, + 1480603400674076736u128.into(), + 1461589542731026166u128.into(), + ) + .await; +} + +async fn price_improvement_fee_sell_order_capped_test(web3: Web3) { + let fee_policy = FeePolicyKind::PriceImprovement { + factor: 1.0, + max_volume_factor: 0.1, + }; + // Without protocol fee: + // Expected executed_surplus_fee is 167058994203399 + // + // With protocol fee: + // Expected executed_surplus_fee is 167058994203399 + + // 0.1*10000000000000000000 = 1000167058994203400 + // + // Final execution is 10000000000000000000 GNO for 8884257395945205588 DAI, with + // executed_surplus_fee = 1000167058994203400 GNO + // + // Settlement contract balance after execution = 1000167058994203400 GNO = + // 1000167058994203400 GNO * 8884257395945205588 / (10000000000000000000 - + // 1000167058994203400) = 987322948025407485 DAI + execute_test( + web3.clone(), + fee_policy, + OrderKind::Sell, + 1000167058994203400u128.into(), + 987322948025407485u128.into(), + ) + .await; +} + +async fn price_improvement_fee_buy_order_test(web3: Web3) { + let fee_policy = FeePolicyKind::PriceImprovement { + factor: 0.3, + max_volume_factor: 1.0, + }; + // Without protocol fee: + // Expected execution is 5040413426236634210 GNO for 5000000000000000000 DAI, + // with executed_surplus_fee = 167058994203399 GNO + // + // With protocol fee: + // surplus in sell token = 10000000000000000000 - 5040413426236634210 = + // 4959586573763365790 + // + // protocol fee in sell token = 0.3*4959586573763365790 = 1487875972129009737 + // + // expected executed_surplus_fee is 167058994203399 + 1487875972129009737 = + // 1488043031123213136 + // + // Settlement contract balance after execution = executed_surplus_fee GNO + execute_test( + web3.clone(), + fee_policy, + OrderKind::Buy, + 1488043031123213136u128.into(), + 1488043031123213136u128.into(), + ) + .await; +} + +async fn price_improvement_fee_buy_order_capped_test(web3: Web3) { + let fee_policy = FeePolicyKind::PriceImprovement { + factor: 1.0, + max_volume_factor: 0.1, + }; + // Without protocol fee: + // Expected execution is 5040413426236634210 GNO for 5000000000000000000 DAI, + // with executed_surplus_fee = 167058994203399 GNO + // + // With protocol fee: + // Expected executed_surplus_fee is 167058994203399 + 0.1*5040413426236634210 = + // 504208401617866820 + // + // Settlement contract balance after execution = executed_surplus_fee GNO + execute_test( + web3.clone(), + fee_policy, + OrderKind::Buy, + 504208401617866820u128.into(), + 504208401617866820u128.into(), + ) + .await; +} + // because of rounding errors, it's good enough to check that the expected value // is within a very narrow range of the executed value fn is_approximately_equal(executed_value: U256, expected_value: U256) -> bool { @@ -358,6 +496,9 @@ async fn execute_test( enum FeePolicyKind { /// How much of the order's surplus should be taken as a protocol fee. Surplus { factor: f64, max_volume_factor: f64 }, + /// How much of the order's price improvement 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 }, } @@ -373,6 +514,14 @@ impl std::fmt::Display for FeePolicyKind { "--fee-policy-kind=surplus:{}:{}", factor, max_volume_factor ), + FeePolicyKind::PriceImprovement { + factor, + max_volume_factor, + } => write!( + f, + "--fee-policy-kind=price-improvement:{}:{}", + factor, max_volume_factor + ), FeePolicyKind::Volume { factor } => { write!(f, "--fee-policy-kind=volume:{}", factor) } diff --git a/crates/orderbook/openapi.yml b/crates/orderbook/openapi.yml index ff9dbdff26..08d9cd8311 100644 --- a/crates/orderbook/openapi.yml +++ b/crates/orderbook/openapi.yml @@ -1561,6 +1561,13 @@ components: items: $ref: "#/components/schemas/CallData" description: The call data to be used for the interaction. + Quote: + type: object + properties: + sell_amount: + type: number + buy_amount: + type: number Surplus: description: The protocol fee is taken as a percent of the surplus. type: object @@ -1572,6 +1579,20 @@ components: required: - factor - max_volume_factor + PriceImprovement: + description: The protocol fee is taken as a percent of the surplus which is based on a quote. + type: object + properties: + factor: + type: number + max_volume_factor: + type: number + quote: + $ref: "#/components/schemas/Quote" + required: + - factor + - max_volume_factor + - quote Volume: description: The protocol fee is taken as a percent of the order volume. type: object @@ -1584,4 +1605,5 @@ components: description: Defines the ways to calculate the protocol fee. oneOf: - $ref: '#/components/schemas/Surplus' + - $ref: '#/components/schemas/PriceImprovement' - $ref: '#/components/schemas/Volume' diff --git a/crates/orderbook/src/dto/order.rs b/crates/orderbook/src/dto/order.rs index a50704125e..d46e1dda01 100644 --- a/crates/orderbook/src/dto/order.rs +++ b/crates/orderbook/src/dto/order.rs @@ -51,4 +51,20 @@ pub enum FeePolicy { Surplus { factor: f64, max_volume_factor: f64 }, #[serde(rename_all = "camelCase")] Volume { factor: f64 }, + #[serde(rename_all = "camelCase")] + PriceImprovement { + factor: f64, + max_volume_factor: f64, + quote: Quote, + }, +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Quote { + #[serde_as(as = "HexOrDecimalU256")] + pub sell_amount: U256, + #[serde_as(as = "HexOrDecimalU256")] + pub buy_amount: U256, } diff --git a/database/README.md b/database/README.md index d49518963a..c24b0ecad7 100644 --- a/database/README.md +++ b/database/README.md @@ -239,15 +239,17 @@ Indexes: Contains all relevant data of fee policies applied to orders during auctions. -Column | Type | Nullable | Details ---------------------------|------------------------------|----------|-------- - auction_id | bigint | not null | unique identifier for the auction - order_uid | bytea | not null | 56 bytes identifier linking to the order in the `orders` table - application_order | serial | not null | the order in which the fee policies are inserted and applied - kind | [PolicyKind](#policykind) | not null | type of the fee policy, defined in the PolicyKind enum - surplus_factor | double precision | | percentage of the surplus for fee calculation; value is between 0 and 1 - max_volume_factor | double precision | | cap for the fee as a percentage of the order volume; value is between 0 and 1 - volume_factor | double precision | | fee percentage of the order volume; value is between 0 and 1 +Column | Type | Nullable | Details +--------------------------|---------------------------|----------|-------- + auction_id | bigint | not null | unique identifier for the auction + order_uid | bytea | not null | 56 bytes identifier linking to the order in the `orders` table + application_order | serial | not null | the order in which the fee policies are inserted and applied + kind | [PolicyKind](#policykind) | not null | type of the fee policy, defined in the PolicyKind enum + surplus_factor | double precision | | percentage of the surplus for fee calculation; value is between 0 and 1 + max_volume_factor | double precision | | cap for the fee as a percentage of the order volume; value is between 0 and 1 + volume_factor | double precision | | fee percentage of the order volume; value is between 0 and 1 + quote_sell_amount | numeric | | quote's sell amount + quote_buy_amount | numeric | | quote's buy amount Indexes: - PRIMARY KEY: composite key(`auction_id`, `order_uid`, `application_order`) @@ -260,6 +262,7 @@ Indexes: Values: - `surplus`: The fee is based on the surplus achieved in the trade. - `volume`: The fee is based on the volume of the order. + - `priceimprovement`: The fee is based on the difference of the order's execution price and the top quote. ### presignature\_events diff --git a/database/sql/V062__add_price_improvement_fee_policy_kind.sql b/database/sql/V062__add_price_improvement_fee_policy_kind.sql new file mode 100644 index 0000000000..7c9f130118 --- /dev/null +++ b/database/sql/V062__add_price_improvement_fee_policy_kind.sql @@ -0,0 +1,7 @@ +ALTER TYPE PolicyKind ADD VALUE 'priceimprovement'; + +ALTER TABLE fee_policies + -- quote's sell amount + ADD COLUMN quote_sell_amount numeric(78,0), + -- quote's buy amount + ADD COLUMN quote_buy_amount numeric(78,0);