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

[BLOCKED] Save executed fee in surplus token #3184

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions crates/autopilot/src/domain/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ impl From<EffectiveGasPrice> for U256 {
value.0.into()
}
}

impl std::ops::Sub<Self> for TokenAmount {
type Output = TokenAmount;

Expand Down
18 changes: 12 additions & 6 deletions crates/autopilot/src/domain/settlement/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,9 @@ impl Settlement {
"possible incomplete fee breakdown calculation",
);
trade::FeeBreakdown {
total: eth::Asset {
// TODO surplus token
token: trade.sell_token(),
amount: num::zero(),
},
executed: num::zero(),
protocol: vec![],
token: trade.surplus_token(),
}
});
(*trade.uid(), fee_breakdown)
Expand Down Expand Up @@ -353,6 +350,15 @@ mod tests {
trade.fee_in_ether(&auction.prices).unwrap().0,
eth::U256::from(6752697350740628u128)
);
// fee read from "executedFee" https://api.cow.fi/mainnet/api/v1/orders/0x10dab31217bb6cc2ace0fe601c15d342f7626a1ee5ef0495449800e73156998740a50cf069e992aa4536211b23f286ef88752187ffffffff
// and then using UCP prices from https://api.cow.fi/mainnet/api/v1/solver_competition/by_tx_hash/0xc48dc0d43ffb43891d8c3ad7bcf05f11465518a2610869b20b0b4ccb61497634
// fee can be converted to surplus token:
// 6890975030480504 / 10115523891911314 * 18446744073709551616 =
// 12566432956304187498
assert_eq!(
trade.fee_breakdown(&auction).unwrap().executed.0,
eth::U256::from(12566432956304187498u128)
);
}

