diff --git a/crates/autopilot/src/domain/eth/mod.rs b/crates/autopilot/src/domain/eth/mod.rs index 3227ad6b1f..5f8ef77f87 100644 --- a/crates/autopilot/src/domain/eth/mod.rs +++ b/crates/autopilot/src/domain/eth/mod.rs @@ -51,6 +51,18 @@ impl TokenAmount { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, From, Into)] pub struct SellTokenAmount(pub U256); +impl From for SellTokenAmount { + fn from(value: TokenAmount) -> Self { + Self(value.0) + } +} + +impl From for TokenAmount { + fn from(value: SellTokenAmount) -> Self { + Self(value.0) + } +} + /// Gas amount in gas units. /// /// The amount of Ether that is paid in transaction fees is proportional to this diff --git a/crates/autopilot/src/domain/settlement/mod.rs b/crates/autopilot/src/domain/settlement/mod.rs index 5a7400f8c4..227ee100cb 100644 --- a/crates/autopilot/src/domain/settlement/mod.rs +++ b/crates/autopilot/src/domain/settlement/mod.rs @@ -8,9 +8,15 @@ use { }; mod auction; +mod observation; mod solution; mod transaction; -pub use {auction::Auction, solution::Solution, transaction::Transaction}; +pub use { + auction::Auction, + observation::Observation, + solution::Solution, + transaction::Transaction, +}; /// A solution together with the `Auction` for which it was picked as a winner /// and executed on-chain. @@ -20,25 +26,41 @@ pub use {auction::Auction, solution::Solution, transaction::Transaction}; #[derive(Debug)] pub struct Settlement { solution: Solution, + transaction: Transaction, auction: Auction, } impl Settlement { pub async fn new( - tx: &Transaction, + transaction: Transaction, domain_separator: ð::DomainSeparator, persistence: &infra::Persistence, ) -> Result { - let solution = Solution::new(&tx.input, domain_separator)?; + let solution = Solution::new(&transaction.input, domain_separator)?; let auction = persistence.get_auction(solution.auction_id()).await?; - Ok(Self { solution, auction }) + Ok(Self { + solution, + transaction, + auction, + }) } /// CIP38 score calculation pub fn score(&self) -> Result { self.solution.score(&self.auction) } + + /// Returns the observation of the settlement. + pub fn observation(&self) -> Observation { + Observation { + gas: self.transaction.gas, + gas_price: self.transaction.effective_gas_price, + surplus: self.solution.native_surplus(&self.auction), + fee: self.solution.native_fee(&self.auction.prices), + order_fees: self.solution.fees(), + } + } } #[derive(Debug, thiserror::Error)] diff --git a/crates/autopilot/src/domain/settlement/observation.rs b/crates/autopilot/src/domain/settlement/observation.rs new file mode 100644 index 0000000000..1be82d2662 --- /dev/null +++ b/crates/autopilot/src/domain/settlement/observation.rs @@ -0,0 +1,25 @@ +//! Aggregated type containing all important information about the mined +//! settlement, including the surplus and fees. +//! +//! Observation is a snapshot of the settlement state, which purpose is to save +//! the state of the settlement to the persistence layer. + +use { + crate::domain::{self, eth}, + std::collections::HashMap, +}; + +#[derive(Debug, Clone)] +pub struct Observation { + /// The gas used by the settlement. + pub gas: eth::Gas, + /// The effective gas price at the time of settlement. + pub gas_price: eth::EffectiveGasPrice, + /// Total surplus expressed in native token. + pub surplus: eth::Ether, + /// Total fee expressed in native token. + pub fee: eth::Ether, + /// Per order fees denominated in sell token. Contains all orders from the + /// settlement + pub order_fees: HashMap>, +} diff --git a/crates/autopilot/src/domain/settlement/solution/mod.rs b/crates/autopilot/src/domain/settlement/solution/mod.rs index 42700dda73..de73a76908 100644 --- a/crates/autopilot/src/domain/settlement/solution/mod.rs +++ b/crates/autopilot/src/domain/settlement/solution/mod.rs @@ -13,6 +13,7 @@ use { mod tokenized; mod trade; pub use error::Error; +use {crate::domain, std::collections::HashMap}; /// A solution that was executed on-chain. /// @@ -44,20 +45,54 @@ impl Solution { )?) } - pub fn native_surplus(&self, auction: &super::Auction) -> Result { + /// Total surplus for all trades in the solution. + /// + /// Always returns a value, even if some trades have incomplete surplus + /// calculation. + pub fn native_surplus(&self, auction: &super::Auction) -> eth::Ether { self.trades .iter() - .map(|trade| trade.native_surplus(auction)) + .map(|trade| { + trade.native_surplus(auction).unwrap_or_else(|err| { + tracing::warn!( + ?err, + "possible incomplete surplus calculation for trade {}", + trade.order_uid() + ); + num::zero() + }) + }) .sum() } - pub fn native_fee(&self, prices: &auction::Prices) -> Result { + /// Total fee for all trades in the solution. + /// + /// Always returns a value, even if some trades have incomplete fee + /// calculation. + pub fn native_fee(&self, prices: &auction::Prices) -> eth::Ether { self.trades .iter() - .map(|trade| trade.native_fee(prices)) + .map(|trade| { + trade.native_fee(prices).unwrap_or_else(|err| { + tracing::warn!( + ?err, + "possible incomplete fee calculation for trade {}", + trade.order_uid() + ); + num::zero() + }) + }) .sum() } + /// Returns fees denominated in sell token for each order in the solution. + pub fn fees(&self) -> HashMap> { + self.trades + .iter() + .map(|trade| (*trade.order_uid(), trade.fee().ok())) + .collect() + } + pub fn new( calldata: ð::Calldata, domain_separator: ð::DomainSeparator, @@ -269,12 +304,12 @@ mod tests { // surplus (score) read from https://api.cow.fi/mainnet/api/v1/solver_competition/by_tx_hash/0xc48dc0d43ffb43891d8c3ad7bcf05f11465518a2610869b20b0b4ccb61497634 assert_eq!( - solution.native_surplus(&auction).unwrap().0, + solution.native_surplus(&auction).0, eth::U256::from(52937525819789126u128) ); // fee read from "executedSurplusFee" https://api.cow.fi/mainnet/api/v1/orders/0x10dab31217bb6cc2ace0fe601c15d342f7626a1ee5ef0495449800e73156998740a50cf069e992aa4536211b23f286ef88752187ffffffff assert_eq!( - solution.native_fee(&auction.prices).unwrap().0, + solution.native_fee(&auction.prices).0, eth::U256::from(6890975030480504u128) ); } diff --git a/crates/autopilot/src/domain/settlement/solution/trade.rs b/crates/autopilot/src/domain/settlement/solution/trade.rs index 867139e759..0261c406a2 100644 --- a/crates/autopilot/src/domain/settlement/solution/trade.rs +++ b/crates/autopilot/src/domain/settlement/solution/trade.rs @@ -46,6 +46,10 @@ impl Trade { } } + pub fn order_uid(&self) -> &domain::OrderUid { + &self.order_uid + } + /// CIP38 score defined as surplus + protocol fee /// /// Denominated in NATIVE token @@ -130,6 +134,18 @@ impl Trade { /// /// Denominated in NATIVE token pub fn native_fee(&self, prices: &auction::Prices) -> Result { + let fee = self.fee()?; + let price = prices + .get(&self.sell.token) + .ok_or(Error::MissingPrice(self.sell.token))?; + Ok(price.in_eth(fee.into())) + } + + /// Total fee (protocol fee + network fee). Equal to a surplus difference + /// before and after applying the fees. + /// + /// Denominated in SELL token + pub fn fee(&self) -> Result { let fee = self .surplus_over_limit_price_before_fee()? .amount @@ -147,12 +163,9 @@ impl Trade { .ok_or(error::Math::Overflow)? .checked_div(&self.prices.uniform.sell.into()) .ok_or(error::Math::DivisionByZero)?, - }; - - let price = prices - .get(&self.sell.token) - .ok_or(Error::MissingPrice(self.sell.token))?; - Ok(price.in_eth(fee_in_sell_token)) + } + .into(); + Ok(fee_in_sell_token) } /// Protocol fees is defined by fee policies attached to the order. diff --git a/crates/autopilot/src/on_settlement_event_updater.rs b/crates/autopilot/src/on_settlement_event_updater.rs index 931978acf5..729dc5138e 100644 --- a/crates/autopilot/src/on_settlement_event_updater.rs +++ b/crates/autopilot/src/on_settlement_event_updater.rs @@ -191,7 +191,7 @@ impl Inner { }; let domain_separator = self.eth.contracts().settlement_domain_separator(); let settlement = domain::settlement::Settlement::new( - &transaction, + transaction, domain_separator, &self.persistence, ) @@ -199,7 +199,7 @@ impl Inner { tracing::info!( "settlement object {:?}", - settlement.map(|settlement| settlement.score()) + settlement.map(|settlement| (settlement.observation(), settlement.score())) ); }