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 fees breakdown #2879

Merged
merged 8 commits into from
Aug 13, 2024
Merged
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
38 changes: 38 additions & 0 deletions crates/autopilot/src/domain/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,44 @@ impl From<SellTokenAmount> for TokenAmount {
}
}

impl std::ops::Add for SellTokenAmount {
type Output = Self;

fn add(self, rhs: Self) -> Self {
Self(self.0 + rhs.0)
}
}

impl num::Zero for SellTokenAmount {
fn zero() -> Self {
Self(U256::zero())
}

fn is_zero(&self) -> bool {
self.0.is_zero()
}
}

impl std::iter::Sum for SellTokenAmount {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(num::Zero::zero(), std::ops::Add::add)
}
}

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

fn sub(self, rhs: Self) -> Self::Output {
self.0.sub(rhs.0).into()
}
}

impl num::CheckedSub for SellTokenAmount {
fn checked_sub(&self, other: &Self) -> Option<Self> {
self.0.checked_sub(other.0).map(Into::into)
}
}

/// Gas amount in gas units.
///
/// The amount of Ether that is paid in transaction fees is proportional to this
Expand Down
8 changes: 4 additions & 4 deletions crates/autopilot/src/domain/settlement/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! A winning solution becomes a [`Settlement`] once it is executed on-chain.

use {
self::solution::ExecutedFee,
crate::{domain, domain::eth, infra},
std::collections::HashMap,
};
Expand Down Expand Up @@ -102,10 +103,9 @@ impl Settlement {
self.solution.native_fee(&self.auction.prices)
}

/// Per order fees denominated in sell token. Contains all orders from the
/// settlement
pub fn order_fees(&self) -> HashMap<domain::OrderUid, Option<eth::SellTokenAmount>> {
self.solution.fees(&self.auction.prices)
/// Per order fees breakdown. Contains all orders from the settlement
pub fn order_fees(&self) -> HashMap<domain::OrderUid, Option<ExecutedFee>> {
self.solution.fees(&self.auction)
}
}

