Skip to content

Commit

Permalink
Forward fee policy to solvers (#2544)
Browse files Browse the repository at this point in the history
# Description
Related to #2440

Forwards fee policies from `driver` to `solvers`. 

I assume same will need to be added to `gnosis/solvers` repo.

Potentially breaking the api, depends how external solvers parse unknown
fields.

## How to test
Existing tests.

<!--
## Related Issues

Fixes #2544
-->

---------

Co-authored-by: Mateo <[email protected]>
Co-authored-by: sunce86 <[email protected]>
Co-authored-by: Martin Beckmann <[email protected]>
  • Loading branch information
3 people authored Mar 27, 2024
1 parent a50a2e6 commit 600b9bf
Show file tree
Hide file tree
Showing 29 changed files with 402 additions and 67 deletions.
16 changes: 14 additions & 2 deletions crates/driver/src/domain/competition/solution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use {
},
infra::{
blockchain::{self, Ethereum},
config::file::FeeHandler,
simulator,
solver::Solver,
Simulator,
Expand Down Expand Up @@ -44,6 +45,7 @@ pub struct Solution {
score: SolverScore,
weth: eth::WethAddress,
gas: Option<eth::Gas>,
fee_handler: FeeHandler,
}

impl Solution {
Expand All @@ -57,6 +59,7 @@ impl Solution {
score: SolverScore,
weth: eth::WethAddress,
gas: Option<eth::Gas>,
fee_handler: FeeHandler,
) -> Result<Self, error::Solution> {
let solution = Self {
id,
Expand All @@ -67,6 +70,7 @@ impl Solution {
score,
weth,
gas,
fee_handler,
};

// Check that the solution includes clearing prices for all user trades.
Expand All @@ -77,7 +81,11 @@ impl Solution {
return Err(error::Solution::InvalidClearingPrices);
}

// Apply protocol fees
// Apply protocol fees only if the drivers is set to handler the fees
if fee_handler != FeeHandler::Driver {
return Ok(solution);
}

let mut trades = Vec::with_capacity(solution.trades.len());
for trade in solution.trades {
match &trade {
Expand Down Expand Up @@ -179,7 +187,11 @@ impl Solution {
trade.order().side,
executed,
custom_prices,
trade.order().protocol_fees.clone(),
if self.fee_handler == FeeHandler::Driver {
trade.order().protocol_fees.clone()
} else {
vec![]
},
))
}

Expand Down
2 changes: 0 additions & 2 deletions crates/driver/src/domain/competition/solution/scoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,6 @@ pub struct CustomClearingPrices {
pub enum Error {
#[error("multiple fee policies are not supported yet")]
MultipleFeePolicies,
#[error("fee policy not implemented yet")]
UnimplementedFeePolicy,
#[error("missing native price for token {0:?}")]
MissingPrice(eth::TokenAddress),
#[error(transparent)]
Expand Down
1 change: 1 addition & 0 deletions crates/driver/src/infra/config/file/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub async fn load(chain: eth::ChainId, path: &Path) -> infra::Config {
},
request_headers: config.request_headers,
rank_by_surplus_date: config.rank_by_surplus_date,
fee_handler: config.fee_handler,
}
}))
.await,
Expand Down
14 changes: 13 additions & 1 deletion crates/driver/src/infra/config/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ pub use load::load;
use {
crate::{domain::eth, util::serialize},
reqwest::Url,
serde::Deserialize,
serde::{Deserialize, Serialize},
serde_with::serde_as,
solver::solver::Arn,
std::{collections::HashMap, time::Duration},
Expand Down Expand Up @@ -188,6 +188,18 @@ struct SolverConfig {

/// Datetime when the CIP38 rank by surplus rules should be activated.
rank_by_surplus_date: Option<chrono::DateTime<chrono::Utc>>,

/// Determines whether the `solver` or the `driver` handles the fees
#[serde(default)]
fee_handler: FeeHandler,
}

#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum FeeHandler {
#[default]
Driver,
Solver,
}

#[serde_as]
Expand Down
118 changes: 94 additions & 24 deletions crates/driver/src/infra/solver/dto/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use {
competition,
competition::{
order,
order::{FeePolicy, Side},
order::{fees, Side},
},
eth,
liquidity,
},
infra::config::file::FeeHandler,
util::{
conv::{rational_to_big_decimal, u256::U256Ext},
serialize,
Expand All @@ -25,6 +26,7 @@ impl Auction {
auction: &competition::Auction,
liquidity: &[liquidity::Liquidity],
weth: eth::WethAddress,
fee_handler: FeeHandler,
) -> Self {
let mut tokens: HashMap<eth::H160, _> = auction
.tokens()
Expand Down Expand Up @@ -68,31 +70,35 @@ impl Auction {
.iter()
.map(|order| {
let mut available = order.available(weth);
// Solvers are unaware of the protocol fees. In case of volume based fees,
// fee withheld by driver might be higher than the surplus of the solution. This
// would lead to violating limit prices when driver tries to withhold the
// volume based fee. To avoid this, we artifically adjust the order limit
// amounts (make then worse) before sending to solvers, to force solvers to only
// submit solutions with enough surplus to cover the fee.
// In case of volume based fees, fee withheld by driver might be higher than the
// surplus of the solution. This would lead to violating limit prices when
// driver tries to withhold the volume based fee. To avoid this, we artificially
// adjust the order limit amounts (make then worse) before sending to solvers,
// to force solvers to only submit solutions with enough surplus to cover the
// fee.
//
// https://github.com/cowprotocol/services/issues/2440
if let Some(FeePolicy::Volume { factor }) = order.protocol_fees.first() {
match order.side {
Side::Buy => {
// reduce sell amount by factor
available.sell.amount = available
.sell
.amount
.apply_factor(1.0 / (1.0 + factor))
.unwrap_or_default();
}
Side::Sell => {
// increase buy amount by factor
available.buy.amount = available
.buy
.amount
.apply_factor(1.0 / (1.0 - factor))
.unwrap_or_default();
if fee_handler == FeeHandler::Driver {
if let Some(fees::FeePolicy::Volume { factor }) =
order.protocol_fees.first()
{
match order.side {
Side::Buy => {
// reduce sell amount by factor
available.sell.amount = available
.sell
.amount
.apply_factor(1.0 / (1.0 + factor))
.unwrap_or_default();
}
Side::Sell => {
// increase buy amount by factor
available.buy.amount = available
.buy
.amount
.apply_factor(1.0 / (1.0 - factor))
.unwrap_or_default();
}
}
}
}
Expand All @@ -113,6 +119,13 @@ impl Auction {
competition::order::Kind::Limit { .. } => Class::Limit,
competition::order::Kind::Liquidity => Class::Liquidity,
},
fee_policies: order
.protocol_fees
.iter()
.filter(|_| fee_handler == FeeHandler::Solver)
.cloned()
.map(Into::into)
.collect(),
}
})
.collect(),
Expand Down Expand Up @@ -275,6 +288,7 @@ struct Order {
kind: Kind,
partially_fillable: bool,
class: Class,
fee_policies: Vec<FeePolicy>,
}

#[derive(Debug, Serialize)]
Expand All @@ -292,6 +306,62 @@ enum Class {
Liquidity,
}

#[serde_as]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
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 },
}

impl From<fees::FeePolicy> for FeePolicy {
fn from(value: order::FeePolicy) -> Self {
match value {
order::FeePolicy::Surplus {
factor,
max_volume_factor,
} => FeePolicy::Surplus {
factor,
max_volume_factor,
},
order::FeePolicy::PriceImprovement {
factor,
max_volume_factor,
quote,
} => FeePolicy::PriceImprovement {
factor,
max_volume_factor,
quote: Quote {
sell_amount: quote.sell.amount.into(),
buy_amount: quote.buy.amount.into(),
fee: quote.fee.amount.into(),
},
},
order::FeePolicy::Volume { factor } => FeePolicy::Volume { factor },
}
}
}

#[serde_as]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Quote {
#[serde_as(as = "serialize::U256")]
pub sell_amount: eth::U256,
#[serde_as(as = "serialize::U256")]
pub buy_amount: eth::U256,
#[serde_as(as = "serialize::U256")]
pub fee: eth::U256,
}

#[serde_as]
#[derive(Default, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
Expand Down
8 changes: 5 additions & 3 deletions crates/driver/src/infra/solver/dto/solution.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use {
crate::{
domain::{competition, competition::order, eth, liquidity},
infra::Solver,
infra::{solver::Config, Solver},
util::serialize,
},
itertools::Itertools,
Expand All @@ -17,7 +17,7 @@ impl Solutions {
liquidity: &[liquidity::Liquidity],
weth: eth::WethAddress,
solver: Solver,
rank_by_surplus_date: Option<chrono::DateTime<chrono::Utc>>,
solver_config: &Config,
) -> Result<Vec<competition::Solution>, super::Error> {
self.solutions
.into_iter()
Expand Down Expand Up @@ -189,7 +189,8 @@ impl Solutions {
})
.try_collect()?,
solver.clone(),
match rank_by_surplus_date
match solver_config
.rank_by_surplus_date
.is_some_and(|date| auction.deadline().driver() > date)
{
true => competition::solution::SolverScore::Surplus,
Expand All @@ -206,6 +207,7 @@ impl Solutions {
},
weth,
solution.gas.map(|gas| eth::Gas(gas.into())),
solver_config.fee_handler,
)
.map_err(|err| match err {
competition::solution::error::Solution::InvalidClearingPrices => {
Expand Down
20 changes: 11 additions & 9 deletions crates/driver/src/infra/solver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use {
liquidity,
time::Remaining,
},
infra::blockchain::Ethereum,
infra::{blockchain::Ethereum, config::file::FeeHandler},
util,
},
anyhow::Result,
Expand Down Expand Up @@ -104,6 +104,8 @@ pub struct Config {
pub request_headers: HashMap<String, String>,
/// Datetime when the CIP38 rank by surplus rules should be activated.
pub rank_by_surplus_date: Option<chrono::DateTime<chrono::Utc>>,
/// Determines whether the `solver` or the `driver` handles the fees
pub fee_handler: FeeHandler,
}

impl Solver {
Expand Down Expand Up @@ -167,7 +169,13 @@ impl Solver {
) -> Result<Vec<Solution>, Error> {
// Fetch the solutions from the solver.
let weth = self.eth.contracts().weth_address();
let body = serde_json::to_string(&dto::Auction::new(auction, liquidity, weth)).unwrap();
let body = serde_json::to_string(&dto::Auction::new(
auction,
liquidity,
weth,
self.config.fee_handler,
))
.unwrap();
let url = shared::url::join(&self.config.endpoint, "solve");
super::observe::solver_request(&url, &body);
let mut req = self
Expand All @@ -183,13 +191,7 @@ impl Solver {
let res = res?;
let res: dto::Solutions = serde_json::from_str(&res)
.tap_err(|err| tracing::warn!(res, ?err, "failed to parse solver response"))?;
let solutions = res.into_domain(
auction,
liquidity,
weth,
self.clone(),
self.config.rank_by_surplus_date,
)?;
let solutions = res.into_domain(auction, liquidity, weth, self.clone(), &self.config)?;

super::observe::solutions(&solutions);
Ok(solutions)
Expand Down
4 changes: 3 additions & 1 deletion crates/driver/src/tests/cases/fees.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::{
domain::competition::order,
infra::config::file::FeeHandler,
tests::{
self,
setup::{ab_order, ab_pool, ab_solution},
setup::{ab_order, ab_pool, ab_solution, test_solver},
},
};

Expand Down Expand Up @@ -35,6 +36,7 @@ async fn solver_fee() {
.solver_fee(Some(500.into()));
let test = tests::setup()
.name(format!("Solver Fee: {side:?}"))
.solvers(vec![test_solver().fee_handler(FeeHandler::Driver)])
.pool(ab_pool())
.order(order.clone())
.solution(ab_solution())
Expand Down
12 changes: 9 additions & 3 deletions crates/driver/src/tests/cases/order_prioritization.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::tests::{
cases::EtherExt,
setup::{ab_order, ab_pool, ab_solution, setup, Order},
use crate::{
infra::config::file::FeeHandler,
tests::{
cases::EtherExt,
setup::{ab_order, ab_pool, ab_solution, setup, test_solver, Order},
},
};

/// Test that orders are sorted correctly before being sent to the solver:
Expand All @@ -11,6 +14,9 @@ use crate::tests::{
#[ignore]
async fn sorting() {
let test = setup()
.solvers(vec![
test_solver().fee_handler(FeeHandler::Driver)
])
.pool(ab_pool())
// Orders with better price ratios come first.
.order(ab_order())
Expand Down
Loading

0 comments on commit 600b9bf

Please sign in to comment.