Skip to content

Commit

Permalink
Protocol fee adjustment in driver (#2213)
Browse files Browse the repository at this point in the history
# Description
Replacement PR for #2097

As @fleupold suggested, protocol fee adjustments are done fully in
`driver`, in class `Fulfillment`. It goes like this:
1. Solver responds with solution (accounts for surplus_fee only)
2. Driver creates the fulfillment (executed amount + surplus_fee) based
on (1)
3. Driver adjusts fulfillment for protocol fees. It
- calculates protocol fee
- reduces executed amount by protocol fee.
(note that this is basically the same as increasing `surplus_fee`)
4. Driver encodes the fulfillment using the `SettlementEncoder`
functionality, meaning custom price adjustments will be reused for
protocol fee also.

## How to test
Added unit tests for
1. sell limit order fok
2. buy limit order fok
3. sell limit order partial (with 3 fulfillments)
(but now removed to keep the domain clean)
  • Loading branch information
sunce86 authored Jan 11, 2024
1 parent 197fdf5 commit 04b18f6
Show file tree
Hide file tree
Showing 10 changed files with 672 additions and 39 deletions.
218 changes: 218 additions & 0 deletions crates/driver/src/domain/competition/solution/fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
//! Applies the protocol fee to the solution received from the solver.
//!
//! Solvers respond differently for the sell and buy orders.
//!
//! EXAMPLES:
//!
//! SELL ORDER
//! Selling 1 WETH for at least `x` amount of USDC. Solvers respond with
//! Fee = 0.05 WETH (always expressed in sell token)
//! Executed = 0.95 WETH (always expressed in target token)
//!
//! This response is adjusted by the protocol fee of 0.1 WETH:
//! Fee = 0.05 WETH + 0.1 WETH = 0.15 WETH
//! Executed = 0.95 WETH - 0.1 WETH = 0.85 WETH
//!
//! BUY ORDER
//! Buying 1 WETH for at most `x` amount of USDC. Solvers respond with
//! Fee = 10 USDC (always expressed in sell token)
//! Executed = 1 WETH (always expressed in target token)
//!
//! This response is adjusted by the protocol fee of 5 USDC:
//! Fee = 10 USDC + 5 USDC = 15 USDC
//! Executed = 1 WETH
use {
super::trade::{Fee, Fulfillment, InvalidExecutedAmount},
crate::domain::{
competition::{
order,
order::{FeePolicy, Side},
},
eth,
},
};

impl Fulfillment {
/// Applies the protocol fee to the existing fulfillment creating a new one.
pub fn with_protocol_fee(&self, prices: ClearingPrices) -> Result<Self, Error> {
let protocol_fee = self.protocol_fee(prices)?;

// Increase the fee by the protocol fee
let fee = match self.surplus_fee() {
None => {
if !protocol_fee.is_zero() {
return Err(Error::ProtocolFeeOnStaticOrder);
}
Fee::Static
}
Some(fee) => {
Fee::Dynamic((fee.0.checked_add(protocol_fee).ok_or(Error::Overflow)?).into())
}
};

// Reduce the executed amount by the protocol fee. This is because solvers are
// unaware of the protocol fee that driver introduces and they only account
// for their own fee.
let order = self.order().clone();
let executed = match order.side {
order::Side::Buy => self.executed(),
order::Side::Sell => order::TargetAmount(
self.executed()
.0
.checked_sub(protocol_fee)
.ok_or(Error::Overflow)?,
),
};

Fulfillment::new(order, executed, fee).map_err(Into::into)
}

fn protocol_fee(&self, prices: ClearingPrices) -> Result<eth::U256, Error> {
// TODO: support multiple fee policies
if self.order().fee_policies.len() > 1 {
return Err(Error::MultipleFeePolicies);
}

match self.order().fee_policies.first() {
Some(FeePolicy::PriceImprovement {
factor,
max_volume_factor,
}) => {
let price_improvement_fee = self.price_improvement_fee(prices, *factor)?;
let max_volume_fee = self.volume_fee(prices, *max_volume_factor)?;
// take the smaller of the two
tracing::debug!(uid=?self.order().uid, price_improvement_fee=?price_improvement_fee, max_volume_fee=?max_volume_fee, protocol_fee=?(std::cmp::min(price_improvement_fee, max_volume_fee)), executed=?self.executed(), surplus_fee=?self.surplus_fee(), "calculated protocol fee");
Ok(std::cmp::min(price_improvement_fee, max_volume_fee))
}
Some(FeePolicy::Volume { factor }) => self.volume_fee(prices, *factor),
None => Ok(0.into()),
}
}

fn price_improvement_fee(
&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;
let executed = self.executed().0;
let executed_sell_amount = match self.order().side {
Side::Buy => {
// How much `sell_token` we need to sell to buy `executed` amount of `buy_token`
executed
.checked_mul(prices.buy)
.ok_or(Error::Overflow)?
.checked_div(prices.sell)
.ok_or(Error::DivisionByZero)?
}
Side::Sell => executed,
};
// Sell slightly more `sell_token` to capture the `surplus_fee`
let executed_sell_amount_with_fee = executed_sell_amount
.checked_add(
// surplus_fee is always expressed in sell token
self.surplus_fee()
.map(|fee| fee.0)
.ok_or(Error::ProtocolFeeOnStaticOrder)?,
)
.ok_or(Error::Overflow)?;
let surplus_in_sell_token = match self.order().side {
Side::Buy => {
// Scale to support partially fillable orders
let limit_sell_amount = sell_amount
.checked_mul(executed)
.ok_or(Error::Overflow)?
.checked_div(buy_amount)
.ok_or(Error::DivisionByZero)?;
// Remaining surplus after fees
// Do not return error if `checked_sub` fails because violated limit prices will
// be caught by simulation
limit_sell_amount
.checked_sub(executed_sell_amount_with_fee)
.unwrap_or(eth::U256::zero())
}
Side::Sell => {
// Scale to support partially fillable orders
let limit_buy_amount = buy_amount
.checked_mul(executed_sell_amount_with_fee)
.ok_or(Error::Overflow)?
.checked_div(sell_amount)
.ok_or(Error::DivisionByZero)?;
// How much `buy_token` we get for `executed` amount of `sell_token`
let executed_buy_amount = executed
.checked_mul(prices.sell)
.ok_or(Error::Overflow)?
.checked_div(prices.buy)
.ok_or(Error::DivisionByZero)?;
// Remaining surplus after fees
// Do not return error if `checked_sub` fails because violated limit prices will
// be caught by simulation
let surplus = executed_buy_amount
.checked_sub(limit_buy_amount)
.unwrap_or(eth::U256::zero());
// surplus in sell token
surplus
.checked_mul(prices.buy)
.ok_or(Error::Overflow)?
.checked_div(prices.sell)
.ok_or(Error::DivisionByZero)?
}
};
apply_factor(surplus_in_sell_token, factor)
}

fn volume_fee(&self, prices: ClearingPrices, factor: f64) -> Result<eth::U256, Error> {
let executed = self.executed().0;
let executed_sell_amount = match self.order().side {
Side::Buy => {
// How much `sell_token` we need to sell to buy `executed` amount of `buy_token`
executed
.checked_mul(prices.buy)
.ok_or(Error::Overflow)?
.checked_div(prices.sell)
.ok_or(Error::DivisionByZero)?
}
Side::Sell => executed,
};
// Sell slightly more `sell_token` to capture the `surplus_fee`
let executed_sell_amount_with_fee = executed_sell_amount
.checked_add(
// surplus_fee is always expressed in sell token
self.surplus_fee()
.map(|fee| fee.0)
.ok_or(Error::ProtocolFeeOnStaticOrder)?,
)
.ok_or(Error::Overflow)?;
apply_factor(executed_sell_amount_with_fee, factor)
}
}

fn apply_factor(amount: eth::U256, factor: f64) -> Result<eth::U256, Error> {
Ok(amount
.checked_mul(eth::U256::from_f64_lossy(factor * 10000.))
.ok_or(Error::Overflow)?
/ 10000)
}

/// Uniform clearing prices at which the trade was executed.
#[derive(Debug, Clone, Copy)]
pub struct ClearingPrices {
pub sell: eth::U256,
pub buy: eth::U256,
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("orders with non solver determined gas cost fees are not supported")]
ProtocolFeeOnStaticOrder,
#[error("multiple fee policies are not supported yet")]
MultipleFeePolicies,
#[error("overflow error while calculating protocol fee")]
Overflow,
#[error("division by zero error while calculating protocol fee")]
DivisionByZero,
#[error(transparent)]
InvalidExecutedAmount(#[from] InvalidExecutedAmount),
}
43 changes: 35 additions & 8 deletions crates/driver/src/domain/competition/solution/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use {
self::fee::ClearingPrices,
crate::{
boundary,
domain::{
Expand All @@ -18,6 +19,7 @@ use {
thiserror::Error,
};

pub mod fee;
pub mod interaction;
pub mod settlement;
pub mod trade;
Expand Down Expand Up @@ -49,7 +51,7 @@ impl Solution {
solver: Solver,
score: SolverScore,
weth: eth::WethAddress,
) -> Result<Self, InvalidClearingPrices> {
) -> Result<Self, SolutionError> {
let solution = Self {
id,
trades,
Expand All @@ -65,9 +67,9 @@ impl Solution {
solution.clearing_price(trade.order().sell.token).is_some()
&& solution.clearing_price(trade.order().buy.token).is_some()
}) {
Ok(solution)
Ok(solution.with_protocol_fees()?)
} else {
Err(InvalidClearingPrices)
Err(SolutionError::InvalidClearingPrices)
}
}

Expand Down Expand Up @@ -176,6 +178,29 @@ impl Solution {
Settlement::encode(self, auction, eth, simulator).await
}

pub fn with_protocol_fees(self) -> Result<Self, fee::Error> {
let mut trades = Vec::with_capacity(self.trades.len());
for trade in self.trades {
match &trade {
Trade::Fulfillment(fulfillment) => match fulfillment.order().kind {
order::Kind::Market | order::Kind::Limit { .. } => {
let prices = ClearingPrices {
sell: self.prices[&fulfillment.order().sell.token.wrap(self.weth)],
buy: self.prices[&fulfillment.order().buy.token.wrap(self.weth)],
};
let fulfillment = fulfillment.with_protocol_fee(prices)?;
trades.push(Trade::Fulfillment(fulfillment))
}
order::Kind::Liquidity => {
trades.push(trade);
}
},
Trade::Jit(_) => trades.push(trade),
}
}
Ok(Self { trades, ..self })
}

/// Token prices settled by this solution, expressed using an arbitrary
/// reference unit chosen by the solver. These values are only
/// meaningful in relation to each others.
Expand Down Expand Up @@ -279,8 +304,6 @@ pub enum Error {
Boundary(#[from] boundary::Error),
#[error("simulation error: {0:?}")]
Simulation(#[from] simulator::Error),
#[error(transparent)]
Execution(#[from] trade::ExecutionError),
#[error(
"non bufferable tokens used: solution attempts to internalize tokens which are not trusted"
)]
Expand All @@ -293,6 +316,10 @@ pub enum Error {
DifferentSolvers,
}

#[derive(Debug, Error)]
#[error("invalid clearing prices")]
pub struct InvalidClearingPrices;
#[derive(Debug, thiserror::Error)]
pub enum SolutionError {
#[error("invalid clearing prices")]
InvalidClearingPrices,
#[error(transparent)]
ProtocolFee(#[from] fee::Error),
}
28 changes: 13 additions & 15 deletions crates/driver/src/domain/competition/solution/trade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,19 @@ impl Fulfillment {
// the target amount. Otherwise, the executed amount must be equal to the target
// amount.
let valid_execution = {
let surplus_fee = match order.side {
let fee = match order.side {
order::Side::Buy => order::TargetAmount::default(),
order::Side::Sell => order::TargetAmount(match fee {
Fee::Static => eth::U256::default(),
Fee::Dynamic(fee) => fee.0,
}),
};

let executed_with_fee =
order::TargetAmount(executed.0.checked_add(fee.0).ok_or(InvalidExecutedAmount)?);
match order.partial {
order::Partial::Yes { available } => {
order::TargetAmount(executed.0 + surplus_fee.0) <= available
}
order::Partial::No => {
order::TargetAmount(executed.0 + surplus_fee.0) == order.target()
}
order::Partial::Yes { available } => executed_with_fee <= available,
order::Partial::No => executed_with_fee == order.target(),
}
};

Expand Down Expand Up @@ -97,6 +95,14 @@ impl Fulfillment {
}
}

/// Returns the solver determined fee if it exists.
pub fn surplus_fee(&self) -> Option<order::SellAmount> {
match self.fee {
Fee::Static => None,
Fee::Dynamic(fee) => Some(fee),
}
}

/// The effective amount that left the user's wallet including all fees.
pub fn sell_amount(
&self,
Expand Down Expand Up @@ -195,11 +201,3 @@ pub struct Execution {
#[derive(Debug, thiserror::Error)]
#[error("invalid executed amount")]
pub struct InvalidExecutedAmount;

#[derive(Debug, thiserror::Error)]
pub enum ExecutionError {
#[error("overflow error while calculating executed amounts")]
Overflow,
#[error("missing clearing price for {0:?}")]
ClearingPriceMissing(eth::TokenAddress),
}
1 change: 0 additions & 1 deletion crates/driver/src/infra/notify/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ pub fn encoding_failed(
simulation_failed(solver, auction_id, solution_id, error, false);
return;
}
solution::Error::Execution(_) => return,
solution::Error::FailingInternalization => return,
solution::Error::DifferentSolvers => return,
};
Expand Down
2 changes: 1 addition & 1 deletion crates/driver/src/infra/solver/dto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ pub use {auction::Auction, notification::Notification, solution::Solutions};

#[derive(Debug, thiserror::Error)]
#[error("{0}")]
pub struct Error(pub &'static str);
pub struct Error(pub String);
Loading

0 comments on commit 04b18f6

Please sign in to comment.