Skip to content

Commit

Permalink
Protocol fees breakdown (#2879)
Browse files Browse the repository at this point in the history
# Description
Implements task ~2~ 1 from
#2862:

> Extend order_execution with a field that contains a list of executed
protocol fees in surplus token, together with the surplus token address

`autopilot::domain::Settlement` now exposes breakdown of protocol fees
in surplus token.

Applies the same refactoring to driver, to keep the code 1:1 (and also
to use driver tests to confirm no bugs were introduced).

## How to test
Existing tests.
  • Loading branch information
sunce86 committed Aug 13, 2024
1 parent 0412ffa commit cd40c68
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 86 deletions.
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

0 comments on commit cd40c68

Please sign in to comment.