Expand Down
53 changes: 42 additions & 11 deletions crates/autopilot/src/domain/settlement/solution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ use {
mod tokenized;
mod trade;
pub use error::Error;
use {crate::domain, std::collections::HashMap};
use {
crate::{domain, domain::fee},
num::CheckedSub,
std::collections::HashMap,
};

/// A solution that was executed on-chain.
///
Expand Down Expand Up @@ -85,14 +89,24 @@ impl Solution {
.sum()
}

/// Returns fees denominated in sell token for each order in the solution.
pub fn fees(
&self,
prices: &auction::Prices,
) -> HashMap<domain::OrderUid, Option<eth::SellTokenAmount>> {
/// Returns fees breakdown for each order in the solution.
pub fn fees(&self, auction: &super::Auction) -> HashMap<domain::OrderUid, Option<ExecutedFee>> {
self.trades
.iter()
.map(|trade| (*trade.order_uid(), trade.fee_in_sell_token(prices).ok()))
.map(|trade| {
(*trade.order_uid(), {
let total = trade.total_fee_in_sell_token(&auction.prices);
let protocol = trade.protocol_fees_in_sell_token(auction);
match (total, protocol) {
(Ok(total), Ok(protocol)) => {
let network =
total.checked_sub(&protocol.iter().map(|(fee, _)| *fee).sum());
network.map(|network| ExecutedFee { protocol, network })
}
_ => None,
}
})
})
.collect()
}

Expand Down Expand Up @@ -194,6 +208,24 @@ pub mod error {
}
}

/// Fee per trade in a solution. These fees are taken for the execution of the
/// trade.
#[derive(Debug, Clone)]
pub struct ExecutedFee {
/// Gas fee spent to bring the order onchain
pub network: eth::SellTokenAmount,
/// Breakdown of protocol fees. Executed protocol fees are in the same order
/// as policies are defined for an order.
pub protocol: Vec<(eth::SellTokenAmount, fee::Policy)>,
}

impl ExecutedFee {
/// Total fee paid for the trade.
pub fn total(&self) -> eth::SellTokenAmount {
self.network + self.protocol.iter().map(|(fee, _)| *fee).sum()
}
}

#[cfg(test)]
mod tests {
use {
Expand Down Expand Up @@ -325,10 +357,9 @@ mod tests {
eth::U256::from(6752697350740628u128)
);
// fee read from "executedSurplusFee" https://api.cow.fi/mainnet/api/v1/orders/0x10dab31217bb6cc2ace0fe601c15d342f7626a1ee5ef0495449800e73156998740a50cf069e992aa4536211b23f286ef88752187ffffffff
assert_eq!(
solution.fees(&auction.prices),
HashMap::from([(domain::OrderUid(hex!("10dab31217bb6cc2ace0fe601c15d342f7626a1ee5ef0495449800e73156998740a50cf069e992aa4536211b23f286ef88752187ffffffff")), Some(eth::SellTokenAmount(eth::U256::from(6752697350740628u128))))])
);
let order_fees = solution.fees(&auction);
let order_fee = order_fees.get(&domain::OrderUid(hex!("10dab31217bb6cc2ace0fe601c15d342f7626a1ee5ef0495449800e73156998740a50cf069e992aa4536211b23f286ef88752187ffffffff"))).unwrap().clone().unwrap();
assert_eq!(order_fee.total().0, eth::U256::from(6752697350740628u128));
}

// https://etherscan.io/tx/0x688508eb59bd20dc8c0d7c0c0b01200865822c889f0fcef10113e28202783243
Expand Down
117 changes: 75 additions & 42 deletions crates/autopilot/src/domain/settlement/solution/trade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,26 +146,22 @@ impl Trade {
Ok(price.in_eth(fee.amount))
}

/// Total fee (protocol fee + network fee). Equal to a surplus difference
/// before and after applying the fees.
///
/// Denominated in SELL token
pub fn fee_in_sell_token(
/// Converts given surplus fee into sell token fee.
fn fee_into_sell_token(
&self,
fee: eth::TokenAmount,
prices: &auction::Prices,
) -> Result<eth::SellTokenAmount, Error> {
let fee = self.fee()?;
let fee_in_sell_token = match self.side {
order::Side::Buy => fee.amount,
order::Side::Buy => fee,
order::Side::Sell => {
let buy_price = prices
.get(&self.buy.token)
.ok_or(Error::MissingPrice(self.buy.token))?;
let sell_price = prices
.get(&self.sell.token)
.ok_or(Error::MissingPrice(self.sell.token))?;
fee.amount
.checked_mul(&buy_price.get().0.into())
fee.checked_mul(&buy_price.get().0.into())
.ok_or(error::Math::Overflow)?
.checked_div(&sell_price.get().0.into())
.ok_or(error::Math::DivisionByZero)?
Expand All @@ -175,6 +171,18 @@ impl Trade {
Ok(fee_in_sell_token)
}

/// Total fee (protocol fee + network fee). Equal to a surplus difference
/// before and after applying the fees.
///
/// Denominated in SELL token
pub fn total_fee_in_sell_token(
&self,
prices: &auction::Prices,
) -> Result<eth::SellTokenAmount, Error> {
let fee = self.fee()?;
self.fee_into_sell_token(fee.amount, prices)
}

/// Total fee (protocol fee + network fee). Equal to a surplus difference
/// before and after applying the fees.
///
Expand All @@ -191,26 +199,52 @@ impl Trade {
})
}

/// Protocol fees is defined by fee policies attached to the order.
/// Protocol fees are defined by fee policies attached to the order.
///
/// Denominated in SELL token
pub fn protocol_fees_in_sell_token(
&self,
auction: &settlement::Auction,
) -> Result<Vec<(eth::SellTokenAmount, fee::Policy)>, Error> {
self.protocol_fees(auction)?
.into_iter()
.map(|(fee, policy)| {
Ok((
self.fee_into_sell_token(fee.amount, &auction.prices)?,
policy,
))
})
.collect()
}

/// Protocol fees are defined by fee policies attached to the order.
///
/// Denominated in SURPLUS token
fn protocol_fees(&self, policies: &[fee::Policy]) -> Result<eth::Asset, Error> {
fn protocol_fees(
&self,
auction: &settlement::Auction,
) -> Result<Vec<(eth::Asset, fee::Policy)>, Error> {
let policies = auction
.orders
.get(&self.order_uid)
.map(|value| value.as_slice())
.unwrap_or_default();
let mut current_trade = self.clone();
let mut amount = eth::TokenAmount::default();
let mut total = eth::TokenAmount::default();
let mut fees = vec![];
for (i, protocol_fee) in policies.iter().enumerate().rev() {
let fee = current_trade.protocol_fee(protocol_fee)?;
// Do not need to calculate the last custom prices because in the last iteration
// the prices are not used anymore to calculate the protocol fee
amount += fee;
fees.push((fee, *protocol_fee));
total += fee.amount;
if !i.is_zero() {
current_trade.prices.custom = self.calculate_custom_prices(amount)?;
current_trade.prices.custom = self.calculate_custom_prices(total)?;
}
}

Ok(eth::Asset {
token: self.surplus_token(),
amount,
})
// Reverse the fees to have them in the same order as the policies
fees.reverse();
Ok(fees)
}

/// The effective amount that left the user's wallet including all fees.
Expand Down Expand Up @@ -280,34 +314,36 @@ 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::TokenAmount, Error> {
match fee_policy {
fn protocol_fee(&self, fee_policy: &fee::Policy) -> Result<eth::Asset, Error> {
let amount = match fee_policy {
fee::Policy::Surplus {
factor,
max_volume_factor,
} => {
let surplus = self.surplus_over_limit_price()?;
let fee = std::cmp::min(
std::cmp::min(
self.surplus_fee(surplus, (*factor).into())?.amount,
self.volume_fee((*max_volume_factor).into())?.amount,
);
Ok::<eth::TokenAmount, Error>(fee)
)
}
fee::Policy::PriceImprovement {
factor,
max_volume_factor,
quote,
} => {
let price_improvement = self.price_improvement(quote)?;
let fee = std::cmp::min(
std::cmp::min(
self.surplus_fee(price_improvement, (*factor).into())?
.amount,
self.volume_fee((*max_volume_factor).into())?.amount,
);
Ok(fee)
)
}
fee::Policy::Volume { factor } => Ok(self.volume_fee((*factor).into())?.amount),
}
fee::Policy::Volume { factor } => self.volume_fee((*factor).into())?.amount,
};
Ok(eth::Asset {
token: self.surplus_token(),
amount,
})
}

fn price_improvement(&self, quote: &domain::fee::Quote) -> Result<eth::Asset, Error> {
Expand Down Expand Up @@ -455,19 +491,16 @@ impl Trade {
///
/// Denominated in NATIVE token
fn native_protocol_fee(&self, auction: &settlement::Auction) -> Result<eth::Ether, Error> {
let protocol_fee = self.protocol_fees(
auction
.orders
.get(&self.order_uid)
.map(|value| value.as_slice())
.unwrap_or_default(),
)?;
let price = auction
.prices
.get(&protocol_fee.token)
.ok_or(Error::MissingPrice(protocol_fee.token))?;

Ok(price.in_eth(protocol_fee.amount))
self.protocol_fees(auction)?
.into_iter()
.map(|(fee, _)| {
let price = auction
.prices
.get(&fee.token)
.ok_or(Error::MissingPrice(fee.token))?;
Ok(price.in_eth(fee.amount))
})
.sum()
}

fn surplus_token(&self) -> eth::TokenAddress {
Expand Down
4 changes: 3 additions & 1 deletion crates/autopilot/src/on_settlement_event_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,9 @@ impl Inner {
"automatic check error: order_fees missing"
);
} else {
let settlement_fee = order_fees[&domain::OrderUid(fee.0 .0)];
let settlement_fee = order_fees[&domain::OrderUid(fee.0 .0)]
.as_ref()
.map(|fee| fee.total());
if settlement_fee.unwrap_or_default().0 != fee.1 {
tracing::warn!(
?auction_id,
Expand Down
Loading
Loading