diff --git a/src/domain/auction.rs b/src/domain/auction.rs index 26ac8cc..3ffca85 100644 --- a/src/domain/auction.rs +++ b/src/domain/auction.rs @@ -82,6 +82,12 @@ impl Price { } } +impl From for U256 { + fn from(value: Price) -> Self { + value.0 .0 + } +} + /// The estimated effective gas price that will likely be used for executing the /// settlement transaction. #[derive(Clone, Copy, Debug)] diff --git a/src/domain/solution.rs b/src/domain/solution.rs index 69a848f..7346391 100644 --- a/src/domain/solution.rs +++ b/src/domain/solution.rs @@ -1,9 +1,15 @@ use { crate::{ - domain::{auction, eth, liquidity, order}, + domain::{ + auction, + eth::{self, TokenAddress}, + liquidity, + order::{self, Side}, + }, util, }, ethereum_types::{Address, U256}, + shared::conversions::U256Ext, std::{collections::HashMap, slice}, }; @@ -232,6 +238,80 @@ pub struct Fulfillment { fee: Fee, } +impl Trade { + fn side(&self) -> Side { + match self { + Trade::Fulfillment(fulfillment) => fulfillment.order.side, + Trade::Jit(jit) => jit.order.side, + } + } + + fn executed(&self) -> U256 { + match self { + Trade::Fulfillment(fulfillment) => fulfillment.executed, + Trade::Jit(jit) => jit.executed, + } + } + + fn fee(&self) -> U256 { + match self { + Trade::Fulfillment(fulfillment) => fulfillment + .surplus_fee() + .map(|surplus| surplus.amount) + .unwrap_or(U256::zero()), + Trade::Jit(_) => U256::zero(), + } + } + + /// Returns the trade sell token + pub fn sell_token(&self) -> TokenAddress { + match self { + Trade::Fulfillment(fulfillment) => fulfillment.order.sell.token, + Trade::Jit(jit) => jit.order.sell.token, + } + } + + /// Returns the trade buy token + pub fn buy_token(&self) -> TokenAddress { + match self { + Trade::Fulfillment(fulfillment) => fulfillment.order.buy.token, + Trade::Jit(jit) => jit.order.sell.token, + } + } + + /// The effective amount that left the user's wallet including all fees. + pub fn sell_amount(&self, sell_price: U256, buy_price: U256) -> Result { + let before_fee = match self.side() { + Side::Sell => self.executed(), + Side::Buy => self + .executed() + .checked_mul(buy_price) + .ok_or(error::Math::Overflow)? + .checked_div(sell_price) + .ok_or(error::Math::DivisionByZero)?, + }; + before_fee + .checked_add(self.fee()) + .ok_or(error::Math::Overflow) + } + + /// The effective amount the user received after all fees. + /// + /// Settlement contract uses `ceil` division for buy amount calculation. + pub fn buy_amount(&self, sell_price: U256, buy_price: U256) -> Result { + let amount = match self.side() { + Side::Buy => self.executed(), + Side::Sell => self + .executed() + .checked_mul(sell_price) + .ok_or(error::Math::Overflow)? + .checked_ceil_div(&buy_price) + .ok_or(error::Math::DivisionByZero)?, + }; + Ok(amount) + } +} + impl Fulfillment { /// Creates a new order filled to the specified amount. Returns `None` if /// the fill amount is incompatible with the order. @@ -297,6 +377,18 @@ impl Fulfillment { } } +pub mod error { + #[derive(Debug, thiserror::Error)] + pub enum Math { + #[error("overflow")] + Overflow, + #[error("division by zero")] + DivisionByZero, + #[error("negative")] + Negative, + } +} + /// The fee that is charged to a user for executing an order. #[derive(Clone, Copy, Debug)] pub enum Fee { diff --git a/src/domain/solver/dex/mod.rs b/src/domain/solver/dex/mod.rs index 5836b98..85f0aab 100644 --- a/src/domain/solver/dex/mod.rs +++ b/src/domain/solver/dex/mod.rs @@ -13,6 +13,7 @@ use { }, infra, }, + ethereum_types::U256, futures::{future, stream, FutureExt, StreamExt}, std::num::NonZeroUsize, tracing::Instrument, @@ -188,7 +189,7 @@ impl Dex { let dex_order = self.fills.dex_order(order, tokens)?; let swap = self.try_solve(order, &dex_order, tokens).await?; let sell = tokens.reference_price(&order.sell.token); - let Some(solution) = swap + let Some(mut solution) = swap .into_solution( order.clone(), gas_price, @@ -202,6 +203,41 @@ impl Dex { return None; }; + solution.trades = solution + .trades + .into_iter() + .filter_map(|trade| { + let (buy_price, sell_price) = match ( + tokens + .get(&trade.buy_token()) + .and_then(|token| token.reference_price), + tokens + .get(&trade.sell_token()) + .and_then(|token| token.reference_price), + ) { + (Some(buy_amount), Some(sell_amount)) => { + (U256::from(buy_amount), U256::from(sell_amount)) + } + _ => return None, + }; + + let buy_amount = trade.buy_amount(buy_price, sell_price).ok()?; + let sell_amount = trade.sell_amount(buy_price, sell_price).ok()?; + + if sell_amount.checked_mul(sell_price)? >= buy_amount.checked_mul(buy_price)? { + Some(trade) + } else { + tracing::warn!( + ?trade, + ?tokens, + "Settlement contract rule not fulfilled: order.sellAmount.mul(sellPrice) \ + >= order.buyAmount.mul(buyPrice)" + ); + None + } + }) + .collect(); + tracing::debug!("solved"); // Maybe some liquidity appeared that enables a bigger fill. self.fills.increase_next_try(order.uid);