// https://etherscan.io/tx/0x688508eb59bd20dc8c0d7c0c0b01200865822c889f0fcef10113e28202783243
Expand Down Expand Up @@ -828,7 +834,7 @@ mod tests {
assert_eq!(jit_trade.fee_in_ether(&auction.prices).unwrap().0, 0.into());
assert_eq!(jit_trade.score(&auction).unwrap().0, 0.into());
assert_eq!(
jit_trade.fee_breakdown(&auction).unwrap().total.amount.0,
jit_trade.fee_breakdown(&auction).unwrap().executed.0,
0.into()
);
assert!(jit_trade
Expand Down
162 changes: 76 additions & 86 deletions crates/autopilot/src/domain/settlement/trade/math.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
pub use error::Error;
use {
super::ExecutedProtocolFee,
crate::{
domain::{
self,
Expand Down Expand Up @@ -47,7 +46,7 @@ impl Trade {
&self,
prices: &ClearingPrices,
price_limits: PriceLimits,
) -> Result<eth::Asset, error::Math> {
) -> Result<eth::TokenAmount, error::Math> {
match self.side {
order::Side::Buy => {
// scale limit sell to support partially fillable orders
Expand Down Expand Up @@ -91,78 +90,57 @@ impl Trade {
bought.checked_sub(limit_buy).ok_or(error::Math::Negative)
}
}
.map(|surplus| eth::Asset {
token: self.surplus_token(),
amount: surplus.into(),
})
.map(eth::TokenAmount)
}

/// Surplus based on custom clearing prices returns the surplus after all
/// fees have been applied.
pub fn surplus_in_ether(&self, prices: &auction::Prices) -> Result<eth::Ether, Error> {
let surplus = self.surplus_over_limit_price()?;
let price = prices
.get(&surplus.token)
.ok_or(Error::MissingPrice(surplus.token))?;
.get(&self.surplus_token())
.ok_or(Error::MissingPrice(self.surplus_token()))?;

Ok(price.in_eth(surplus.amount))
Ok(price.in_eth(surplus))
}

/// Total fee (protocol fee + network fee). Equal to a surplus difference
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
/// Total fee (protocol fee + gas fee). Equal to a surplus difference
/// before and after applying the fees.
pub fn fee_in_ether(&self, prices: &auction::Prices) -> Result<eth::Ether, Error> {
let fee = self.fee()?;
let price = prices
.get(&fee.token)
.ok_or(Error::MissingPrice(fee.token))?;
Ok(price.in_eth(fee.amount))
.get(&self.surplus_token())
.ok_or(Error::MissingPrice(self.surplus_token()))?;
Ok(price.in_eth(fee))
}

/// Converts given surplus fee into sell token fee.
fn fee_into_sell_token(&self, fee: eth::TokenAmount) -> Result<eth::SellTokenAmount, Error> {
let fee_in_sell_token = match self.side {
order::Side::Buy => fee,
order::Side::Sell => fee
.checked_mul(&self.prices.uniform.buy.into())
.ok_or(error::Math::Overflow)?
.checked_div(&self.prices.uniform.sell.into())
.ok_or(error::Math::DivisionByZero)?,
}
.into();
Ok(fee_in_sell_token)
}

/// Total fee (protocol fee + network fee). Equal to a surplus difference
/// before and after applying the fees.
pub fn fee_in_sell_token(&self) -> Result<eth::Asset, Error> {
let fee = self.fee()?;
self.fee_into_sell_token(fee.amount)
.map(|amount| eth::Asset {
token: self.sell.token,
amount: amount.into(),
})
/// Complete breakdown of all fees taken for the trade.
///
/// Denominated in SURPLUS token
pub fn fee_breakdown(&self, auction: &settlement::Auction) -> Result<FeeBreakdown, Error> {
Ok(FeeBreakdown {
executed: self.fee()?,
protocol: self.protocol_fees(auction)?,
token: self.surplus_token(),
})
}

/// Total fee (protocol fee + network fee). Equal to a surplus difference
/// Total fee (protocol fee + gas fee). Equal to a surplus difference
/// before and after applying the fees.
///
/// Denominated in SURPLUS token
fn fee(&self) -> Result<eth::Asset, Error> {
fn fee(&self) -> Result<eth::TokenAmount, Error> {
let fee = self
.surplus_over_limit_price_before_fee()?
.amount
.checked_sub(&self.surplus_over_limit_price()?.amount)
.checked_sub(&self.surplus_over_limit_price()?)
.ok_or(error::Math::Negative)?;
Ok(eth::Asset {
token: self.surplus_token(),
amount: fee,
})
Ok(fee)
}

/// Protocol fees are defined by fee policies attached to the order.
///
/// Denominated in SURPLUS token
pub fn protocol_fees(
fn protocol_fees(
&self,
auction: &settlement::Auction,
) -> Result<Vec<ExecutedProtocolFee>, Error> {
Expand All @@ -182,7 +160,7 @@ impl Trade {
policy: *policy,
fee,
});
total += fee.amount;
total += fee;
Copy link
Contributor

Choose a reason for hiding this comment

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

Safer would be to use checked_add().

if !i.is_zero() {
current_trade.prices.custom = self.calculate_custom_prices(total)?;
}
Expand Down Expand Up @@ -259,16 +237,16 @@ impl Trade {
/// Protocol fee is defined by a fee policy attached to the order.
///
/// Denominated in SURPLUS token
fn protocol_fee(&self, fee_policy: &fee::Policy) -> Result<eth::Asset, Error> {
fn protocol_fee(&self, fee_policy: &fee::Policy) -> Result<eth::TokenAmount, Error> {
let amount = match fee_policy {
fee::Policy::Surplus {
factor,
max_volume_factor,
} => {
let surplus = self.surplus_over_limit_price()?;
std::cmp::min(
self.surplus_fee(surplus, (*factor).into())?.amount,
self.volume_fee((*max_volume_factor).into())?.amount,
self.surplus_fee(surplus, (*factor).into())?,
self.volume_fee((*max_volume_factor).into())?,
)
}
fee::Policy::PriceImprovement {
Expand All @@ -278,35 +256,28 @@ impl Trade {
} => {
let price_improvement = self.price_improvement(quote)?;
std::cmp::min(
self.surplus_fee(price_improvement, (*factor).into())?
.amount,
self.volume_fee((*max_volume_factor).into())?.amount,
self.surplus_fee(price_improvement, (*factor).into())?,
self.volume_fee((*max_volume_factor).into())?,
)
}
fee::Policy::Volume { factor } => self.volume_fee((*factor).into())?.amount,
fee::Policy::Volume { factor } => self.volume_fee((*factor).into())?,
};
Ok(eth::Asset {
token: self.surplus_token(),
amount,
})
Ok(amount)
}

fn price_improvement(&self, quote: &domain::fee::Quote) -> Result<eth::Asset, Error> {
fn price_improvement(&self, quote: &domain::fee::Quote) -> Result<eth::TokenAmount, Error> {
let surplus = self.surplus_over_quote(quote);
// negative surplus is not error in this case, as solutions often have no
// improvement over quote which results in negative surplus
if let Err(error::Math::Negative) = surplus {
return Ok(eth::Asset {
token: self.surplus_token(),
amount: Default::default(),
});
return Ok(num::zero());
}
Ok(surplus?)
}

/// Uses custom prices to calculate the surplus after the protocol fee and
/// network fee are applied.
fn surplus_over_limit_price(&self) -> Result<eth::Asset, error::Math> {
/// gas fee are applied.
fn surplus_over_limit_price(&self) -> Result<eth::TokenAmount, error::Math> {
let limit_price = PriceLimits {
sell: self.sell.amount,
buy: self.buy.amount,
Expand All @@ -315,16 +286,19 @@ impl Trade {
}

/// Uses uniform prices to calculate the surplus as if the protocol fee and
/// network fee are not applied.
fn surplus_over_limit_price_before_fee(&self) -> Result<eth::Asset, error::Math> {
/// gas fee are not applied.
fn surplus_over_limit_price_before_fee(&self) -> Result<eth::TokenAmount, error::Math> {
let limit_price = PriceLimits {
sell: self.sell.amount,
buy: self.buy.amount,
};
self.surplus_over(&self.prices.uniform, limit_price)
}

fn surplus_over_quote(&self, quote: &domain::fee::Quote) -> Result<eth::Asset, error::Math> {
fn surplus_over_quote(
&self,
quote: &domain::fee::Quote,
) -> Result<eth::TokenAmount, error::Math> {
let quote = adjust_quote_to_order_limits(
Order {
sell: self.sell.amount,
Expand All @@ -341,7 +315,11 @@ impl Trade {
}

/// Protocol fee as a cut of surplus, denominated in SURPLUS token
fn surplus_fee(&self, surplus: eth::Asset, factor: f64) -> Result<eth::Asset, Error> {
fn surplus_fee(
&self,
surplus: eth::TokenAmount,
factor: f64,
Copy link
Contributor

@mstrug mstrug Jan 2, 2025

Choose a reason for hiding this comment

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

Maybe use FeeFactor here instead of f64 so we can ensure on type-level that factor cannot have value of 1 (so there will be no division by 0).

Same for volume_fee().

) -> Result<eth::TokenAmount, Error> {
// Surplus fee is specified as a `factor` from raw surplus (before fee). Since
// this module works with trades that already have the protocol fee applied, we
// need to calculate the protocol fee as an observation of the eventually traded
Expand All @@ -360,18 +338,13 @@ impl Trade {
// Finally:
// fee = surplus_after_fee * factor / (1 - factor)
let fee = surplus
.amount
.apply_factor(factor / (1.0 - factor))
.ok_or(error::Math::Overflow)?;

Ok(eth::Asset {
token: surplus.token,
amount: fee,
})
Ok(fee)
}

/// Protocol fee as a cut of the trade volume, denominated in SURPLUS token
fn volume_fee(&self, factor: f64) -> Result<eth::Asset, Error> {
fn volume_fee(&self, factor: f64) -> Result<eth::TokenAmount, Error> {
// Volume fee is specified as a `factor` from raw volume (before fee). Since
// this module works with trades that already have the protocol fee applied, we
// need to calculate the protocol fee as an observation of a the eventually
Expand Down Expand Up @@ -409,14 +382,10 @@ impl Trade {
order::Side::Buy => factor / (1.0 + factor),
};

Ok(eth::Asset {
token: self.surplus_token(),
amount: {
executed_in_surplus_token
.apply_factor(factor)
.ok_or(error::Math::Overflow)?
},
})
let fee = executed_in_surplus_token
.apply_factor(factor)
.ok_or(error::Math::Overflow)?;
Ok(fee)
}

/// Protocol fee is defined by fee policies attached to the order.
Expand All @@ -426,9 +395,9 @@ impl Trade {
.map(|ExecutedProtocolFee { fee, policy: _ }| {
let price = auction
.prices
.get(&fee.token)
.ok_or(Error::MissingPrice(fee.token))?;
Ok(price.in_eth(fee.amount))
.get(&self.surplus_token())
.ok_or(Error::MissingPrice(self.surplus_token()))?;
Ok(price.in_eth(fee))
})
.sum()
}
Expand All @@ -441,6 +410,27 @@ impl Trade {
}
}

/// Fee per trade in a solution. These fees are taken for the execution of the
/// trade.
#[derive(Debug, Clone)]
pub struct FeeBreakdown {
/// Fee reported by solvers, representing difference between UCP and custom
/// prices.
pub executed: eth::TokenAmount,
Comment on lines +417 to +419
Copy link
Contributor

Choose a reason for hiding this comment

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

Just by reading the struct and comments it's not really clear to me what this field is.
It sounds like this is just the sum of all protocol fees but I think since we need to account for the gas in the breakdown this is all protocol fees + gas fees.
In that case I think it would be nicer to have a field like gas_costs and a public member function total_fees(). WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is how I implemented it initially, but I had to revert those changes. I commented here on the reasons why.

/// Breakdown of protocol fees.
pub protocol: Vec<ExecutedProtocolFee>,
Comment on lines +420 to +421
Copy link
Contributor

Choose a reason for hiding this comment

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

The order of items also indicates the order in which the fee policies got applied, right?
Seems important enough to point out in a comment.

/// Surplus token in which all the fees were taken.
pub token: eth::TokenAddress,
}
Comment on lines +413 to +424
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be helpful to include details from your comment somewhere here.


#[derive(Debug, Clone)]
pub struct ExecutedProtocolFee {
/// Policy that was used to calculate the fee.
pub policy: fee::Policy,
/// Fee that was taken for the trade, in surplus token.
pub fee: eth::TokenAmount,
}

#[derive(Clone, Debug)]
pub struct PriceLimits {
pub sell: eth::TokenAmount,
Expand Down
Loading
Loading