Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protocol fee adjustment in driver #2213

Merged
merged 38 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
bc6df24
Protocol fee calculation
sunce86 Dec 25, 2023
36443b9
sell limit orders good
sunce86 Dec 26, 2023
5acaaf3
buy limit order example
sunce86 Dec 26, 2023
ffe84f9
pretty format
sunce86 Dec 26, 2023
950e747
fix comments
sunce86 Dec 26, 2023
6db55cd
added unit test for partial limit order
sunce86 Dec 26, 2023
1de0456
wrap eth
sunce86 Dec 26, 2023
9d5abd6
volume based fee
sunce86 Dec 26, 2023
06da3ef
zero surplus is ok
sunce86 Dec 26, 2023
1bff251
add protocol fee to static also
sunce86 Dec 26, 2023
04c1c5a
fee policies for limit orders only
sunce86 Dec 26, 2023
42b442d
revert static
sunce86 Dec 26, 2023
bfd38a1
fix previous
sunce86 Dec 26, 2023
3176004
refactor
sunce86 Dec 27, 2023
121cb95
apply fees
sunce86 Dec 27, 2023
a59be01
reverts
sunce86 Dec 27, 2023
60d200a
further refactor
sunce86 Dec 28, 2023
15bccce
refactor clearing prices
sunce86 Dec 28, 2023
41d8b2f
error handling
sunce86 Dec 28, 2023
0b135aa
cr fixes
sunce86 Dec 29, 2023
490f0b8
sell amount expectation
sunce86 Dec 29, 2023
b9df28f
removed UTs
sunce86 Dec 29, 2023
7b4939d
revert driver tests
sunce86 Dec 29, 2023
490ecb4
Add driver tests
sunce86 Jan 6, 2024
654181b
Merge branch 'main' into protocol-fee-adjustment-poc
sunce86 Jan 6, 2024
e2bc18c
cr fixes
sunce86 Jan 6, 2024
3767853
removed loop
sunce86 Jan 6, 2024
0cb7c19
camelcase
sunce86 Jan 6, 2024
32c100e
facepalm
sunce86 Jan 6, 2024
fe86304
Merge branch 'main' into protocol-fee-adjustment-poc
sunce86 Jan 10, 2024
c10e82c
reverted tests for protocol fee
sunce86 Jan 10, 2024
010cfc4
fix failing tests
sunce86 Jan 10, 2024
42cd81d
fix driver tests
sunce86 Jan 10, 2024
3fa36b9
fix comments
sunce86 Jan 10, 2024
0f5d374
cr fixes
sunce86 Jan 11, 2024
59d8dae
E2E test protocol fee (#2260)
sunce86 Jan 11, 2024
e7cd0e3
Merge branch 'main' into protocol-fee-adjustment-poc
sunce86 Jan 11, 2024
7fccdcd
log
sunce86 Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
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());
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
// 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()?)
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
} 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
Loading