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 29 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
226 changes: 226 additions & 0 deletions crates/driver/src/domain/competition/solution/fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
//! 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
//! Executed = 0.95 WETH
//!
//! 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 = 0.05 WETH
//! Executed = 1 WETH
//!
//! This response is adjusted by the protocol fee of 0.1 WETH:
//! Fee = 0.05 WETH + 0.1 WETH = 0.15 WETH
//! 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.dynamic_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);
}

let mut protocol_fee = eth::U256::zero();
if let Some(fee_policy) = self.order().fee_policies.first() {
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
match fee_policy {
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
protocol_fee = std::cmp::min(price_improvement_fee, max_volume_fee);
}
FeePolicy::Volume { factor } => {
protocol_fee = self.volume_fee(prices, *factor)?;
}
}
}
Ok(protocol_fee)
}

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 surplus_fee = self
.dynamic_fee()
.map(|fee| fee.0)
.ok_or(Error::ProtocolFeeOnStaticOrder)?;
match self.order().side {
Side::Buy => {
// How much `sell_token` we need to sell to buy `executed` amount of `buy_token`
let executed_sell_amount = executed
.checked_mul(prices.buy)
.ok_or(Error::Overflow)?
.checked_div(prices.sell)
.ok_or(Error::DivisionByZero)?;
// Sell slightly more `sell_token` to capture the `surplus_fee`
let executed_sell_amount_with_fee = executed_sell_amount
.checked_add(surplus_fee)
.ok_or(Error::Overflow)?;
// 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
let surplus = limit_sell_amount
.checked_sub(executed_sell_amount_with_fee)
.unwrap_or(eth::U256::zero());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is used for the surplus based fee we need to reference the quoted price somehow.
Otherwise we'll only compare against the signed limit price which already has the slippage baked in. This is not what we want, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation only works for out of market limit orders (where the quote is worse than the limit price). It's true that the naming is a bit misleading and the real fee policy we are implementing here is is SurplusFee not PriceImprovement

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the PriceImprovement as a general term - improvement over SOME price, whether it's a limit price or some other artificial price (artificial because driver works with limit orders technically and doesn't care whether it has slippage baked in and in which amount).
If it were called MarketPriceImprovement then it would be misleading IMO. Having a general term is better since we will implement the improvement over quotes 99.99% so renaming it would lead to renaming the autopilot/driver interface which is not too problematic right now, but it could be in the future.
OTOH, I might be biased since I spent a lot of time thinking in those terms, so if you both prefer to have SurplusFee I will update it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've captured the potential renaming #2266
I believe this is ok since chances are higher that we will implement this so it's easier to close the issue than to revert the changes.

Ok(surplus
.checked_mul(eth::U256::from_f64_lossy(factor * 100.))
.ok_or(Error::Overflow)?
/ 100)
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
}
Side::Sell => {
// 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)?;
let executed_sell_amount_with_fee =
executed.checked_add(surplus_fee).ok_or(Error::Overflow)?;
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
// 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)?;
// 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
let surplus_in_sell_token = surplus
.checked_mul(prices.buy)
.ok_or(Error::Overflow)?
.checked_div(prices.sell)
.ok_or(Error::DivisionByZero)?;
Ok(surplus_in_sell_token
.checked_mul(eth::U256::from_f64_lossy(factor * 100.))
.ok_or(Error::Overflow)?
/ 100)
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

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

/// 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::new();
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
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 dynamic_fee(&self) -> Option<order::SellAmount> {
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
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
9 changes: 7 additions & 2 deletions crates/driver/src/infra/solver/dto/solution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,13 @@ impl Solutions {
},
weth,
)
.map_err(|competition::solution::InvalidClearingPrices| {
super::Error("invalid clearing prices")
.map_err(|err| match err {
competition::solution::SolutionError::InvalidClearingPrices => {
super::Error("invalid clearing prices")
}
competition::solution::SolutionError::ProtocolFee(_) => {
super::Error("failed protocol fee")
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
}
})
})
.collect()
Expand Down
Loading
Loading