Skip to content

Commit

Permalink
Price improvement fee policy in driver (#2375)
Browse files Browse the repository at this point in the history
# Description
Closes task n3 from the
#2287

## How to test
TBD

---------

Co-authored-by: Martin Beckmann <martin.beckmann@protonmail.com>
  • Loading branch information
squadgazzz and MartinquaXD authored Feb 12, 2024
1 parent 11ec759 commit 72cac8c
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 8 deletions.
26 changes: 26 additions & 0 deletions crates/driver/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ components:
type: object
oneOf:
- $ref: "#/components/schemas/SurplusFee"
- $ref: "#/components/schemas/PriceImprovement"
- $ref: "#/components/schemas/VolumeFee"
SurplusFee:
description: |
Expand All @@ -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.01
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:
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions crates/driver/src/domain/competition/order/fees.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,6 +17,25 @@ 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 as a percentage of this price improvement.
PriceImprovement {
/// Price improvement is the difference between executed price and the
/// best quote or limit price, whichever is better for the user.
///
/// E.g. if a user received 2000USDC for 1ETH while having a best quote
/// of 1995USDC and limit price of 1990USDC, their surplus is 10USDC
/// while the price improvement is 5USDC. A factor of 0.1 requires the
/// solver to pay 0.5USDC to the protocol for settling this order. In
/// case the best quote was 1990USDC while the limit price was 1995USDC,
/// the solver should also pay 0.5USDC to the protocol.
factor: f64,
/// Cap protocol fee with a percentage of the order's volume.
max_volume_factor: f64,
/// The best quote received.
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.
Expand All @@ -24,3 +45,10 @@ pub enum FeePolicy {
factor: f64,
},
}

#[derive(Clone, Debug)]
pub struct Quote {
pub sell: eth::Asset,
pub buy: eth::Asset,
pub fee: eth::Asset,
}
204 changes: 196 additions & 8 deletions crates/driver/src/domain/competition/solution/fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,59 @@ 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, buy_amount) = adjusted_price_improvement_amounts(
self.order().sell.amount.0,
self.order().buy.amount.0,
self.order().side,
quote.sell.amount.0,
quote.buy.amount.0,
quote.fee.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<eth::U256, Error> {
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<eth::U256, Error> {
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
let protocol_fee = std::cmp::min(fee_from_surplus, fee_from_volume);
tracing::debug!(uid=?self.order().uid, ?fee_from_surplus, ?fee_from_volume, ?protocol_fee, executed=?self.executed(), surplus_fee=?self.surplus_fee(), "calculated protocol fee");
Ok(protocol_fee)
}

fn fee_from_surplus(
&self,
sell_amount: eth::U256,
buy_amount: eth::U256,
prices: ClearingPrices,
factor: f64,
) -> Result<eth::U256, Error> {
let executed = self.executed().0;
let executed_sell_amount = match self.order().side {
Side::Buy => {
Expand Down Expand Up @@ -192,6 +230,40 @@ fn apply_factor(amount: eth::U256, factor: f64) -> Result<eth::U256, Error> {
/ 10000)
}

fn adjusted_price_improvement_amounts(
order_sell_amount: eth::U256,
order_buy_amount: eth::U256,
order_side: Side,
quote_sell_amount: eth::U256,
quote_buy_amount: eth::U256,
quote_fee_amount: eth::U256,
) -> Result<(eth::U256, eth::U256), Error> {
let quote_sell_amount = quote_sell_amount
.checked_add(quote_fee_amount)
.ok_or(Error::Overflow)?;

match order_side {
Side::Sell => {
let scaled_buy_amount = quote_buy_amount
.checked_mul(order_sell_amount)
.ok_or(Error::Overflow)?
.checked_div(quote_sell_amount)
.ok_or(Error::DivisionByZero)?;
let buy_amount = order_buy_amount.max(scaled_buy_amount);
Ok((order_sell_amount, buy_amount))
}
Side::Buy => {
let scaled_sell_amount = quote_sell_amount
.checked_mul(order_buy_amount)
.ok_or(Error::Overflow)?
.checked_div(quote_buy_amount)
.ok_or(Error::DivisionByZero)?;
let sell_amount = order_sell_amount.min(scaled_sell_amount);
Ok((sell_amount, order_buy_amount))
}
}
}

/// Uniform clearing prices at which the trade was executed.
#[derive(Debug, Clone, Copy)]
pub struct ClearingPrices {
Expand All @@ -212,3 +284,119 @@ pub enum Error {
#[error(transparent)]
InvalidExecutedAmount(#[from] InvalidExecutedAmount),
}

// todo: should be removed once integration tests are implemented
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_adjusted_price_improvement_amounts_for_sell_order() {
let order_sell_amount = to_wei(20);
let order_buy_amount = to_wei(19);
let quote_sell_amount = to_wei(21);
let quote_buy_amount = to_wei(18);
let quote_fee_amount = to_wei(1);

let (sell_amount, _) = adjusted_price_improvement_amounts(
order_sell_amount,
order_buy_amount,
Side::Sell,
quote_sell_amount,
quote_buy_amount,
quote_fee_amount,
)
.unwrap();

assert_eq!(
sell_amount, order_sell_amount,
"Sell amount should match order sell amount for sell orders."
);
}

#[test]
fn test_adjusted_price_improvement_amounts_for_buy_order() {
let order_sell_amount = to_wei(20);
let order_buy_amount = to_wei(19);
let quote_sell_amount = to_wei(21);
let quote_buy_amount = to_wei(18);
let quote_fee_amount = to_wei(1);

let (_, buy_amount) = adjusted_price_improvement_amounts(
order_sell_amount,
order_buy_amount,
Side::Buy,
quote_sell_amount,
quote_buy_amount,
quote_fee_amount,
)
.unwrap();

assert_eq!(
buy_amount, order_buy_amount,
"Buy amount should match order buy amount for buy orders."
);
}

#[test]
fn test_sell_order_with_quote_in_market_price() {
let order_sell_amount = to_wei(10);
let order_buy_amount = to_wei(20);
let quote_sell_amount = to_wei(9);
let quote_buy_amount = to_wei(25);
let quote_fee_amount = to_wei(1);

let (sell_amount, buy_amount) = adjusted_price_improvement_amounts(
order_sell_amount,
order_buy_amount,
Side::Sell,
quote_sell_amount,
quote_buy_amount,
quote_fee_amount,
)
.unwrap();

assert_eq!(
sell_amount, order_sell_amount,
"Sell amount should be taken from the quote for sell orders in market price."
);
assert_eq!(
buy_amount, quote_buy_amount,
"Buy amount should reflect the improved market condition from the quote."
);
}

#[test]
fn test_buy_order_with_quote_in_market_price() {
let order_sell_amount = to_wei(20);
let order_buy_amount = to_wei(10);
let quote_sell_amount = to_wei(17);
let quote_buy_amount = to_wei(10);
let quote_fee_amount = to_wei(1);

let (sell_amount, buy_amount) = adjusted_price_improvement_amounts(
order_sell_amount,
order_buy_amount,
Side::Buy,
quote_sell_amount,
quote_buy_amount,
quote_fee_amount,
)
.unwrap();

assert_eq!(
sell_amount,
quote_sell_amount + quote_fee_amount,
"Sell amount should reflect the improved market condition from the quote for buy \
orders."
);
assert_eq!(
buy_amount, order_buy_amount,
"Buy amount should be taken from the quote for buy orders in market price."
);
}

pub fn to_wei(base: u32) -> eth::U256 {
eth::U256::from(base) * eth::U256::exp10(18)
}
}
46 changes: 46 additions & 0 deletions crates/driver/src/infra/api/routes/solve/dto/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ impl Auction {
factor,
max_volume_factor,
},
FeePolicy::PriceImprovement {
factor,
max_volume_factor,
quote,
} => competition::order::FeePolicy::PriceImprovement {
factor,
max_volume_factor,
quote: quote.into_domain(order.sell_token, order.buy_token),
},
FeePolicy::Volume { factor } => {
competition::order::FeePolicy::Volume { factor }
}
Expand Down Expand Up @@ -306,5 +315,42 @@ 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,
pub fee: eth::U256,
}

impl Quote {
fn into_domain(
self,
sell_token: eth::H160,
buy_token: eth::H160,
) -> competition::order::fees::Quote {
competition::order::fees::Quote {
sell: eth::Asset {
amount: eth::TokenAmount(self.sell_amount),
token: eth::TokenAddress(eth::ContractAddress(sell_token)),
},
buy: eth::Asset {
amount: eth::TokenAmount(self.buy_amount),
token: eth::TokenAddress(eth::ContractAddress(buy_token)),
},
fee: eth::Asset {
amount: eth::TokenAmount(self.fee),
token: eth::TokenAddress(eth::ContractAddress(sell_token)),
},
}
}
}

0 comments on commit 72cac8c

Please sign in to comment.