From 4dcb2fce3e2b4bd9c7bd1295b8a1604067f45499 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 28 Feb 2024 21:18:50 +0100 Subject: [PATCH 01/31] init commit --- crates/driver/src/boundary/settlement.rs | 111 ++++++++- .../driver/src/domain/competition/auction.rs | 11 + crates/driver/src/domain/competition/mod.rs | 1 + .../src/domain/competition/settled/mod.rs | 234 ++++++++++++++++++ .../domain/competition/solution/settlement.rs | 38 +++ crates/driver/src/domain/eth/mod.rs | 8 + .../driver/src/infra/blockchain/contracts.rs | 17 ++ crates/shared/src/encoded_settlement.rs | 59 +++++ 8 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 crates/driver/src/domain/competition/settled/mod.rs diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs index 398ce72043..5e6e1868ea 100644 --- a/crates/driver/src/boundary/settlement.rs +++ b/crates/driver/src/boundary/settlement.rs @@ -15,7 +15,7 @@ use { infra::Ethereum, util::conv::u256::U256Ext, }, - anyhow::{anyhow, Context, Result}, + anyhow::{anyhow, Context, Ok, Result}, model::{ app_data::AppDataHash, interaction::InteractionData, @@ -34,6 +34,7 @@ use { DomainSeparator, }, shared::{ + encoded_settlement::EncodedSettlement, external_prices::ExternalPrices, http_solver::{ self, @@ -175,6 +176,12 @@ impl Settlement { }) } + pub fn encoded_settlement(&self) -> EncodedSettlement { + self.inner + .clone() + .encode(InternalizationStrategy::EncodeAllInteractions) + } + pub fn tx( &self, auction_id: auction::Id, @@ -240,6 +247,37 @@ impl Settlement { Ok(eth::U256::from_big_rational(&quality)?.into()) } + /// Normalized token prices. + pub fn prices( + &self, + eth: &Ethereum, + auction: &competition::Auction, + ) -> Result, boundary::Error> { + let external_prices = ExternalPrices::try_from_auction_prices( + eth.contracts().weth().address(), + auction + .tokens() + .iter() + .filter_map(|token| { + token + .price + .map(|price| (token.address.into(), price.into())) + }) + .collect(), + )?; + + let prices = auction + .tokens() + .iter() + .fold(HashMap::new(), |mut prices, token| { + if let Some(price) = external_prices.price(&token.address.0 .0) { + prices.insert(token.address, price.clone().into()); + } + prices + }); + Ok(prices) + } + pub fn merge(self, other: Self) -> Result { self.inner.merge(other.inner).map(|inner| Self { inner, @@ -464,6 +502,77 @@ pub fn to_boundary_interaction( } } +pub fn to_domain_settled_settlement( + settlement: &EncodedSettlement, + policies: &HashMap>, + normalized_prices: &HashMap, + domain_separator: ð::DomainSeparator, +) -> Option { + let order_uids = settlement + .uids(model::DomainSeparator(domain_separator.0)) + .ok()?; + let trades = settlement + .trades + .iter() + .zip(order_uids.iter()) + .map(|(trade, uid)| { + let uid = uid.0.into(); + let side = if trade.8.byte(0) & 0b1 == 0 { + order::Side::Sell + } else { + order::Side::Buy + }; + let sell_token_index = trade.0.as_usize(); + let buy_token_index = trade.1.as_usize(); + let sell_token = settlement.tokens[sell_token_index]; + let buy_token = settlement.tokens[buy_token_index]; + let uniform_sell_token_index = settlement + .tokens + .iter() + .position(|token| token == &sell_token) + .unwrap(); + let uniform_buy_token_index = settlement + .tokens + .iter() + .position(|token| token == &buy_token) + .unwrap(); + let sell = eth::Asset { + token: sell_token.into(), + amount: trade.3.into(), + }; + let buy = eth::Asset { + token: buy_token.into(), + amount: trade.4.into(), + }; + let executed = eth::Asset { + token: match side { + order::Side::Sell => sell.token, + order::Side::Buy => buy.token, + }, + amount: trade.9.into(), + }; + let prices = competition::settled::Prices { + uniform: competition::settled::ClearingPrices { + sell: settlement.clearing_prices[uniform_sell_token_index], + buy: settlement.clearing_prices[uniform_buy_token_index], + }, + custom: competition::settled::ClearingPrices { + sell: settlement.clearing_prices[sell_token_index], + buy: settlement.clearing_prices[buy_token_index], + }, + native: competition::settled::NormalizedPrices { + sell: normalized_prices[&sell_token.into()].clone(), + buy: normalized_prices[&buy_token.into()].clone(), + }, + }; + let policies = policies.get(&uid).cloned().unwrap_or_default(); + competition::settled::Trade::new(sell, buy, side, executed, prices, policies) + }) + .collect(); + + Some(competition::settled::Settlement::new(trades)) +} + fn to_big_decimal(value: bigdecimal::BigDecimal) -> num::BigRational { let (x, exp) = value.into_bigint_and_exponent(); let numerator_bytes = x.to_bytes_le(); diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index 5966dfde9b..afabd3b75c 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -12,6 +12,7 @@ use { }, futures::future::{join_all, BoxFuture, FutureExt, Shared}, itertools::Itertools, + num::BigRational, std::{ collections::{HashMap, HashSet}, sync::{Arc, Mutex}, @@ -418,6 +419,16 @@ impl From for Price { } } +/// The price of a token in ETH. Price normalized to native token. +#[derive(Debug, Clone)] +pub struct NormalizedPrice(pub BigRational); + +impl From for NormalizedPrice { + fn from(value: BigRational) -> Self { + Self(value) + } +} + #[derive(Debug, Clone, Copy)] pub struct Id(pub i64); diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index 16286aba18..bc5c753e87 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -25,6 +25,7 @@ use { pub mod auction; pub mod order; pub mod score; +pub mod settled; pub mod solution; pub use { diff --git a/crates/driver/src/domain/competition/settled/mod.rs b/crates/driver/src/domain/competition/settled/mod.rs new file mode 100644 index 0000000000..b938443585 --- /dev/null +++ b/crates/driver/src/domain/competition/settled/mod.rs @@ -0,0 +1,234 @@ +use { + super::{ + auction, + order::{self, Side}, + }, + crate::{domain::eth, util::conv::u256::U256Ext}, + number::conversions::big_rational_to_u256, +}; + +/// Settlement built from onchain calldata. +#[derive(Debug, Clone)] +pub struct Settlement { + trades: Vec, +} + +impl Settlement { + pub fn new(trades: Vec) -> Self { + Self { trades } + } + + /// Score of a settlement as per CIP38 + /// + /// Denominated in NATIVE token + pub fn score(&self) -> Option { + self.trades + .iter() + .map(Trade::score) + .try_fold(eth::TokenAmount(eth::U256::zero()), |acc, score| { + score.map(|score| acc + score) + }) + } +} + +#[derive(Debug, Clone)] +pub struct Trade { + sell: eth::Asset, + buy: eth::Asset, + side: Side, + executed: eth::Asset, + prices: Prices, + policies: Vec, +} + +impl Trade { + pub fn new( + sell: eth::Asset, + buy: eth::Asset, + side: Side, + executed: eth::Asset, + prices: Prices, + policies: Vec, + ) -> Self { + Self { + sell, + buy, + side, + executed, + prices, + policies, + } + } + + /// CIP38 score defined as surplus + protocol fee + /// + /// Denominated in NATIVE token + pub fn score(&self) -> Option { + self.native_surplus() + .zip(self.native_protocol_fee()) + .map(|(surplus, fee)| surplus + fee) + } + + /// Surplus based on custom clearing prices returns the surplus after all + /// fees have been applied. + /// + /// Denominated in SURPLUS token + fn surplus(&self) -> Option { + match self.side { + Side::Buy => { + // scale limit sell to support partially fillable orders + let limit_sell = self + .sell + .amount + .0 + .checked_mul(self.executed.amount.into())? + .checked_div(self.buy.amount.into())?; + // difference between limit sell and executed amount converted to sell token + limit_sell.checked_sub( + self.executed + .amount + .0 + .checked_mul(self.prices.custom.buy)? + .checked_div(self.prices.custom.sell)?, + ) + } + Side::Sell => { + // scale limit buy to support partially fillable orders + let limit_buy = self + .executed + .amount + .0 + .checked_mul(self.buy.amount.into())? + .checked_div(self.sell.amount.into())?; + // difference between executed amount converted to buy token and limit buy + self.executed + .amount + .0 + .checked_mul(self.prices.custom.sell)? + .checked_div(self.prices.custom.buy)? + .checked_sub(limit_buy) + } + } + .map(|surplus| match self.side { + Side::Buy => eth::Asset { + amount: surplus.into(), + token: self.sell.token, + }, + Side::Sell => eth::Asset { + amount: surplus.into(), + token: self.buy.token, + }, + }) + } + + /// Surplus based on custom clearing prices returns the surplus after all + /// fees have been applied. + /// + /// Denominated in NATIVE token + fn native_surplus(&self) -> Option { + big_rational_to_u256( + &(self.surplus()?.amount.0.to_big_rational() * self.surplus_token_price().0), + ) + .map(Into::into) + .ok() + } + + /// Protocol fee is defined by fee policies attached to the order. + /// + /// Denominated in SURPLUS token + fn protocol_fee(&self) -> Option { + // TODO: support multiple fee policies + if self.policies.len() > 1 { + return None; + } + + match self.policies.first()? { + order::FeePolicy::Surplus { + factor, + max_volume_factor, + } => Some(eth::Asset { + token: match self.side { + Side::Sell => self.buy.token, + Side::Buy => self.sell.token, + }, + amount: std::cmp::min( + { + // If the surplus after all fees is X, then the original + // surplus before protocol fee is X / (1 - factor) + apply_factor(self.surplus()?.amount.into(), factor / (1.0 - factor))? + }, + { + // Convert the executed amount to surplus token so it can be compared with + // the surplus + let executed_in_surplus_token = match self.side { + Side::Sell => { + self.executed.amount.0 * self.prices.custom.sell + / self.prices.custom.buy + } + Side::Buy => { + self.executed.amount.0 * self.prices.custom.buy + / self.prices.custom.sell + } + }; + apply_factor(executed_in_surplus_token, *max_volume_factor)? + }, + ) + .into(), + }), + order::FeePolicy::PriceImprovement { + factor: _, + max_volume_factor: _, + quote: _, + } => todo!(), + order::FeePolicy::Volume { factor: _ } => todo!(), + } + } + + /// Protocol fee is defined by fee policies attached to the order. + /// + /// Denominated in NATIVE token + fn native_protocol_fee(&self) -> Option { + big_rational_to_u256( + &(self.protocol_fee()?.amount.0.to_big_rational() * self.surplus_token_price().0), + ) + .map(Into::into) + .ok() + } + + /// Returns the normalized price of the trade surplus token + fn surplus_token_price(&self) -> auction::NormalizedPrice { + match self.side { + Side::Buy => self.prices.native.sell.clone(), + Side::Sell => self.prices.native.buy.clone(), + } + } +} + +fn apply_factor(amount: eth::U256, factor: f64) -> Option { + Some( + amount.checked_mul(eth::U256::from_f64_lossy(factor * 1000000000000.))? / 1000000000000u128, + ) +} + +#[derive(Debug, Clone)] +pub struct Prices { + pub uniform: ClearingPrices, + /// Adjusted uniform prices to account for fees (gas cost and protocol fees) + pub custom: ClearingPrices, + /// Prices normalized to the same native token (ETH) + pub native: NormalizedPrices, +} + +/// Uniform clearing prices at which the trade was executed. +#[derive(Debug, Clone)] +pub struct ClearingPrices { + pub sell: eth::U256, + pub buy: eth::U256, +} + +/// Normalized prices to the same native token (ETH) +#[derive(Debug, Clone)] +pub struct NormalizedPrices { + pub sell: auction::NormalizedPrice, + pub buy: auction::NormalizedPrice, +} diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 938cab4f17..ad1989cdf9 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -248,6 +248,22 @@ impl Settlement { } }; + { + // For testing purposes, always calculate CIP38 score, even if the score is not + // used. + match self + .settled(ð, auction) + .and_then(|settled| settled.score()) + { + Some(score) => { + tracing::info!(?score, "CIP38 score"); + } + None => { + tracing::warn!("could not calculate CIP38 score"); + } + } + } + if score > quality { return Err(score::Error::ScoreHigherThanQuality(score, quality)); } @@ -341,6 +357,28 @@ impl Settlement { _ => None, } } + + /// Settlement built from the settlement transaction calldata + fn settled( + &self, + eth: &Ethereum, + auction: &competition::Auction, + ) -> Option { + boundary::settlement::to_domain_settled_settlement( + &self.boundary.encoded_settlement(), + &self + .solutions + .values() + .flat_map(|solution| { + solution + .user_trades() + .map(|trade| (trade.order().uid, trade.order().protocol_fees.clone())) + }) + .collect(), + &self.boundary.prices(eth, auction).ok()?, + eth.contracts().settlement_domain_separator(), + ) + } } /// Should the interactions be internalized? diff --git a/crates/driver/src/domain/eth/mod.rs b/crates/driver/src/domain/eth/mod.rs index 4745ddf51a..2ccc8681a4 100644 --- a/crates/driver/src/domain/eth/mod.rs +++ b/crates/driver/src/domain/eth/mod.rs @@ -195,6 +195,14 @@ impl From for TokenAmount { } } +impl std::ops::Add for TokenAmount { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + impl std::fmt::Display for TokenAmount { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) diff --git a/crates/driver/src/infra/blockchain/contracts.rs b/crates/driver/src/infra/blockchain/contracts.rs index fef7ee73bd..8e99d07620 100644 --- a/crates/driver/src/infra/blockchain/contracts.rs +++ b/crates/driver/src/infra/blockchain/contracts.rs @@ -10,6 +10,9 @@ pub struct Contracts { vault_relayer: eth::ContractAddress, vault: contracts::BalancerV2Vault, weth: contracts::WETH9, + + /// The domain separator for settlement contract used for signing orders. + settlement_domain_separator: eth::DomainSeparator, } #[derive(Debug, Default, Clone, Copy)] @@ -48,11 +51,21 @@ impl Contracts { address_for(contracts::WETH9::raw_contract(), addresses.weth), ); + let settlement_domain_separator = eth::DomainSeparator( + settlement + .domain_separator() + .call() + .await + .expect("domain separator") + .0, + ); + Ok(Self { settlement, vault_relayer, vault, weth, + settlement_domain_separator, }) } @@ -75,6 +88,10 @@ impl Contracts { pub fn weth_address(&self) -> eth::WethAddress { self.weth.address().into() } + + pub fn settlement_domain_separator(&self) -> ð::DomainSeparator { + &self.settlement_domain_separator + } } /// Returns the address of a contract for the specified network, or `None` if diff --git a/crates/shared/src/encoded_settlement.rs b/crates/shared/src/encoded_settlement.rs index a0646e843e..cf1ae27d1c 100644 --- a/crates/shared/src/encoded_settlement.rs +++ b/crates/shared/src/encoded_settlement.rs @@ -1,5 +1,6 @@ use { crate::interaction::EncodedInteraction, + anyhow::Result, ethcontract::Bytes, model::{ order::{BuyTokenDestination, OrderData, OrderKind, SellTokenSource}, @@ -84,6 +85,64 @@ pub struct EncodedSettlement { pub interactions: [Vec; 3], } +impl EncodedSettlement { + /// Order uids for all trades in the settlement. + /// + /// Returns all order uids or none. + pub fn uids( + &self, + domain_separator: model::DomainSeparator, + ) -> Result> { + self.trades + .iter() + .map(|trade| { + let order = model::order::OrderData { + sell_token: self.tokens[trade.0.as_u64() as usize], + buy_token: self.tokens[trade.1.as_u64() as usize], + sell_amount: trade.3, + buy_amount: trade.4, + valid_to: trade.5, + app_data: model::app_data::AppDataHash(trade.6 .0), + fee_amount: trade.7, + kind: if trade.8.byte(0) & 0b1 == 0 { + model::order::OrderKind::Sell + } else { + model::order::OrderKind::Buy + }, + partially_fillable: trade.8.byte(0) & 0b10 != 0, + receiver: Some(trade.2), + sell_token_balance: if trade.8.byte(0) & 0x08 == 0 { + model::order::SellTokenSource::Erc20 + } else if trade.8.byte(0) & 0x04 == 0 { + model::order::SellTokenSource::External + } else { + model::order::SellTokenSource::Internal + }, + buy_token_balance: if trade.8.byte(0) & 0x10 == 0 { + model::order::BuyTokenDestination::Erc20 + } else { + model::order::BuyTokenDestination::Internal + }, + }; + let signing_scheme = match trade.8.byte(0) >> 5 { + 0b00 => model::signature::SigningScheme::Eip712, + 0b01 => model::signature::SigningScheme::EthSign, + 0b10 => model::signature::SigningScheme::Eip1271, + 0b11 => model::signature::SigningScheme::PreSign, + _ => unreachable!(), + }; + let signature = Signature::from_bytes(signing_scheme, &trade.10 .0)?; + let owner = signature.recover_owner( + &signature.to_bytes(), + &domain_separator, + &order.hash_struct(), + )?; + Ok(order.uid(&domain_separator, &owner)) + }) + .collect() + } +} + #[cfg(test)] mod tests { use {super::*, ethcontract::H256, hex_literal::hex, model::signature::EcdsaSignature}; From baab38265488a9cdf068e7213962669db63368ba Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 29 Feb 2024 00:50:33 +0100 Subject: [PATCH 02/31] Few fixes --- crates/driver/src/domain/competition/settled/mod.rs | 8 ++++++-- crates/driver/src/domain/competition/solution/fee.rs | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/driver/src/domain/competition/settled/mod.rs b/crates/driver/src/domain/competition/settled/mod.rs index b938443585..407c27d10e 100644 --- a/crates/driver/src/domain/competition/settled/mod.rs +++ b/crates/driver/src/domain/competition/settled/mod.rs @@ -170,7 +170,10 @@ impl Trade { / self.prices.custom.sell } }; - apply_factor(executed_in_surplus_token, *max_volume_factor)? + apply_factor( + executed_in_surplus_token, + max_volume_factor / (1.0 - max_volume_factor), + )? }, ) .into(), @@ -206,7 +209,8 @@ impl Trade { fn apply_factor(amount: eth::U256, factor: f64) -> Option { Some( - amount.checked_mul(eth::U256::from_f64_lossy(factor * 1000000000000.))? / 1000000000000u128, + amount.checked_mul(eth::U256::from_f64_lossy(factor * 1000000000000000000.))? + / 1000000000000000000u128, ) } diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 73ad29d224..2c73edd9bc 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -170,9 +170,9 @@ impl Fulfillment { fn apply_factor(amount: eth::U256, factor: f64) -> Result { Ok(amount - .checked_mul(eth::U256::from_f64_lossy(factor * 10000.)) + .checked_mul(eth::U256::from_f64_lossy(factor * 1000000000000000000.)) .ok_or(trade::Error::Overflow)? - / 10000) + / 1000000000000000000u128) } /// This function adjusts quote amounts to directly compare them with the From cfd645f06bd14dc1cc3349ef94a10d558187dd8d Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 29 Feb 2024 10:24:16 +0100 Subject: [PATCH 03/31] small --- crates/driver/src/boundary/settlement.rs | 4 ++-- crates/driver/src/domain/competition/solution/settlement.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs index 5e6e1868ea..5238be6601 100644 --- a/crates/driver/src/boundary/settlement.rs +++ b/crates/driver/src/boundary/settlement.rs @@ -502,11 +502,11 @@ pub fn to_boundary_interaction( } } -pub fn to_domain_settled_settlement( +pub fn to_domain_settlement( settlement: &EncodedSettlement, - policies: &HashMap>, normalized_prices: &HashMap, domain_separator: ð::DomainSeparator, + policies: &HashMap>, ) -> Option { let order_uids = settlement .uids(model::DomainSeparator(domain_separator.0)) diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index ad1989cdf9..ffd0efa163 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -364,8 +364,10 @@ impl Settlement { eth: &Ethereum, auction: &competition::Auction, ) -> Option { - boundary::settlement::to_domain_settled_settlement( + boundary::settlement::to_domain_settlement( &self.boundary.encoded_settlement(), + &self.boundary.prices(eth, auction).ok()?, + eth.contracts().settlement_domain_separator(), &self .solutions .values() @@ -375,8 +377,6 @@ impl Settlement { .map(|trade| (trade.order().uid, trade.order().protocol_fees.clone())) }) .collect(), - &self.boundary.prices(eth, auction).ok()?, - eth.contracts().settlement_domain_separator(), ) } } From e399b3b16097e5ee401316c40350a30c7653e755 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 29 Feb 2024 13:19:52 +0100 Subject: [PATCH 04/31] refactor, fixes --- crates/driver/src/boundary/settlement.rs | 177 +++++++++++------- .../src/domain/competition/settled/mod.rs | 34 +++- .../src/domain/competition/solution/fee.rs | 19 +- .../domain/competition/solution/settlement.rs | 24 +-- 4 files changed, 159 insertions(+), 95 deletions(-) diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs index 5238be6601..5d127627f6 100644 --- a/crates/driver/src/boundary/settlement.rs +++ b/crates/driver/src/boundary/settlement.rs @@ -296,6 +296,112 @@ impl Settlement { pub fn revertable(&self) -> bool { self.inner.revertable() != Revertable::NoRisk } + + pub fn settled( + &self, + eth: &Ethereum, + auction: &competition::Auction, + policies: &HashMap>, + ) -> Option { + let external_prices = ExternalPrices::try_from_auction_prices( + eth.contracts().weth().address(), + auction + .tokens() + .iter() + .filter_map(|token| { + token + .price + .map(|price| (token.address.into(), price.into())) + }) + .collect(), + ) + .ok()?; + + // TODO: Add normalized prices to the competition::Auction. Abandon + // ExternalPrices + let normalized_prices: HashMap = auction + .tokens() + .iter() + .fold(HashMap::new(), |mut prices, token| { + if let Some(price) = external_prices.price(&token.address.0 .0) { + prices.insert(token.address, price.clone().into()); + } + prices + }); + + let settlement = self + .inner + .clone() + .encode(InternalizationStrategy::EncodeAllInteractions); + + let order_uids = settlement + .uids(model::DomainSeparator( + eth.contracts().settlement_domain_separator().0, + )) + .ok()?; + + // TODO: build trades from fulfillments directly + let trades = settlement + .trades + .iter() + .zip(order_uids.iter()) + .map(|(trade, uid)| { + let uid = uid.0.into(); + let side = if trade.8.byte(0) & 0b1 == 0 { + order::Side::Sell + } else { + order::Side::Buy + }; + let sell_token_index = trade.0.as_usize(); + let buy_token_index = trade.1.as_usize(); + let sell_token = settlement.tokens[sell_token_index]; + let buy_token = settlement.tokens[buy_token_index]; + let uniform_sell_token_index = settlement + .tokens + .iter() + .position(|token| token == &sell_token) + .unwrap(); + let uniform_buy_token_index = settlement + .tokens + .iter() + .position(|token| token == &buy_token) + .unwrap(); + let sell = eth::Asset { + token: sell_token.into(), + amount: trade.3.into(), + }; + let buy = eth::Asset { + token: buy_token.into(), + amount: trade.4.into(), + }; + let executed = eth::Asset { + token: match side { + order::Side::Sell => sell.token, + order::Side::Buy => buy.token, + }, + amount: trade.9.into(), + }; + let prices = competition::settled::Prices { + uniform: competition::settled::ClearingPrices { + sell: settlement.clearing_prices[uniform_sell_token_index], + buy: settlement.clearing_prices[uniform_buy_token_index], + }, + custom: competition::settled::ClearingPrices { + sell: settlement.clearing_prices[sell_token_index], + buy: settlement.clearing_prices[buy_token_index], + }, + native: competition::settled::NormalizedPrices { + sell: normalized_prices[&sell_token.into()].clone(), + buy: normalized_prices[&buy_token.into()].clone(), + }, + }; + let policies = policies.get(&uid).cloned().unwrap_or_default(); + competition::settled::Trade::new(sell, buy, side, executed, prices, policies) + }) + .collect(); + + Some(competition::settled::Settlement::new(trades)) + } } fn to_boundary_order(order: &competition::Order) -> Order { @@ -502,77 +608,6 @@ pub fn to_boundary_interaction( } } -pub fn to_domain_settlement( - settlement: &EncodedSettlement, - normalized_prices: &HashMap, - domain_separator: ð::DomainSeparator, - policies: &HashMap>, -) -> Option { - let order_uids = settlement - .uids(model::DomainSeparator(domain_separator.0)) - .ok()?; - let trades = settlement - .trades - .iter() - .zip(order_uids.iter()) - .map(|(trade, uid)| { - let uid = uid.0.into(); - let side = if trade.8.byte(0) & 0b1 == 0 { - order::Side::Sell - } else { - order::Side::Buy - }; - let sell_token_index = trade.0.as_usize(); - let buy_token_index = trade.1.as_usize(); - let sell_token = settlement.tokens[sell_token_index]; - let buy_token = settlement.tokens[buy_token_index]; - let uniform_sell_token_index = settlement - .tokens - .iter() - .position(|token| token == &sell_token) - .unwrap(); - let uniform_buy_token_index = settlement - .tokens - .iter() - .position(|token| token == &buy_token) - .unwrap(); - let sell = eth::Asset { - token: sell_token.into(), - amount: trade.3.into(), - }; - let buy = eth::Asset { - token: buy_token.into(), - amount: trade.4.into(), - }; - let executed = eth::Asset { - token: match side { - order::Side::Sell => sell.token, - order::Side::Buy => buy.token, - }, - amount: trade.9.into(), - }; - let prices = competition::settled::Prices { - uniform: competition::settled::ClearingPrices { - sell: settlement.clearing_prices[uniform_sell_token_index], - buy: settlement.clearing_prices[uniform_buy_token_index], - }, - custom: competition::settled::ClearingPrices { - sell: settlement.clearing_prices[sell_token_index], - buy: settlement.clearing_prices[buy_token_index], - }, - native: competition::settled::NormalizedPrices { - sell: normalized_prices[&sell_token.into()].clone(), - buy: normalized_prices[&buy_token.into()].clone(), - }, - }; - let policies = policies.get(&uid).cloned().unwrap_or_default(); - competition::settled::Trade::new(sell, buy, side, executed, prices, policies) - }) - .collect(); - - Some(competition::settled::Settlement::new(trades)) -} - fn to_big_decimal(value: bigdecimal::BigDecimal) -> num::BigRational { let (x, exp) = value.into_bigint_and_exponent(); let numerator_bytes = x.to_bytes_le(); diff --git a/crates/driver/src/domain/competition/settled/mod.rs b/crates/driver/src/domain/competition/settled/mod.rs index 407c27d10e..b9b2bdfd01 100644 --- a/crates/driver/src/domain/competition/settled/mod.rs +++ b/crates/driver/src/domain/competition/settled/mod.rs @@ -172,7 +172,10 @@ impl Trade { }; apply_factor( executed_in_surplus_token, - max_volume_factor / (1.0 - max_volume_factor), + match self.side { + Side::Sell => max_volume_factor / (1.0 - max_volume_factor), + Side::Buy => max_volume_factor / (1.0 + max_volume_factor), + }, )? }, ) @@ -183,7 +186,34 @@ impl Trade { max_volume_factor: _, quote: _, } => todo!(), - order::FeePolicy::Volume { factor: _ } => todo!(), + order::FeePolicy::Volume { factor } => Some(eth::Asset { + token: match self.side { + Side::Sell => self.buy.token, + Side::Buy => self.sell.token, + }, + amount: { + // Convert the executed amount to surplus token so it can be compared with + // the surplus + let executed_in_surplus_token = match self.side { + Side::Sell => { + self.executed.amount.0 * self.prices.custom.sell + / self.prices.custom.buy + } + Side::Buy => { + self.executed.amount.0 * self.prices.custom.buy + / self.prices.custom.sell + } + }; + apply_factor( + executed_in_surplus_token, + match self.side { + Side::Sell => factor / (1.0 - factor), + Side::Buy => factor / (1.0 + factor), + }, + )? + } + .into(), + }), } } diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 2c73edd9bc..b2ea9d05f5 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -156,14 +156,17 @@ impl Fulfillment { Side::Sell => executed, }; // Sell slightly more `sell_token` to capture the `surplus_fee` - let executed_sell_amount_with_fee = executed_sell_amount - .checked_add( - // surplus_fee is always expressed in sell token - self.surplus_fee() - .map(|fee| fee.0) - .ok_or(trade::Error::ProtocolFeeOnStaticOrder)?, - ) - .ok_or(trade::Error::Overflow)?; + let executed_sell_amount_with_fee = match self.order().side { + Side::Buy => executed_sell_amount + .checked_add( + // surplus_fee is always expressed in sell token + self.surplus_fee() + .map(|fee| fee.0) + .ok_or(trade::Error::ProtocolFeeOnStaticOrder)?, + ) + .ok_or(trade::Error::Overflow)?, + Side::Sell => executed_sell_amount, + }; apply_factor(executed_sell_amount_with_fee, factor) } } diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index ffd0efa163..5fff2d04eb 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -364,20 +364,16 @@ impl Settlement { eth: &Ethereum, auction: &competition::Auction, ) -> Option { - boundary::settlement::to_domain_settlement( - &self.boundary.encoded_settlement(), - &self.boundary.prices(eth, auction).ok()?, - eth.contracts().settlement_domain_separator(), - &self - .solutions - .values() - .flat_map(|solution| { - solution - .user_trades() - .map(|trade| (trade.order().uid, trade.order().protocol_fees.clone())) - }) - .collect(), - ) + let policies = &self + .solutions + .values() + .flat_map(|solution| { + solution + .user_trades() + .map(|trade| (trade.order().uid, trade.order().protocol_fees.clone())) + }) + .collect(); + self.boundary.settled(eth, auction, policies) } } From 75582862b3c10058119dd9cc0ae78b582f4747f1 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 29 Feb 2024 13:22:29 +0100 Subject: [PATCH 05/31] remove unused --- crates/driver/src/boundary/settlement.rs | 37 ------------------------ 1 file changed, 37 deletions(-) diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs index 5d127627f6..b4ab4bc4bc 100644 --- a/crates/driver/src/boundary/settlement.rs +++ b/crates/driver/src/boundary/settlement.rs @@ -176,12 +176,6 @@ impl Settlement { }) } - pub fn encoded_settlement(&self) -> EncodedSettlement { - self.inner - .clone() - .encode(InternalizationStrategy::EncodeAllInteractions) - } - pub fn tx( &self, auction_id: auction::Id, @@ -247,37 +241,6 @@ impl Settlement { Ok(eth::U256::from_big_rational(&quality)?.into()) } - /// Normalized token prices. - pub fn prices( - &self, - eth: &Ethereum, - auction: &competition::Auction, - ) -> Result, boundary::Error> { - let external_prices = ExternalPrices::try_from_auction_prices( - eth.contracts().weth().address(), - auction - .tokens() - .iter() - .filter_map(|token| { - token - .price - .map(|price| (token.address.into(), price.into())) - }) - .collect(), - )?; - - let prices = auction - .tokens() - .iter() - .fold(HashMap::new(), |mut prices, token| { - if let Some(price) = external_prices.price(&token.address.0 .0) { - prices.insert(token.address, price.clone().into()); - } - prices - }); - Ok(prices) - } - pub fn merge(self, other: Self) -> Result { self.inner.merge(other.inner).map(|inner| Self { inner, From 0f67ca60b581b343fc5e9be80da481ca78914f1d Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 29 Feb 2024 13:27:14 +0100 Subject: [PATCH 06/31] fix comment --- crates/driver/src/boundary/settlement.rs | 1 - crates/driver/src/domain/competition/auction.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs index b4ab4bc4bc..183ccb2cb7 100644 --- a/crates/driver/src/boundary/settlement.rs +++ b/crates/driver/src/boundary/settlement.rs @@ -34,7 +34,6 @@ use { DomainSeparator, }, shared::{ - encoded_settlement::EncodedSettlement, external_prices::ExternalPrices, http_solver::{ self, diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index afabd3b75c..6ea8ab5ef0 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -419,7 +419,7 @@ impl From for Price { } } -/// The price of a token in ETH. Price normalized to native token. +/// The price of a token normalized to native token. #[derive(Debug, Clone)] pub struct NormalizedPrice(pub BigRational); From 31468824087b7a2b8f753af2e1773f44826b2529 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 29 Feb 2024 13:32:55 +0100 Subject: [PATCH 07/31] more comments --- crates/driver/src/boundary/settlement.rs | 1 + crates/shared/src/encoded_settlement.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs index 183ccb2cb7..14ee28d98e 100644 --- a/crates/driver/src/boundary/settlement.rs +++ b/crates/driver/src/boundary/settlement.rs @@ -296,6 +296,7 @@ impl Settlement { .clone() .encode(InternalizationStrategy::EncodeAllInteractions); + // Same length and order as in the `settlement.trades` let order_uids = settlement .uids(model::DomainSeparator( eth.contracts().settlement_domain_separator().0, diff --git a/crates/shared/src/encoded_settlement.rs b/crates/shared/src/encoded_settlement.rs index cf1ae27d1c..1a50218c13 100644 --- a/crates/shared/src/encoded_settlement.rs +++ b/crates/shared/src/encoded_settlement.rs @@ -88,7 +88,8 @@ pub struct EncodedSettlement { impl EncodedSettlement { /// Order uids for all trades in the settlement. /// - /// Returns all order uids or none. + /// Returns order uids for all trades in the settlement in the same order as + /// the trades. pub fn uids( &self, domain_separator: model::DomainSeparator, From f7c46f5a345b02d4305b86c9f43dc03f6f773beb Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 29 Feb 2024 14:05:52 +0100 Subject: [PATCH 08/31] fix tests --- crates/e2e/tests/e2e/protocol_fee.rs | 37 ++++++++++++++++------------ 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/crates/e2e/tests/e2e/protocol_fee.rs b/crates/e2e/tests/e2e/protocol_fee.rs index fd461a505d..88d8f92ab5 100644 --- a/crates/e2e/tests/e2e/protocol_fee.rs +++ b/crates/e2e/tests/e2e/protocol_fee.rs @@ -88,24 +88,25 @@ async fn surplus_fee_sell_order_capped_test(web3: Web3) { max_volume_factor: 0.1, }; // Without protocol fee: - // Expected executed_surplus_fee is 167058994203399 + // Expected execution is 10000000000000000000 GNO for + // 9871415430342266811 DAI, with executed_surplus_fee = 167058994203399 GNO // // With protocol fee: // Expected executed_surplus_fee is 167058994203399 + - // 0.1*10000000000000000000 = 1000167058994203400 + // 0.1*(10000000000000000000 - 167058994203399) = 1000150353094783059 // - // Final execution is 10000000000000000000 GNO for 8884257395945205588 DAI, with - // executed_surplus_fee = 1000167058994203400 GNO + // Final execution is 10000000000000000000 GNO for 8884273887308040129 DAI, with + // executed_surplus_fee = 1000150353094783059 GNO // - // Settlement contract balance after execution = 1000167058994203400 GNO = - // 1000167058994203400 GNO * 8884257395945205588 / (10000000000000000000 - - // 1000167058994203400) = 987322948025407485 DAI + // Settlement contract balance after execution = 1000150353094783059 GNO = + // 1000150353094783059 GNO * 8884273887308040129 / (10000000000000000000 - + // 1000150353094783059) = 987306456662572858 DAI execute_test( web3.clone(), fee_policy, OrderKind::Sell, - 1000167058994203400u128.into(), - 987322948025407485u128.into(), + 1000150353094783059u128.into(), + 987306456662572858u128.into(), ) .await; } @@ -113,21 +114,25 @@ async fn surplus_fee_sell_order_capped_test(web3: Web3) { async fn volume_fee_sell_order_test(web3: Web3) { let fee_policy = FeePolicyKind::Volume { factor: 0.1 }; // Without protocol fee: - // Expected executed_surplus_fee is 167058994203399 + // Expected execution is 10000000000000000000 GNO for + // 9871415430342266811 DAI, with executed_surplus_fee = 167058994203399 GNO // // With protocol fee: // Expected executed_surplus_fee is 167058994203399 + - // 0.1*10000000000000000000 = 1000167058994203400 + // 0.1*(10000000000000000000 - 167058994203399) = 1000150353094783059 + // + // Final execution is 10000000000000000000 GNO for 8884273887308040129 DAI, with + // executed_surplus_fee = 1000150353094783059 GNO // - // Settlement contract balance after execution = 1000167058994203400 GNO = - // 1000167058994203400 GNO * 8884257395945205588 / (10000000000000000000 - - // 1000167058994203400) = 987322948025407485 DAI + // Settlement contract balance after execution = 1000150353094783059 GNO = + // 1000150353094783059 GNO * 8884273887308040129 / (10000000000000000000 - + // 1000150353094783059) = 987306456662572858 DAI execute_test( web3.clone(), fee_policy, OrderKind::Sell, - 1000167058994203400u128.into(), - 987322948025407485u128.into(), + 1000150353094783059u128.into(), + 987306456662572858u128.into(), ) .await; } From 31939473ce14eda470f5ae7ce90c79ede27fd320 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 29 Feb 2024 15:08:39 +0100 Subject: [PATCH 09/31] fix bug --- crates/shared/src/encoded_settlement.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/shared/src/encoded_settlement.rs b/crates/shared/src/encoded_settlement.rs index 1a50218c13..8ead05ebf0 100644 --- a/crates/shared/src/encoded_settlement.rs +++ b/crates/shared/src/encoded_settlement.rs @@ -134,7 +134,7 @@ impl EncodedSettlement { }; let signature = Signature::from_bytes(signing_scheme, &trade.10 .0)?; let owner = signature.recover_owner( - &signature.to_bytes(), + &trade.10 .0, &domain_separator, &order.hash_struct(), )?; From 281f5aed93a0d83f2082f4ba89c5bc65a1bfe5e0 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Thu, 29 Feb 2024 19:21:21 +0100 Subject: [PATCH 10/31] build settled directly from solution --- crates/driver/src/boundary/settlement.rs | 107 ------------------ .../driver/src/domain/competition/auction.rs | 20 +++- .../src/domain/competition/settled/mod.rs | 70 ++++++------ .../src/domain/competition/solution/mod.rs | 50 ++++++++ .../domain/competition/solution/settlement.rs | 37 ++---- 5 files changed, 112 insertions(+), 172 deletions(-) diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs index 14ee28d98e..ed4b585689 100644 --- a/crates/driver/src/boundary/settlement.rs +++ b/crates/driver/src/boundary/settlement.rs @@ -258,113 +258,6 @@ impl Settlement { pub fn revertable(&self) -> bool { self.inner.revertable() != Revertable::NoRisk } - - pub fn settled( - &self, - eth: &Ethereum, - auction: &competition::Auction, - policies: &HashMap>, - ) -> Option { - let external_prices = ExternalPrices::try_from_auction_prices( - eth.contracts().weth().address(), - auction - .tokens() - .iter() - .filter_map(|token| { - token - .price - .map(|price| (token.address.into(), price.into())) - }) - .collect(), - ) - .ok()?; - - // TODO: Add normalized prices to the competition::Auction. Abandon - // ExternalPrices - let normalized_prices: HashMap = auction - .tokens() - .iter() - .fold(HashMap::new(), |mut prices, token| { - if let Some(price) = external_prices.price(&token.address.0 .0) { - prices.insert(token.address, price.clone().into()); - } - prices - }); - - let settlement = self - .inner - .clone() - .encode(InternalizationStrategy::EncodeAllInteractions); - - // Same length and order as in the `settlement.trades` - let order_uids = settlement - .uids(model::DomainSeparator( - eth.contracts().settlement_domain_separator().0, - )) - .ok()?; - - // TODO: build trades from fulfillments directly - let trades = settlement - .trades - .iter() - .zip(order_uids.iter()) - .map(|(trade, uid)| { - let uid = uid.0.into(); - let side = if trade.8.byte(0) & 0b1 == 0 { - order::Side::Sell - } else { - order::Side::Buy - }; - let sell_token_index = trade.0.as_usize(); - let buy_token_index = trade.1.as_usize(); - let sell_token = settlement.tokens[sell_token_index]; - let buy_token = settlement.tokens[buy_token_index]; - let uniform_sell_token_index = settlement - .tokens - .iter() - .position(|token| token == &sell_token) - .unwrap(); - let uniform_buy_token_index = settlement - .tokens - .iter() - .position(|token| token == &buy_token) - .unwrap(); - let sell = eth::Asset { - token: sell_token.into(), - amount: trade.3.into(), - }; - let buy = eth::Asset { - token: buy_token.into(), - amount: trade.4.into(), - }; - let executed = eth::Asset { - token: match side { - order::Side::Sell => sell.token, - order::Side::Buy => buy.token, - }, - amount: trade.9.into(), - }; - let prices = competition::settled::Prices { - uniform: competition::settled::ClearingPrices { - sell: settlement.clearing_prices[uniform_sell_token_index], - buy: settlement.clearing_prices[uniform_buy_token_index], - }, - custom: competition::settled::ClearingPrices { - sell: settlement.clearing_prices[sell_token_index], - buy: settlement.clearing_prices[buy_token_index], - }, - native: competition::settled::NormalizedPrices { - sell: normalized_prices[&sell_token.into()].clone(), - buy: normalized_prices[&buy_token.into()].clone(), - }, - }; - let policies = policies.get(&uid).cloned().unwrap_or_default(); - competition::settled::Trade::new(sell, buy, side, executed, prices, policies) - }) - .collect(); - - Some(competition::settled::Settlement::new(trades)) - } } fn to_boundary_order(order: &competition::Order) -> Order { diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index 6ea8ab5ef0..31736b8dd6 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -8,7 +8,7 @@ use { time, }, infra::{self, blockchain, observe, Ethereum}, - util, + util::{self, conv::u256::U256Ext}, }, futures::future::{join_all, BoxFuture, FutureExt, Shared}, itertools::Itertools, @@ -113,6 +113,24 @@ impl Auction { pub fn score_cap(&self) -> Score { self.score_cap } + + pub fn normalized_prices(&self) -> HashMap { + self.tokens + .0 + .iter() + .filter_map(|(address, token)| { + token.price.map(|price| { + ( + *address, + auction::NormalizedPrice( + price.0 .0.to_big_rational() + / BigRational::from_integer(1_000_000_000_000_000_000_u128.into()), // TODO polish + ), + ) + }) + }) + .collect() + } } #[derive(Clone)] diff --git a/crates/driver/src/domain/competition/settled/mod.rs b/crates/driver/src/domain/competition/settled/mod.rs index b9b2bdfd01..3714cae637 100644 --- a/crates/driver/src/domain/competition/settled/mod.rs +++ b/crates/driver/src/domain/competition/settled/mod.rs @@ -5,6 +5,7 @@ use { }, crate::{domain::eth, util::conv::u256::U256Ext}, number::conversions::big_rational_to_u256, + std::collections::HashMap, }; /// Settlement built from onchain calldata. @@ -21,10 +22,13 @@ impl Settlement { /// Score of a settlement as per CIP38 /// /// Denominated in NATIVE token - pub fn score(&self) -> Option { + pub fn score( + &self, + prices: &HashMap, + ) -> Option { self.trades .iter() - .map(Trade::score) + .map(|t| t.score(prices)) .try_fold(eth::TokenAmount(eth::U256::zero()), |acc, score| { score.map(|score| acc + score) }) @@ -36,7 +40,7 @@ pub struct Trade { sell: eth::Asset, buy: eth::Asset, side: Side, - executed: eth::Asset, + executed: order::TargetAmount, prices: Prices, policies: Vec, } @@ -46,7 +50,7 @@ impl Trade { sell: eth::Asset, buy: eth::Asset, side: Side, - executed: eth::Asset, + executed: order::TargetAmount, prices: Prices, policies: Vec, ) -> Self { @@ -63,9 +67,12 @@ impl Trade { /// CIP38 score defined as surplus + protocol fee /// /// Denominated in NATIVE token - pub fn score(&self) -> Option { - self.native_surplus() - .zip(self.native_protocol_fee()) + pub fn score( + &self, + prices: &HashMap, + ) -> Option { + self.native_surplus(prices) + .zip(self.native_protocol_fee(prices)) .map(|(surplus, fee)| surplus + fee) } @@ -81,12 +88,11 @@ impl Trade { .sell .amount .0 - .checked_mul(self.executed.amount.into())? + .checked_mul(self.executed.into())? .checked_div(self.buy.amount.into())?; // difference between limit sell and executed amount converted to sell token limit_sell.checked_sub( self.executed - .amount .0 .checked_mul(self.prices.custom.buy)? .checked_div(self.prices.custom.sell)?, @@ -96,13 +102,11 @@ impl Trade { // scale limit buy to support partially fillable orders let limit_buy = self .executed - .amount .0 .checked_mul(self.buy.amount.into())? .checked_div(self.sell.amount.into())?; // difference between executed amount converted to buy token and limit buy self.executed - .amount .0 .checked_mul(self.prices.custom.sell)? .checked_div(self.prices.custom.buy)? @@ -125,9 +129,12 @@ impl Trade { /// fees have been applied. /// /// Denominated in NATIVE token - fn native_surplus(&self) -> Option { + fn native_surplus( + &self, + prices: &HashMap, + ) -> Option { big_rational_to_u256( - &(self.surplus()?.amount.0.to_big_rational() * self.surplus_token_price().0), + &(self.surplus()?.amount.0.to_big_rational() * self.surplus_token_price(prices).0), ) .map(Into::into) .ok() @@ -162,12 +169,10 @@ impl Trade { // the surplus let executed_in_surplus_token = match self.side { Side::Sell => { - self.executed.amount.0 * self.prices.custom.sell - / self.prices.custom.buy + self.executed.0 * self.prices.custom.sell / self.prices.custom.buy } Side::Buy => { - self.executed.amount.0 * self.prices.custom.buy - / self.prices.custom.sell + self.executed.0 * self.prices.custom.buy / self.prices.custom.sell } }; apply_factor( @@ -196,12 +201,10 @@ impl Trade { // the surplus let executed_in_surplus_token = match self.side { Side::Sell => { - self.executed.amount.0 * self.prices.custom.sell - / self.prices.custom.buy + self.executed.0 * self.prices.custom.sell / self.prices.custom.buy } Side::Buy => { - self.executed.amount.0 * self.prices.custom.buy - / self.prices.custom.sell + self.executed.0 * self.prices.custom.buy / self.prices.custom.sell } }; apply_factor( @@ -220,19 +223,25 @@ impl Trade { /// Protocol fee is defined by fee policies attached to the order. /// /// Denominated in NATIVE token - fn native_protocol_fee(&self) -> Option { + fn native_protocol_fee( + &self, + prices: &HashMap, + ) -> Option { big_rational_to_u256( - &(self.protocol_fee()?.amount.0.to_big_rational() * self.surplus_token_price().0), + &(self.protocol_fee()?.amount.0.to_big_rational() * self.surplus_token_price(prices).0), ) .map(Into::into) .ok() } /// Returns the normalized price of the trade surplus token - fn surplus_token_price(&self) -> auction::NormalizedPrice { + fn surplus_token_price( + &self, + prices: &HashMap, + ) -> auction::NormalizedPrice { match self.side { - Side::Buy => self.prices.native.sell.clone(), - Side::Sell => self.prices.native.buy.clone(), + Side::Buy => prices[&self.sell.token].clone(), + Side::Sell => prices[&self.buy.token].clone(), } } } @@ -249,8 +258,6 @@ pub struct Prices { pub uniform: ClearingPrices, /// Adjusted uniform prices to account for fees (gas cost and protocol fees) pub custom: ClearingPrices, - /// Prices normalized to the same native token (ETH) - pub native: NormalizedPrices, } /// Uniform clearing prices at which the trade was executed. @@ -259,10 +266,3 @@ pub struct ClearingPrices { pub sell: eth::U256, pub buy: eth::U256, } - -/// Normalized prices to the same native token (ETH) -#[derive(Debug, Clone)] -pub struct NormalizedPrices { - pub sell: auction::NormalizedPrice, - pub buy: auction::NormalizedPrice, -} diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 30c77f28a5..717b8867a5 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -1,5 +1,6 @@ use { self::trade::ClearingPrices, + super::settled, crate::{ boundary, domain::{ @@ -119,6 +120,55 @@ impl Solution { &self.score } + /// Trade in a settled form and semantics. + pub fn settled(&self) -> Vec { + self.trades + .iter() + .filter_map(|trade| match trade { + Trade::Fulfillment(fulfillment) => { + let executed = match fulfillment.order().side { + order::Side::Sell => { + (fulfillment.executed().0 + fulfillment.fee().0).into() + } + order::Side::Buy => fulfillment.executed(), + }; + let uniform_prices = settled::ClearingPrices { + sell: self.prices[&fulfillment.order().sell.token], + buy: self.prices[&fulfillment.order().buy.token], + }; + let custom_prices = settled::ClearingPrices { + sell: match fulfillment.order().side { + order::Side::Sell => { + fulfillment.executed().0 * uniform_prices.sell / uniform_prices.buy + } + order::Side::Buy => fulfillment.executed().0, + }, + buy: match fulfillment.order().side { + order::Side::Sell => fulfillment.executed().0 + fulfillment.fee().0, + order::Side::Buy => { + (fulfillment.executed().0) * uniform_prices.buy + / uniform_prices.sell + + fulfillment.fee().0 + } + }, + }; + Some(settled::Trade::new( + fulfillment.order().sell, + fulfillment.order().buy, + fulfillment.order().side, + executed, + settled::Prices { + uniform: uniform_prices, + custom: custom_prices, + }, + fulfillment.order().protocol_fees.clone(), + )) + } + Trade::Jit(_) => None, + }) + .collect() + } + /// Approval interactions necessary for encoding the settlement. pub async fn approvals( &self, diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 5fff2d04eb..857a0334ff 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -251,17 +251,14 @@ impl Settlement { { // For testing purposes, always calculate CIP38 score, even if the score is not // used. - match self - .settled(ð, auction) - .and_then(|settled| settled.score()) - { - Some(score) => { - tracing::info!(?score, "CIP38 score"); - } - None => { - tracing::warn!("could not calculate CIP38 score"); - } - } + let settlement = competition::settled::Settlement::new( + self.solutions + .values() + .flat_map(|solution| solution.settled()) + .collect(), + ); + let score = settlement.score(&auction.normalized_prices()); + println!("CIP38 score: {:?}", score); } if score > quality { @@ -357,24 +354,6 @@ impl Settlement { _ => None, } } - - /// Settlement built from the settlement transaction calldata - fn settled( - &self, - eth: &Ethereum, - auction: &competition::Auction, - ) -> Option { - let policies = &self - .solutions - .values() - .flat_map(|solution| { - solution - .user_trades() - .map(|trade| (trade.order().uid, trade.order().protocol_fees.clone())) - }) - .collect(); - self.boundary.settled(eth, auction, policies) - } } /// Should the interactions be internalized? From 616fb1996d7308b2c56cbbfa0662ed5596ab853c Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 09:56:24 +0100 Subject: [PATCH 11/31] bug fix --- crates/driver/src/domain/competition/solution/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 717b8867a5..a092dd07b9 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -133,8 +133,8 @@ impl Solution { order::Side::Buy => fulfillment.executed(), }; let uniform_prices = settled::ClearingPrices { - sell: self.prices[&fulfillment.order().sell.token], - buy: self.prices[&fulfillment.order().buy.token], + sell: self.prices[&fulfillment.order().sell.token.wrap(self.weth)], + buy: self.prices[&fulfillment.order().buy.token.wrap(self.weth)], }; let custom_prices = settled::ClearingPrices { sell: match fulfillment.order().side { From b79e2eb5b77f4fc4252fb886b91228b61e988745 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 10:10:42 +0100 Subject: [PATCH 12/31] add buy token address to auction --- crates/driver/src/domain/competition/auction.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index 31736b8dd6..7a832501a9 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -115,7 +115,8 @@ impl Auction { } pub fn normalized_prices(&self) -> HashMap { - self.tokens + let mut prices = self + .tokens .0 .iter() .filter_map(|(address, token)| { @@ -123,13 +124,18 @@ impl Auction { ( *address, auction::NormalizedPrice( - price.0 .0.to_big_rational() - / BigRational::from_integer(1_000_000_000_000_000_000_u128.into()), // TODO polish + price.0 .0.to_big_rational() / BigRational::from_integer(1_000_000_000_000_000_000_u128.into()), // TODO polish ), ) }) }) - .collect() + .collect::>(); + // Add the buy eth address + prices.insert( + eth::ETH_TOKEN, + auction::NormalizedPrice(BigRational::from_integer(1.into())), + ); + prices } } From 21cef1471061072dd7b78d47d178b9671f96a978 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 10:42:50 +0100 Subject: [PATCH 13/31] remove obsolete --- crates/shared/src/encoded_settlement.rs | 60 ------------------------- 1 file changed, 60 deletions(-) diff --git a/crates/shared/src/encoded_settlement.rs b/crates/shared/src/encoded_settlement.rs index 8ead05ebf0..a0646e843e 100644 --- a/crates/shared/src/encoded_settlement.rs +++ b/crates/shared/src/encoded_settlement.rs @@ -1,6 +1,5 @@ use { crate::interaction::EncodedInteraction, - anyhow::Result, ethcontract::Bytes, model::{ order::{BuyTokenDestination, OrderData, OrderKind, SellTokenSource}, @@ -85,65 +84,6 @@ pub struct EncodedSettlement { pub interactions: [Vec; 3], } -impl EncodedSettlement { - /// Order uids for all trades in the settlement. - /// - /// Returns order uids for all trades in the settlement in the same order as - /// the trades. - pub fn uids( - &self, - domain_separator: model::DomainSeparator, - ) -> Result> { - self.trades - .iter() - .map(|trade| { - let order = model::order::OrderData { - sell_token: self.tokens[trade.0.as_u64() as usize], - buy_token: self.tokens[trade.1.as_u64() as usize], - sell_amount: trade.3, - buy_amount: trade.4, - valid_to: trade.5, - app_data: model::app_data::AppDataHash(trade.6 .0), - fee_amount: trade.7, - kind: if trade.8.byte(0) & 0b1 == 0 { - model::order::OrderKind::Sell - } else { - model::order::OrderKind::Buy - }, - partially_fillable: trade.8.byte(0) & 0b10 != 0, - receiver: Some(trade.2), - sell_token_balance: if trade.8.byte(0) & 0x08 == 0 { - model::order::SellTokenSource::Erc20 - } else if trade.8.byte(0) & 0x04 == 0 { - model::order::SellTokenSource::External - } else { - model::order::SellTokenSource::Internal - }, - buy_token_balance: if trade.8.byte(0) & 0x10 == 0 { - model::order::BuyTokenDestination::Erc20 - } else { - model::order::BuyTokenDestination::Internal - }, - }; - let signing_scheme = match trade.8.byte(0) >> 5 { - 0b00 => model::signature::SigningScheme::Eip712, - 0b01 => model::signature::SigningScheme::EthSign, - 0b10 => model::signature::SigningScheme::Eip1271, - 0b11 => model::signature::SigningScheme::PreSign, - _ => unreachable!(), - }; - let signature = Signature::from_bytes(signing_scheme, &trade.10 .0)?; - let owner = signature.recover_owner( - &trade.10 .0, - &domain_separator, - &order.hash_struct(), - )?; - Ok(order.uid(&domain_separator, &owner)) - }) - .collect() - } -} - #[cfg(test)] mod tests { use {super::*, ethcontract::H256, hex_literal::hex, model::signature::EcdsaSignature}; From 64acb181ba9fdb03af4509996d5e9d3a70954022 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 13:19:11 +0100 Subject: [PATCH 14/31] error handling --- .../src/domain/competition/settled/mod.rs | 222 +++++++++--------- .../src/domain/competition/solution/mod.rs | 105 +++++---- .../domain/competition/solution/settlement.rs | 21 +- .../driver/src/infra/solver/dto/solution.rs | 3 + 4 files changed, 187 insertions(+), 164 deletions(-) diff --git a/crates/driver/src/domain/competition/settled/mod.rs b/crates/driver/src/domain/competition/settled/mod.rs index 3714cae637..893351dadb 100644 --- a/crates/driver/src/domain/competition/settled/mod.rs +++ b/crates/driver/src/domain/competition/settled/mod.rs @@ -8,7 +8,8 @@ use { std::collections::HashMap, }; -/// Settlement built from onchain calldata. +/// Settlement in a settleable form and semantics, aligned with what the +/// settlement contract expects. #[derive(Debug, Clone)] pub struct Settlement { trades: Vec, @@ -21,14 +22,19 @@ impl Settlement { /// Score of a settlement as per CIP38 /// + /// Score of a settlement is the sum of scores of all user trades in the + /// settlement. + /// + /// Settlement score is valid only if all trade scores are valid. + /// /// Denominated in NATIVE token pub fn score( &self, prices: &HashMap, - ) -> Option { + ) -> Result { self.trades .iter() - .map(|t| t.score(prices)) + .map(|trade| trade.score(prices)) .try_fold(eth::TokenAmount(eth::U256::zero()), |acc, score| { score.map(|score| acc + score) }) @@ -41,7 +47,7 @@ pub struct Trade { buy: eth::Asset, side: Side, executed: order::TargetAmount, - prices: Prices, + custom_price: CustomClearingPrices, policies: Vec, } @@ -51,7 +57,7 @@ impl Trade { buy: eth::Asset, side: Side, executed: order::TargetAmount, - prices: Prices, + custom_price: CustomClearingPrices, policies: Vec, ) -> Self { Self { @@ -59,7 +65,7 @@ impl Trade { buy, side, executed, - prices, + custom_price, policies, } } @@ -70,10 +76,8 @@ impl Trade { pub fn score( &self, prices: &HashMap, - ) -> Option { - self.native_surplus(prices) - .zip(self.native_protocol_fee(prices)) - .map(|(surplus, fee)| surplus + fee) + ) -> Result { + Ok(self.native_surplus(prices)? + self.native_protocol_fee(prices)?) } /// Surplus based on custom clearing prices returns the surplus after all @@ -94,8 +98,8 @@ impl Trade { limit_sell.checked_sub( self.executed .0 - .checked_mul(self.prices.custom.buy)? - .checked_div(self.prices.custom.sell)?, + .checked_mul(self.custom_price.buy)? + .checked_div(self.custom_price.sell)?, ) } Side::Sell => { @@ -108,20 +112,14 @@ impl Trade { // difference between executed amount converted to buy token and limit buy self.executed .0 - .checked_mul(self.prices.custom.sell)? - .checked_div(self.prices.custom.buy)? + .checked_mul(self.custom_price.sell)? + .checked_div(self.custom_price.buy)? .checked_sub(limit_buy) } } - .map(|surplus| match self.side { - Side::Buy => eth::Asset { - amount: surplus.into(), - token: self.sell.token, - }, - Side::Sell => eth::Asset { - amount: surplus.into(), - token: self.buy.token, - }, + .map(|surplus| eth::Asset { + token: self.surplus_token(), + amount: surplus.into(), }) } @@ -132,92 +130,75 @@ impl Trade { fn native_surplus( &self, prices: &HashMap, - ) -> Option { - big_rational_to_u256( - &(self.surplus()?.amount.0.to_big_rational() * self.surplus_token_price(prices).0), - ) - .map(Into::into) - .ok() + ) -> Result { + let surplus = self + .surplus() + .ok_or(Error::Surplus(self.sell, self.buy))? + .amount; + let native_price = self.surplus_token_price(prices)?; + big_rational_to_u256(&(surplus.0.to_big_rational() * native_price.0)) + .map(Into::into) + .map_err(Into::into) } /// Protocol fee is defined by fee policies attached to the order. /// /// Denominated in SURPLUS token - fn protocol_fee(&self) -> Option { + fn protocol_fee(&self) -> Result { // TODO: support multiple fee policies if self.policies.len() > 1 { - return None; + return Err(Error::MultipleFeePolicies); } - match self.policies.first()? { - order::FeePolicy::Surplus { - factor, - max_volume_factor, - } => Some(eth::Asset { - token: match self.side { - Side::Sell => self.buy.token, - Side::Buy => self.sell.token, - }, - amount: std::cmp::min( + let protocol_fee = |policy: &order::FeePolicy| { + match policy { + order::FeePolicy::Surplus { + factor, + max_volume_factor, + } => Ok(std::cmp::min( { - // If the surplus after all fees is X, then the original - // surplus before protocol fee is X / (1 - factor) - apply_factor(self.surplus()?.amount.into(), factor / (1.0 - factor))? + // If the surplus after all fees is X, then the original surplus before + // protocol fee is X / (1 - factor) + let surplus = self + .surplus() + .ok_or(Error::Surplus(self.sell, self.buy))? + .amount; + apply_factor(surplus.into(), factor / (1.0 - factor)) + .ok_or(Error::Factor(surplus.0, *factor))? }, { - // Convert the executed amount to surplus token so it can be compared with - // the surplus + // Convert the executed amount to surplus token so it can be compared + // with the surplus let executed_in_surplus_token = match self.side { Side::Sell => { - self.executed.0 * self.prices.custom.sell / self.prices.custom.buy + self.executed.0 * self.custom_price.sell / self.custom_price.buy } Side::Buy => { - self.executed.0 * self.prices.custom.buy / self.prices.custom.sell + self.executed.0 * self.custom_price.buy / self.custom_price.sell } }; - apply_factor( - executed_in_surplus_token, - match self.side { - Side::Sell => max_volume_factor / (1.0 - max_volume_factor), - Side::Buy => max_volume_factor / (1.0 + max_volume_factor), - }, - )? + let factor = match self.side { + Side::Sell => max_volume_factor / (1.0 - max_volume_factor), + Side::Buy => max_volume_factor / (1.0 + max_volume_factor), + }; + apply_factor(executed_in_surplus_token, factor) + .ok_or(Error::Factor(executed_in_surplus_token, factor))? }, - ) - .into(), - }), - order::FeePolicy::PriceImprovement { - factor: _, - max_volume_factor: _, - quote: _, - } => todo!(), - order::FeePolicy::Volume { factor } => Some(eth::Asset { - token: match self.side { - Side::Sell => self.buy.token, - Side::Buy => self.sell.token, - }, - amount: { - // Convert the executed amount to surplus token so it can be compared with - // the surplus - let executed_in_surplus_token = match self.side { - Side::Sell => { - self.executed.0 * self.prices.custom.sell / self.prices.custom.buy - } - Side::Buy => { - self.executed.0 * self.prices.custom.buy / self.prices.custom.sell - } - }; - apply_factor( - executed_in_surplus_token, - match self.side { - Side::Sell => factor / (1.0 - factor), - Side::Buy => factor / (1.0 + factor), - }, - )? - } - .into(), - }), - } + )), + order::FeePolicy::PriceImprovement { + factor: _, + max_volume_factor: _, + quote: _, + } => Err(Error::UnsupportedFeePolicy), + order::FeePolicy::Volume { factor: _ } => Err(Error::UnsupportedFeePolicy), + } + }; + + let protocol_fee = self.policies.first().map(protocol_fee).transpose(); + Ok(eth::Asset { + token: self.surplus_token(), + amount: protocol_fee?.unwrap_or(0.into()).into(), + }) } /// Protocol fee is defined by fee policies attached to the order. @@ -226,23 +207,30 @@ impl Trade { fn native_protocol_fee( &self, prices: &HashMap, - ) -> Option { - big_rational_to_u256( - &(self.protocol_fee()?.amount.0.to_big_rational() * self.surplus_token_price(prices).0), - ) - .map(Into::into) - .ok() + ) -> Result { + let protocol_fee = self.protocol_fee()?.amount; + let native_price = self.surplus_token_price(prices)?; + big_rational_to_u256(&(protocol_fee.0.to_big_rational() * native_price.0)) + .map(Into::into) + .map_err(Into::into) + } + + fn surplus_token(&self) -> eth::TokenAddress { + match self.side { + Side::Buy => self.sell.token, + Side::Sell => self.buy.token, + } } /// Returns the normalized price of the trade surplus token fn surplus_token_price( &self, prices: &HashMap, - ) -> auction::NormalizedPrice { - match self.side { - Side::Buy => prices[&self.sell.token].clone(), - Side::Sell => prices[&self.buy.token].clone(), - } + ) -> Result { + prices + .get(&self.surplus_token()) + .cloned() + .ok_or(Error::MissingPrice(self.surplus_token())) } } @@ -253,16 +241,30 @@ fn apply_factor(amount: eth::U256, factor: f64) -> Option { ) } +/// Custom clearing prices at which the trade was executed. +/// +/// These prices differ from uniform clearing prices, in that they are adjusted +/// to account for all fees (gas cost and protocol fees). +/// +/// These prices determine the actual traded amounts from the user perspective. #[derive(Debug, Clone)] -pub struct Prices { - pub uniform: ClearingPrices, - /// Adjusted uniform prices to account for fees (gas cost and protocol fees) - pub custom: ClearingPrices, -} - -/// Uniform clearing prices at which the trade was executed. -#[derive(Debug, Clone)] -pub struct ClearingPrices { +pub struct CustomClearingPrices { pub sell: eth::U256, pub buy: eth::U256, } + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("multiple fee policies are not supported yet")] + MultipleFeePolicies, + #[error("failed to calculate surplus for trade sell {0:?} buy {1:?}")] + Surplus(eth::Asset, eth::Asset), + #[error("missing native price for token {0:?}")] + MissingPrice(eth::TokenAddress), + #[error("type conversion error")] + TypeConversion(#[from] anyhow::Error), + #[error("fee policy not supported")] + UnsupportedFeePolicy, + #[error("factor {1} multiplication with {0} failed")] + Factor(eth::U256, f64), +} diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index a092dd07b9..14c4c5e078 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -120,53 +120,62 @@ impl Solution { &self.score } - /// Trade in a settled form and semantics. - pub fn settled(&self) -> Vec { - self.trades - .iter() - .filter_map(|trade| match trade { - Trade::Fulfillment(fulfillment) => { - let executed = match fulfillment.order().side { - order::Side::Sell => { - (fulfillment.executed().0 + fulfillment.fee().0).into() - } - order::Side::Buy => fulfillment.executed(), - }; - let uniform_prices = settled::ClearingPrices { - sell: self.prices[&fulfillment.order().sell.token.wrap(self.weth)], - buy: self.prices[&fulfillment.order().buy.token.wrap(self.weth)], - }; - let custom_prices = settled::ClearingPrices { - sell: match fulfillment.order().side { - order::Side::Sell => { - fulfillment.executed().0 * uniform_prices.sell / uniform_prices.buy - } - order::Side::Buy => fulfillment.executed().0, - }, - buy: match fulfillment.order().side { - order::Side::Sell => fulfillment.executed().0 + fulfillment.fee().0, - order::Side::Buy => { - (fulfillment.executed().0) * uniform_prices.buy - / uniform_prices.sell - + fulfillment.fee().0 - } - }, - }; - Some(settled::Trade::new( - fulfillment.order().sell, - fulfillment.order().buy, - fulfillment.order().side, - executed, - settled::Prices { - uniform: uniform_prices, - custom: custom_prices, - }, - fulfillment.order().protocol_fees.clone(), - )) - } - Trade::Jit(_) => None, - }) - .collect() + /// Converts the solution into an onchain settleable form and semantic, as + /// expected by the settlement contract. + /// + /// Skips JIT orders. + pub fn scorable_trades(&self) -> Result, SolutionError> { + let mut trades = Vec::with_capacity(self.trades.len()); + for trade in self.user_trades() { + // Solver generated fulfillment does not include the fee in the executed amount + // for sell orders. + let executed = match trade.order().side { + order::Side::Sell => (trade.executed().0 + trade.fee().0).into(), + order::Side::Buy => trade.executed(), + }; + let uniform_prices = settled::CustomClearingPrices { + sell: *self + .prices + .get(&trade.order().sell.token.wrap(self.weth)) + .ok_or(SolutionError::InvalidClearingPrices)?, + buy: *self + .prices + .get(&trade.order().buy.token.wrap(self.weth)) + .ok_or(SolutionError::InvalidClearingPrices)?, + }; + let custom_prices = settled::CustomClearingPrices { + sell: match trade.order().side { + order::Side::Sell => trade + .executed() + .0 + .checked_mul(uniform_prices.sell) + .ok_or(trade::Error::Overflow)? + .checked_div(uniform_prices.buy) + .ok_or(trade::Error::DivisionByZero)?, + order::Side::Buy => trade.executed().0, + }, + buy: match trade.order().side { + order::Side::Sell => trade.executed().0 + trade.fee().0, + order::Side::Buy => { + (trade.executed().0) + .checked_mul(uniform_prices.buy) + .ok_or(trade::Error::Overflow)? + .checked_div(uniform_prices.sell) + .ok_or(trade::Error::DivisionByZero)? + + trade.fee().0 + } + }, + }; + trades.push(settled::Trade::new( + trade.order().sell, + trade.order().buy, + trade.order().side, + executed, + custom_prices, + trade.order().protocol_fees.clone(), + )) + } + Ok(trades) } /// Approval interactions necessary for encoding the settlement. @@ -371,4 +380,6 @@ pub enum SolutionError { InvalidClearingPrices, #[error(transparent)] ProtocolFee(#[from] fee::Error), + #[error(transparent)] + Fulfillment(#[from] trade::Error), } diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 857a0334ff..d5f586c1a3 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -251,14 +251,21 @@ impl Settlement { { // For testing purposes, always calculate CIP38 score, even if the score is not // used. - let settlement = competition::settled::Settlement::new( - self.solutions - .values() - .flat_map(|solution| solution.settled()) - .collect(), - ); + let trades = self + .solutions + .iter() + .filter_map(|(id, solution)| match solution.scorable_trades() { + Ok(trades) => Some(trades), + Err(err) => { + tracing::debug!(?id, ?err, "could not prepare scorable settlement"); + None + } + }) + .flatten() + .collect(); + let settlement = competition::settled::Settlement::new(trades); let score = settlement.score(&auction.normalized_prices()); - println!("CIP38 score: {:?}", score); + tracing::info!(?score, "CIP38 score for settlement: {:?}", self.solutions()); } if score > quality { diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index 2ed5bc3772..e1ac83749b 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -205,6 +205,9 @@ impl Solutions { competition::solution::SolutionError::ProtocolFee(err) => { super::Error(format!("could not incorporate protocol fee: {err}")) } + competition::solution::SolutionError::Fulfillment(err) => { + super::Error(format!("fulfillment: {err}")) + } }) }) .collect() From 370a714cf06d96623c37c6693c6e1aa828092707 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 13:52:16 +0100 Subject: [PATCH 15/31] polish --- .../driver/src/domain/competition/auction.rs | 20 ++++++++++++---- .../src/domain/competition/settled/mod.rs | 23 +++++++------------ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index 7a832501a9..9ab8036ca9 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -114,7 +114,8 @@ impl Auction { self.score_cap } - pub fn normalized_prices(&self) -> HashMap { + /// All auction prices normalized to native token price. + pub fn normalized_prices(&self) -> NormalizedPrices { let mut prices = self .tokens .0 @@ -123,13 +124,12 @@ impl Auction { token.price.map(|price| { ( *address, - auction::NormalizedPrice( - price.0 .0.to_big_rational() / BigRational::from_integer(1_000_000_000_000_000_000_u128.into()), // TODO polish - ), + auction::NormalizedPrice(price.0 .0.to_big_rational() / &*UNIT), ) }) }) .collect::>(); + // Add the buy eth address prices.insert( eth::ETH_TOKEN, @@ -443,7 +443,14 @@ impl From for Price { } } -/// The price of a token normalized to native token. +lazy_static::lazy_static! { + static ref UNIT: num::BigInt = num::BigInt::from(1_000_000_000_000_000_000_u128); +} + +/// The price of a token normalized to native token price. +/// +/// For example, auction price of 1ETH is 1e18, while normalized price of 1ETH +/// is 1. #[derive(Debug, Clone)] pub struct NormalizedPrice(pub BigRational); @@ -453,6 +460,9 @@ impl From for NormalizedPrice { } } +/// /// All auction prices normalized to native token price. +pub type NormalizedPrices = HashMap; + #[derive(Debug, Clone, Copy)] pub struct Id(pub i64); diff --git a/crates/driver/src/domain/competition/settled/mod.rs b/crates/driver/src/domain/competition/settled/mod.rs index 893351dadb..1ab03f2d15 100644 --- a/crates/driver/src/domain/competition/settled/mod.rs +++ b/crates/driver/src/domain/competition/settled/mod.rs @@ -5,11 +5,10 @@ use { }, crate::{domain::eth, util::conv::u256::U256Ext}, number::conversions::big_rational_to_u256, - std::collections::HashMap, }; -/// Settlement in a settleable form and semantics, aligned with what the -/// settlement contract expects. +/// Settlement in an onchain settleable form and semantics, aligned with what +/// the settlement contract expects. #[derive(Debug, Clone)] pub struct Settlement { trades: Vec, @@ -22,16 +21,13 @@ impl Settlement { /// Score of a settlement as per CIP38 /// - /// Score of a settlement is the sum of scores of all user trades in the + /// Score of a settlement is a sum of scores of all user trades in the /// settlement. /// /// Settlement score is valid only if all trade scores are valid. /// /// Denominated in NATIVE token - pub fn score( - &self, - prices: &HashMap, - ) -> Result { + pub fn score(&self, prices: &auction::NormalizedPrices) -> Result { self.trades .iter() .map(|trade| trade.score(prices)) @@ -73,10 +69,7 @@ impl Trade { /// CIP38 score defined as surplus + protocol fee /// /// Denominated in NATIVE token - pub fn score( - &self, - prices: &HashMap, - ) -> Result { + pub fn score(&self, prices: &auction::NormalizedPrices) -> Result { Ok(self.native_surplus(prices)? + self.native_protocol_fee(prices)?) } @@ -129,7 +122,7 @@ impl Trade { /// Denominated in NATIVE token fn native_surplus( &self, - prices: &HashMap, + prices: &auction::NormalizedPrices, ) -> Result { let surplus = self .surplus() @@ -206,7 +199,7 @@ impl Trade { /// Denominated in NATIVE token fn native_protocol_fee( &self, - prices: &HashMap, + prices: &auction::NormalizedPrices, ) -> Result { let protocol_fee = self.protocol_fee()?.amount; let native_price = self.surplus_token_price(prices)?; @@ -225,7 +218,7 @@ impl Trade { /// Returns the normalized price of the trade surplus token fn surplus_token_price( &self, - prices: &HashMap, + prices: &auction::NormalizedPrices, ) -> Result { prices .get(&self.surplus_token()) From 5098dfbde381c2b98236907285659c0b9086fc22 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 14:02:34 +0100 Subject: [PATCH 16/31] more error handling --- .../driver/src/domain/competition/auction.rs | 2 +- .../src/domain/competition/settled/mod.rs | 24 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index 9ab8036ca9..f7422cb202 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -129,7 +129,7 @@ impl Auction { }) }) .collect::>(); - + // Add the buy eth address prices.insert( eth::ETH_TOKEN, diff --git a/crates/driver/src/domain/competition/settled/mod.rs b/crates/driver/src/domain/competition/settled/mod.rs index 1ab03f2d15..b1fbd599f6 100644 --- a/crates/driver/src/domain/competition/settled/mod.rs +++ b/crates/driver/src/domain/competition/settled/mod.rs @@ -163,12 +163,20 @@ impl Trade { // Convert the executed amount to surplus token so it can be compared // with the surplus let executed_in_surplus_token = match self.side { - Side::Sell => { - self.executed.0 * self.custom_price.sell / self.custom_price.buy - } - Side::Buy => { - self.executed.0 * self.custom_price.buy / self.custom_price.sell - } + Side::Sell => self + .executed + .0 + .checked_mul(self.custom_price.sell) + .ok_or(Error::Overflow)? + .checked_div(self.custom_price.buy) + .ok_or(Error::DivisionByZero)?, + Side::Buy => self + .executed + .0 + .checked_mul(self.custom_price.buy) + .ok_or(Error::Overflow)? + .checked_div(self.custom_price.sell) + .ok_or(Error::DivisionByZero)?, }; let factor = match self.side { Side::Sell => max_volume_factor / (1.0 - max_volume_factor), @@ -260,4 +268,8 @@ pub enum Error { UnsupportedFeePolicy, #[error("factor {1} multiplication with {0} failed")] Factor(eth::U256, f64), + #[error("overflow error while calculating protocol fee")] + Overflow, + #[error("division by zero error while calculating protocol fee")] + DivisionByZero, } From a808add74df4e3ee60641f523feba492b8700373 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 14:11:29 +0100 Subject: [PATCH 17/31] fix comments --- crates/driver/src/domain/competition/auction.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index f7422cb202..f4807836be 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -115,6 +115,8 @@ impl Auction { } /// All auction prices normalized to native token price. + /// + /// WEI to ETHER conversion pub fn normalized_prices(&self) -> NormalizedPrices { let mut prices = self .tokens @@ -448,9 +450,6 @@ lazy_static::lazy_static! { } /// The price of a token normalized to native token price. -/// -/// For example, auction price of 1ETH is 1e18, while normalized price of 1ETH -/// is 1. #[derive(Debug, Clone)] pub struct NormalizedPrice(pub BigRational); @@ -460,7 +459,10 @@ impl From for NormalizedPrice { } } -/// /// All auction prices normalized to native token price. +/// All auction prices normalized to native token price. +/// +/// For example, auction price of 1ETH is 1e18, while normalized price of 1ETH +/// is 1. pub type NormalizedPrices = HashMap; #[derive(Debug, Clone, Copy)] From 677e2ff8dc65ea5c15dc1d4ee1408b2f529c3dee Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 17:13:53 +0100 Subject: [PATCH 18/31] Refactor --- crates/driver/src/boundary/settlement.rs | 2 + crates/driver/src/domain/competition/mod.rs | 1 - crates/driver/src/domain/competition/score.rs | 2 + .../src/domain/competition/solution/mod.rs | 44 ++++++++----- .../{settled/mod.rs => solution/scoring.rs} | 30 +++++---- .../domain/competition/solution/settlement.rs | 63 ++++++++++--------- crates/driver/src/domain/eth/mod.rs | 6 ++ crates/driver/src/infra/notify/mod.rs | 1 + .../driver/src/infra/solver/dto/solution.rs | 4 +- crates/shared/src/http_solver/model.rs | 1 + crates/solvers/src/boundary/legacy.rs | 1 + 11 files changed, 92 insertions(+), 63 deletions(-) rename crates/driver/src/domain/competition/{settled/mod.rs => solution/scoring.rs} (93%) diff --git a/crates/driver/src/boundary/settlement.rs b/crates/driver/src/boundary/settlement.rs index ed4b585689..c8c0c13609 100644 --- a/crates/driver/src/boundary/settlement.rs +++ b/crates/driver/src/boundary/settlement.rs @@ -167,6 +167,7 @@ impl Settlement { gas_amount: None, } } + competition::SolverScore::Surplus => http_solver::model::Score::Surplus, }; Ok(Self { @@ -211,6 +212,7 @@ impl Settlement { success_probability, .. } => competition::SolverScore::RiskAdjusted(success_probability), + http_solver::model::Score::Surplus => competition::SolverScore::Surplus, } } diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index bc5c753e87..16286aba18 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -25,7 +25,6 @@ use { pub mod auction; pub mod order; pub mod score; -pub mod settled; pub mod solution; pub use { diff --git a/crates/driver/src/domain/competition/score.rs b/crates/driver/src/domain/competition/score.rs index 72a94d1f8f..42db6b4aaa 100644 --- a/crates/driver/src/domain/competition/score.rs +++ b/crates/driver/src/domain/competition/score.rs @@ -67,6 +67,8 @@ pub enum Error { #[error(transparent)] RiskAdjusted(#[from] risk::Error), #[error(transparent)] + Scoring(#[from] super::solution::Scoring), + #[error(transparent)] Boundary(#[from] boundary::Error), } diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 14c4c5e078..ad4ab4b258 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -1,6 +1,5 @@ use { self::trade::ClearingPrices, - super::settled, crate::{ boundary, domain::{ @@ -22,6 +21,7 @@ use { pub mod fee; pub mod interaction; +pub mod scoring; pub mod settlement; pub mod trade; @@ -120,11 +120,8 @@ impl Solution { &self.score } - /// Converts the solution into an onchain settleable form and semantic, as - /// expected by the settlement contract. - /// - /// Skips JIT orders. - pub fn scorable_trades(&self) -> Result, SolutionError> { + /// Converts the solution into scoring form + pub fn scoring(&self) -> Result { let mut trades = Vec::with_capacity(self.trades.len()); for trade in self.user_trades() { // Solver generated fulfillment does not include the fee in the executed amount @@ -133,7 +130,7 @@ impl Solution { order::Side::Sell => (trade.executed().0 + trade.fee().0).into(), order::Side::Buy => trade.executed(), }; - let uniform_prices = settled::CustomClearingPrices { + let uniform_prices = ClearingPrices { sell: *self .prices .get(&trade.order().sell.token.wrap(self.weth)) @@ -143,15 +140,15 @@ impl Solution { .get(&trade.order().buy.token.wrap(self.weth)) .ok_or(SolutionError::InvalidClearingPrices)?, }; - let custom_prices = settled::CustomClearingPrices { + let custom_prices = scoring::CustomClearingPrices { sell: match trade.order().side { order::Side::Sell => trade .executed() .0 .checked_mul(uniform_prices.sell) - .ok_or(trade::Error::Overflow)? + .ok_or(Math::Overflow)? .checked_div(uniform_prices.buy) - .ok_or(trade::Error::DivisionByZero)?, + .ok_or(Math::DivisionByZero)?, order::Side::Buy => trade.executed().0, }, buy: match trade.order().side { @@ -159,14 +156,14 @@ impl Solution { order::Side::Buy => { (trade.executed().0) .checked_mul(uniform_prices.buy) - .ok_or(trade::Error::Overflow)? + .ok_or(Math::Overflow)? .checked_div(uniform_prices.sell) - .ok_or(trade::Error::DivisionByZero)? + .ok_or(Math::DivisionByZero)? + trade.fee().0 } }, }; - trades.push(settled::Trade::new( + trades.push(scoring::Trade::new( trade.order().sell, trade.order().buy, trade.order().side, @@ -175,7 +172,7 @@ impl Solution { trade.order().protocol_fees.clone(), )) } - Ok(trades) + Ok(scoring::Scoring::new(trades)) } /// Approval interactions necessary for encoding the settlement. @@ -334,6 +331,7 @@ impl std::fmt::Debug for Solution { pub enum SolverScore { Solver(eth::U256), RiskAdjusted(f64), + Surplus, } /// A unique solution ID. This ID is generated by the solver and only needs to /// be unique within a single round of competition. This ID is only important in @@ -381,5 +379,21 @@ pub enum SolutionError { #[error(transparent)] ProtocolFee(#[from] fee::Error), #[error(transparent)] - Fulfillment(#[from] trade::Error), + Math(#[from] Math), +} + +#[derive(Debug, thiserror::Error)] +pub enum Scoring { + #[error(transparent)] + Solution(#[from] SolutionError), + #[error(transparent)] + Score(#[from] scoring::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum Math { + #[error("overflow")] + Overflow, + #[error("division by zero")] + DivisionByZero, } diff --git a/crates/driver/src/domain/competition/settled/mod.rs b/crates/driver/src/domain/competition/solution/scoring.rs similarity index 93% rename from crates/driver/src/domain/competition/settled/mod.rs rename to crates/driver/src/domain/competition/solution/scoring.rs index b1fbd599f6..258094aeaa 100644 --- a/crates/driver/src/domain/competition/settled/mod.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -1,20 +1,20 @@ use { - super::{ - auction, - order::{self, Side}, + super::order::{self, Side}, + crate::{ + domain::{competition::auction, eth}, + util::conv::u256::U256Ext, }, - crate::{domain::eth, util::conv::u256::U256Ext}, number::conversions::big_rational_to_u256, }; -/// Settlement in an onchain settleable form and semantics, aligned with what -/// the settlement contract expects. +/// Scoring contains trades in an onchain settleable form and semantics, aligned +/// with what the settlement contract expects. #[derive(Debug, Clone)] -pub struct Settlement { +pub struct Scoring { trades: Vec, } -impl Settlement { +impl Scoring { pub fn new(trades: Vec) -> Self { Self { trades } } @@ -167,16 +167,16 @@ impl Trade { .executed .0 .checked_mul(self.custom_price.sell) - .ok_or(Error::Overflow)? + .ok_or(super::Math::Overflow)? .checked_div(self.custom_price.buy) - .ok_or(Error::DivisionByZero)?, + .ok_or(super::Math::DivisionByZero)?, Side::Buy => self .executed .0 .checked_mul(self.custom_price.buy) - .ok_or(Error::Overflow)? + .ok_or(super::Math::Overflow)? .checked_div(self.custom_price.sell) - .ok_or(Error::DivisionByZero)?, + .ok_or(super::Math::DivisionByZero)?, }; let factor = match self.side { Side::Sell => max_volume_factor / (1.0 - max_volume_factor), @@ -268,8 +268,6 @@ pub enum Error { UnsupportedFeePolicy, #[error("factor {1} multiplication with {0} failed")] Factor(eth::U256, f64), - #[error("overflow error while calculating protocol fee")] - Overflow, - #[error("division by zero error while calculating protocol fee")] - DivisionByZero, + #[error(transparent)] + Math(#[from] super::Math), } diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index d5f586c1a3..afaf366c79 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -215,6 +215,19 @@ impl Settlement { .into() } + fn scoring( + &self, + auction: &competition::Auction, + ) -> Result { + let prices = auction.normalized_prices(); + + let mut score = eth::TokenAmount(eth::U256::zero()); + for solution in self.solutions.values() { + score += solution.scoring()?.score(&prices)?; + } + Ok(score) + } + // TODO(#1494): score() should be defined on Solution rather than Settlement. /// Calculate the score for this settlement. pub fn score( @@ -223,12 +236,23 @@ impl Settlement { auction: &competition::Auction, revert_protection: &mempools::RevertProtection, ) -> Result { - let eth = eth.with_metric_label("scoringSolution".into()); - let quality = self.boundary.quality(ð, auction)?; + // For testing purposes, calculate CIP38 even before activation + let score = self.scoring(auction); + tracing::info!(?score, "CIP38 score for settlement: {:?}", self.solutions()); let score = match self.boundary.score() { - competition::SolverScore::Solver(score) => score.try_into()?, + competition::SolverScore::Solver(score) => { + let eth = eth.with_metric_label("scoringSolution".into()); + let quality = self.boundary.quality(ð, auction)?; + let score = score.try_into()?; + if score > quality { + return Err(score::Error::ScoreHigherThanQuality(score, quality)); + } + score + } competition::SolverScore::RiskAdjusted(success_probability) => { + let eth = eth.with_metric_label("scoringSolution".into()); + let quality = self.boundary.quality(ð, auction)?; let gas_cost = self.gas.estimate * auction.gas_price().effective(); let success_probability = success_probability.try_into()?; let objective_value = (quality - gas_cost)?; @@ -239,39 +263,20 @@ impl Settlement { mempools::RevertProtection::Enabled => GasCost::zero(), mempools::RevertProtection::Disabled => gas_cost, }; - competition::Score::new( + let score = competition::Score::new( auction.score_cap(), objective_value, success_probability, failure_cost, - )? + )?; + if score > quality { + return Err(score::Error::ScoreHigherThanQuality(score, quality)); + } + score } + competition::SolverScore::Surplus => score?.0.try_into()?, }; - { - // For testing purposes, always calculate CIP38 score, even if the score is not - // used. - let trades = self - .solutions - .iter() - .filter_map(|(id, solution)| match solution.scorable_trades() { - Ok(trades) => Some(trades), - Err(err) => { - tracing::debug!(?id, ?err, "could not prepare scorable settlement"); - None - } - }) - .flatten() - .collect(); - let settlement = competition::settled::Settlement::new(trades); - let score = settlement.score(&auction.normalized_prices()); - tracing::info!(?score, "CIP38 score for settlement: {:?}", self.solutions()); - } - - if score > quality { - return Err(score::Error::ScoreHigherThanQuality(score, quality)); - } - Ok(score) } diff --git a/crates/driver/src/domain/eth/mod.rs b/crates/driver/src/domain/eth/mod.rs index 2ccc8681a4..66492362bb 100644 --- a/crates/driver/src/domain/eth/mod.rs +++ b/crates/driver/src/domain/eth/mod.rs @@ -203,6 +203,12 @@ impl std::ops::Add for TokenAmount { } } +impl std::ops::AddAssign for TokenAmount { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } +} + impl std::fmt::Display for TokenAmount { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) diff --git a/crates/driver/src/infra/notify/mod.rs b/crates/driver/src/infra/notify/mod.rs index e31c60a7b2..2c03ac4ec1 100644 --- a/crates/driver/src/infra/notify/mod.rs +++ b/crates/driver/src/infra/notify/mod.rs @@ -53,6 +53,7 @@ pub fn scoring_failed( ), score::Error::RiskAdjusted(score::risk::Error::Boundary(_)) => return, score::Error::Boundary(_) => return, + score::Error::Scoring(_) => return, // TODO: should we notify? }; solver.notify(auction_id, solution_id, notification); diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index e1ac83749b..c68b03ab96 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -205,8 +205,8 @@ impl Solutions { competition::solution::SolutionError::ProtocolFee(err) => { super::Error(format!("could not incorporate protocol fee: {err}")) } - competition::solution::SolutionError::Fulfillment(err) => { - super::Error(format!("fulfillment: {err}")) + competition::solution::SolutionError::Math(err) => { + super::Error(format!("math error: {err}")) } }) }) diff --git a/crates/shared/src/http_solver/model.rs b/crates/shared/src/http_solver/model.rs index 843eae0c43..7112c226fa 100644 --- a/crates/shared/src/http_solver/model.rs +++ b/crates/shared/src/http_solver/model.rs @@ -211,6 +211,7 @@ pub enum Score { #[serde_as(as = "Option")] gas_amount: Option, }, + Surplus, } impl Default for Score { diff --git a/crates/solvers/src/boundary/legacy.rs b/crates/solvers/src/boundary/legacy.rs index 8690437214..8878a1ce93 100644 --- a/crates/solvers/src/boundary/legacy.rs +++ b/crates/solvers/src/boundary/legacy.rs @@ -575,6 +575,7 @@ fn to_domain_solution( success_probability, .. } => solution::Score::RiskAdjusted(solution::SuccessProbability(success_probability)), + Score::Surplus => solution::Score::Solver(0.into()), }, }) } From b176f2359289db735f03d3f25c5b11529ee6ab51 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 17:49:33 +0100 Subject: [PATCH 19/31] prices refactor --- .../driver/src/domain/competition/auction.rs | 54 ++++-------------- .../domain/competition/solution/scoring.rs | 56 ++++++++----------- .../domain/competition/solution/settlement.rs | 3 +- 3 files changed, 36 insertions(+), 77 deletions(-) diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index f4807836be..f2e0e56c59 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -8,11 +8,10 @@ use { time, }, infra::{self, blockchain, observe, Ethereum}, - util::{self, conv::u256::U256Ext}, + util::{self}, }, futures::future::{join_all, BoxFuture, FutureExt, Shared}, itertools::Itertools, - num::BigRational, std::{ collections::{HashMap, HashSet}, sync::{Arc, Mutex}, @@ -114,30 +113,16 @@ impl Auction { self.score_cap } - /// All auction prices normalized to native token price. - /// - /// WEI to ETHER conversion - pub fn normalized_prices(&self) -> NormalizedPrices { - let mut prices = self - .tokens + pub fn prices(&self) -> Prices { + self.tokens .0 .iter() - .filter_map(|(address, token)| { - token.price.map(|price| { - ( - *address, - auction::NormalizedPrice(price.0 .0.to_big_rational() / &*UNIT), - ) - }) - }) - .collect::>(); - - // Add the buy eth address - prices.insert( - eth::ETH_TOKEN, - auction::NormalizedPrice(BigRational::from_integer(1.into())), - ); - prices + .filter_map(|(address, token)| token.price.map(|price| (*address, price))) + .chain(std::iter::once(( + eth::ETH_TOKEN, + eth::U256::from(1_000_000_000_000_000_000u128).into(), + ))) + .collect() } } @@ -445,25 +430,8 @@ impl From for Price { } } -lazy_static::lazy_static! { - static ref UNIT: num::BigInt = num::BigInt::from(1_000_000_000_000_000_000_u128); -} - -/// The price of a token normalized to native token price. -#[derive(Debug, Clone)] -pub struct NormalizedPrice(pub BigRational); - -impl From for NormalizedPrice { - fn from(value: BigRational) -> Self { - Self(value) - } -} - -/// All auction prices normalized to native token price. -/// -/// For example, auction price of 1ETH is 1e18, while normalized price of 1ETH -/// is 1. -pub type NormalizedPrices = HashMap; +/// All auction prices +pub type Prices = HashMap; #[derive(Debug, Clone, Copy)] pub struct Id(pub i64); diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index 258094aeaa..fcd878920f 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -1,10 +1,6 @@ use { super::order::{self, Side}, - crate::{ - domain::{competition::auction, eth}, - util::conv::u256::U256Ext, - }, - number::conversions::big_rational_to_u256, + crate::domain::{competition::auction, eth}, }; /// Scoring contains trades in an onchain settleable form and semantics, aligned @@ -27,7 +23,7 @@ impl Scoring { /// Settlement score is valid only if all trade scores are valid. /// /// Denominated in NATIVE token - pub fn score(&self, prices: &auction::NormalizedPrices) -> Result { + pub fn score(&self, prices: &auction::Prices) -> Result { self.trades .iter() .map(|trade| trade.score(prices)) @@ -69,7 +65,7 @@ impl Trade { /// CIP38 score defined as surplus + protocol fee /// /// Denominated in NATIVE token - pub fn score(&self, prices: &auction::NormalizedPrices) -> Result { + pub fn score(&self, prices: &auction::Prices) -> Result { Ok(self.native_surplus(prices)? + self.native_protocol_fee(prices)?) } @@ -120,18 +116,14 @@ impl Trade { /// fees have been applied. /// /// Denominated in NATIVE token - fn native_surplus( - &self, - prices: &auction::NormalizedPrices, - ) -> Result { - let surplus = self - .surplus() - .ok_or(Error::Surplus(self.sell, self.buy))? - .amount; - let native_price = self.surplus_token_price(prices)?; - big_rational_to_u256(&(surplus.0.to_big_rational() * native_price.0)) - .map(Into::into) - .map_err(Into::into) + fn native_surplus(&self, prices: &auction::Prices) -> Result { + let surplus = self.surplus_token_price(prices)?.apply( + self.surplus() + .ok_or(Error::Surplus(self.sell, self.buy))? + .amount, + ); + // normalize + Ok((surplus.0 / &*UNIT).into()) } /// Protocol fee is defined by fee policies attached to the order. @@ -205,15 +197,12 @@ impl Trade { /// Protocol fee is defined by fee policies attached to the order. /// /// Denominated in NATIVE token - fn native_protocol_fee( - &self, - prices: &auction::NormalizedPrices, - ) -> Result { - let protocol_fee = self.protocol_fee()?.amount; - let native_price = self.surplus_token_price(prices)?; - big_rational_to_u256(&(protocol_fee.0.to_big_rational() * native_price.0)) - .map(Into::into) - .map_err(Into::into) + fn native_protocol_fee(&self, prices: &auction::Prices) -> Result { + let protocol_fee = self + .surplus_token_price(prices)? + .apply(self.protocol_fee()?.amount); + // normalize + Ok((protocol_fee.0 / &*UNIT).into()) } fn surplus_token(&self) -> eth::TokenAddress { @@ -223,11 +212,8 @@ impl Trade { } } - /// Returns the normalized price of the trade surplus token - fn surplus_token_price( - &self, - prices: &auction::NormalizedPrices, - ) -> Result { + /// Returns the price of the trade surplus token + fn surplus_token_price(&self, prices: &auction::Prices) -> Result { prices .get(&self.surplus_token()) .cloned() @@ -271,3 +257,7 @@ pub enum Error { #[error(transparent)] Math(#[from] super::Math), } + +lazy_static::lazy_static! { + static ref UNIT: eth::U256 = eth::U256::from(1_000_000_000_000_000_000_u128); +} diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index afaf366c79..b3be76411a 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -219,12 +219,13 @@ impl Settlement { &self, auction: &competition::Auction, ) -> Result { - let prices = auction.normalized_prices(); + let prices = auction.prices(); let mut score = eth::TokenAmount(eth::U256::zero()); for solution in self.solutions.values() { score += solution.scoring()?.score(&prices)?; } + Ok(score) } From c6baef02d0ffa41e96760dad121d5785ff274ddb Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 18:01:05 +0100 Subject: [PATCH 20/31] refactor further --- crates/driver/src/domain/competition/score.rs | 2 +- .../driver/src/domain/competition/solution/mod.rs | 13 ++++++++----- .../src/domain/competition/solution/scoring.rs | 4 ++-- .../src/domain/competition/solution/settlement.rs | 4 ++-- crates/driver/src/infra/solver/dto/solution.rs | 3 --- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/driver/src/domain/competition/score.rs b/crates/driver/src/domain/competition/score.rs index 42db6b4aaa..0e0885b9c8 100644 --- a/crates/driver/src/domain/competition/score.rs +++ b/crates/driver/src/domain/competition/score.rs @@ -67,7 +67,7 @@ pub enum Error { #[error(transparent)] RiskAdjusted(#[from] risk::Error), #[error(transparent)] - Scoring(#[from] super::solution::Scoring), + Scoring(#[from] super::solution::ScoringError), #[error(transparent)] Boundary(#[from] boundary::Error), } diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index ad4ab4b258..0c1aabc71f 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -1,5 +1,6 @@ use { self::trade::ClearingPrices, + super::auction, crate::{ boundary, domain::{ @@ -121,7 +122,7 @@ impl Solution { } /// Converts the solution into scoring form - pub fn scoring(&self) -> Result { + pub fn scoring(&self, prices: &auction::Prices) -> Result { let mut trades = Vec::with_capacity(self.trades.len()); for trade in self.user_trades() { // Solver generated fulfillment does not include the fee in the executed amount @@ -172,7 +173,9 @@ impl Solution { trade.order().protocol_fees.clone(), )) } - Ok(scoring::Scoring::new(trades)) + + let scoring = scoring::Scoring::new(trades); + Ok(scoring.score(prices)?) } /// Approval interactions necessary for encoding the settlement. @@ -378,16 +381,16 @@ pub enum SolutionError { InvalidClearingPrices, #[error(transparent)] ProtocolFee(#[from] fee::Error), - #[error(transparent)] - Math(#[from] Math), } #[derive(Debug, thiserror::Error)] -pub enum Scoring { +pub enum ScoringError { #[error(transparent)] Solution(#[from] SolutionError), #[error(transparent)] Score(#[from] scoring::Error), + #[error(transparent)] + Math(#[from] Math), } #[derive(Debug, thiserror::Error)] diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index fcd878920f..c2d85b7449 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -123,7 +123,7 @@ impl Trade { .amount, ); // normalize - Ok((surplus.0 / &*UNIT).into()) + Ok((surplus.0 / *UNIT).into()) } /// Protocol fee is defined by fee policies attached to the order. @@ -202,7 +202,7 @@ impl Trade { .surplus_token_price(prices)? .apply(self.protocol_fee()?.amount); // normalize - Ok((protocol_fee.0 / &*UNIT).into()) + Ok((protocol_fee.0 / *UNIT).into()) } fn surplus_token(&self) -> eth::TokenAddress { diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index b3be76411a..1afeb90884 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -218,12 +218,12 @@ impl Settlement { fn scoring( &self, auction: &competition::Auction, - ) -> Result { + ) -> Result { let prices = auction.prices(); let mut score = eth::TokenAmount(eth::U256::zero()); for solution in self.solutions.values() { - score += solution.scoring()?.score(&prices)?; + score += solution.scoring(&prices)?; } Ok(score) diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index c68b03ab96..2ed5bc3772 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -205,9 +205,6 @@ impl Solutions { competition::solution::SolutionError::ProtocolFee(err) => { super::Error(format!("could not incorporate protocol fee: {err}")) } - competition::solution::SolutionError::Math(err) => { - super::Error(format!("math error: {err}")) - } }) }) .collect() From 47e8cc3f381ff4994e756822f48341b7cced543c Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 18:05:20 +0100 Subject: [PATCH 21/31] pretty format --- .../src/domain/competition/solution/settlement.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 1afeb90884..4eddb3795b 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -221,12 +221,12 @@ impl Settlement { ) -> Result { let prices = auction.prices(); - let mut score = eth::TokenAmount(eth::U256::zero()); - for solution in self.solutions.values() { - score += solution.scoring(&prices)?; - } - - Ok(score) + self.solutions + .values() + .map(|solution| solution.scoring(&prices)) + .try_fold(eth::TokenAmount(eth::U256::zero()), |acc, score| { + score.map(|score| acc + score) + }) } // TODO(#1494): score() should be defined on Solution rather than Settlement. From 23ca5b9bd092ab541acf119073966d6d8943b5d1 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 18:07:05 +0100 Subject: [PATCH 22/31] fix comment --- crates/driver/src/domain/competition/solution/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 0c1aabc71f..4a7d444b5a 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -121,7 +121,7 @@ impl Solution { &self.score } - /// Converts the solution into scoring form + /// JIT score calculation as per CIP38 pub fn scoring(&self, prices: &auction::Prices) -> Result { let mut trades = Vec::with_capacity(self.trades.len()); for trade in self.user_trades() { From cef632d52f80c83b63ed507962e949e2b896e7a8 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 18:25:02 +0100 Subject: [PATCH 23/31] errors fixed --- .../src/domain/competition/solution/scoring.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index c2d85b7449..44783528ac 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -18,7 +18,8 @@ impl Scoring { /// Score of a settlement as per CIP38 /// /// Score of a settlement is a sum of scores of all user trades in the - /// settlement. + /// settlement. Score is defined as an order's surplus plus its protocol + /// fee. /// /// Settlement score is valid only if all trade scores are valid. /// @@ -182,8 +183,8 @@ impl Trade { factor: _, max_volume_factor: _, quote: _, - } => Err(Error::UnsupportedFeePolicy), - order::FeePolicy::Volume { factor: _ } => Err(Error::UnsupportedFeePolicy), + } => Err(Error::UnimplementedFeePolicy), + order::FeePolicy::Volume { factor: _ } => Err(Error::UnimplementedFeePolicy), } }; @@ -244,14 +245,12 @@ pub struct CustomClearingPrices { pub enum Error { #[error("multiple fee policies are not supported yet")] MultipleFeePolicies, + #[error("fee policy not implemented yet")] + UnimplementedFeePolicy, #[error("failed to calculate surplus for trade sell {0:?} buy {1:?}")] Surplus(eth::Asset, eth::Asset), #[error("missing native price for token {0:?}")] MissingPrice(eth::TokenAddress), - #[error("type conversion error")] - TypeConversion(#[from] anyhow::Error), - #[error("fee policy not supported")] - UnsupportedFeePolicy, #[error("factor {1} multiplication with {0} failed")] Factor(eth::U256, f64), #[error(transparent)] From 320a04f76b649588c246c04dfe77d67ebe717540 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Fri, 1 Mar 2024 18:27:41 +0100 Subject: [PATCH 24/31] private fn --- crates/driver/src/domain/competition/solution/scoring.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index 44783528ac..4e2e1c99d8 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -66,7 +66,7 @@ impl Trade { /// CIP38 score defined as surplus + protocol fee /// /// Denominated in NATIVE token - pub fn score(&self, prices: &auction::Prices) -> Result { + fn score(&self, prices: &auction::Prices) -> Result { Ok(self.native_surplus(prices)? + self.native_protocol_fee(prices)?) } From 4b29c66a7d82a0e64b2dae32e0eca174b52ba2d0 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Sun, 3 Mar 2024 22:18:33 +0100 Subject: [PATCH 25/31] fixed driver tests --- crates/driver/src/tests/cases/protocol_fees.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/driver/src/tests/cases/protocol_fees.rs b/crates/driver/src/tests/cases/protocol_fees.rs index 42e526e32f..02b92eb185 100644 --- a/crates/driver/src/tests/cases/protocol_fees.rs +++ b/crates/driver/src/tests/cases/protocol_fees.rs @@ -144,7 +144,7 @@ async fn surplus_protocol_fee_sell_order_capped() { quote_buy_amount: 40000000000000000000u128.into(), executed: 40000000000000000000u128.into(), executed_sell_amount: 50000000000000000000u128.into(), - executed_buy_amount: 35000000000000000000u128.into(), + executed_buy_amount: 36000000000000000000u128.into(), }; protocol_fee_test_case(test_case).await; @@ -182,7 +182,7 @@ async fn volume_protocol_fee_sell_order() { quote_buy_amount: 40000000000000000000u128.into(), executed: 40000000000000000000u128.into(), executed_sell_amount: 50000000000000000000u128.into(), - executed_buy_amount: 15000000000000000000u128.into(), + executed_buy_amount: 20000000000000000000u128.into(), }; protocol_fee_test_case(test_case).await; From 243792db918cb172e22fd75f87b60db193c336cd Mon Sep 17 00:00:00 2001 From: sunce86 Date: Mon, 4 Mar 2024 14:21:57 +0100 Subject: [PATCH 26/31] cr fixes --- crates/driver/src/domain/competition/auction.rs | 2 +- .../src/domain/competition/solution/scoring.rs | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index f2e0e56c59..036d30ac32 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -120,7 +120,7 @@ impl Auction { .filter_map(|(address, token)| token.price.map(|price| (*address, price))) .chain(std::iter::once(( eth::ETH_TOKEN, - eth::U256::from(1_000_000_000_000_000_000u128).into(), + eth::U256::exp10(18).into(), ))) .collect() } diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index 4e2e1c99d8..e4b4573d78 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -1,6 +1,12 @@ use { super::order::{self, Side}, - crate::domain::{competition::auction, eth}, + crate::{ + domain::{competition::auction, eth}, + util::conv::u256::U256Ext, + }, + bigdecimal::FromPrimitive, + num::CheckedMul, + number::conversions::big_rational_to_u256, }; /// Scoring contains trades in an onchain settleable form and semantics, aligned @@ -223,10 +229,9 @@ impl Trade { } fn apply_factor(amount: eth::U256, factor: f64) -> Option { - Some( - amount.checked_mul(eth::U256::from_f64_lossy(factor * 1000000000000000000.))? - / 1000000000000000000u128, - ) + let amount = amount.to_big_rational(); + let factor = num::BigRational::from_f64(factor)?; + big_rational_to_u256(&amount.checked_mul(&factor)?).ok() } /// Custom clearing prices at which the trade was executed. From 5d8cdcfc807eb3d0e64f7b67c829b61de01ea640 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Mon, 4 Mar 2024 15:16:42 +0100 Subject: [PATCH 27/31] cr fixes 2 --- .../src/domain/competition/solution/scoring.rs | 12 ++++++++++-- .../src/domain/competition/solution/settlement.rs | 4 ++-- crates/solvers/src/boundary/legacy.rs | 4 +++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index e4b4573d78..1744953dbf 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -9,8 +9,11 @@ use { number::conversions::big_rational_to_u256, }; -/// Scoring contains trades in an onchain settleable form and semantics, aligned -/// with what the settlement contract expects. +/// Scoring contains trades with values as they are expected by the settlement +/// contracts. This means that executed amounts and custom clearing prices have +/// the same values here and after being mined onchain. This allows us to use +/// the same math for calculating surplus and fees in the driver and in the +/// autopilot. #[derive(Debug, Clone)] pub struct Scoring { trades: Vec, @@ -40,6 +43,11 @@ impl Scoring { } } +// Trade represents a single trade in a settlement. +// +// It contains values as expected by the settlement contract. That means that +// clearing prices are adjusted to account for all fees (gas cost and protocol +// fees). Also, executed amount contains the fees for sell order. #[derive(Debug, Clone)] pub struct Trade { sell: eth::Asset, diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 4eddb3795b..d52961563c 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -215,7 +215,7 @@ impl Settlement { .into() } - fn scoring( + fn cip38_score( &self, auction: &competition::Auction, ) -> Result { @@ -238,7 +238,7 @@ impl Settlement { revert_protection: &mempools::RevertProtection, ) -> Result { // For testing purposes, calculate CIP38 even before activation - let score = self.scoring(auction); + let score = self.cip38_score(auction); tracing::info!(?score, "CIP38 score for settlement: {:?}", self.solutions()); let score = match self.boundary.score() { diff --git a/crates/solvers/src/boundary/legacy.rs b/crates/solvers/src/boundary/legacy.rs index 8878a1ce93..4eaa64778e 100644 --- a/crates/solvers/src/boundary/legacy.rs +++ b/crates/solvers/src/boundary/legacy.rs @@ -575,7 +575,9 @@ fn to_domain_solution( success_probability, .. } => solution::Score::RiskAdjusted(solution::SuccessProbability(success_probability)), - Score::Surplus => solution::Score::Solver(0.into()), + Score::Surplus => { + return Err(anyhow::anyhow!("solvers not allowed to use surplus score")) + } }, }) } From 4c912504c3f7c7189346aa5d283787ac795d11b9 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 6 Mar 2024 12:26:07 +0100 Subject: [PATCH 28/31] fix errors --- crates/driver/src/domain/competition/score.rs | 2 +- .../src/domain/competition/solution/mod.rs | 58 ++++++++----------- .../domain/competition/solution/scoring.rs | 15 +++-- .../domain/competition/solution/settlement.rs | 2 +- .../driver/src/infra/solver/dto/solution.rs | 4 +- 5 files changed, 38 insertions(+), 43 deletions(-) diff --git a/crates/driver/src/domain/competition/score.rs b/crates/driver/src/domain/competition/score.rs index 0e0885b9c8..e39f33f957 100644 --- a/crates/driver/src/domain/competition/score.rs +++ b/crates/driver/src/domain/competition/score.rs @@ -67,7 +67,7 @@ pub enum Error { #[error(transparent)] RiskAdjusted(#[from] risk::Error), #[error(transparent)] - Scoring(#[from] super::solution::ScoringError), + Scoring(#[from] super::solution::error::Scoring), #[error(transparent)] Boundary(#[from] boundary::Error), } diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index cce10a48e5..a1afd2e2d0 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -53,7 +53,7 @@ impl Solution { solver: Solver, score: SolverScore, weth: eth::WethAddress, - ) -> Result { + ) -> Result { let solution = Self { id, trades, @@ -69,7 +69,7 @@ impl Solution { solution.clearing_price(trade.order().sell.token).is_none() || solution.clearing_price(trade.order().buy.token).is_none() }) { - return Err(SolutionError::InvalidClearingPrices); + return Err(error::Solution::InvalidClearingPrices); } // Apply protocol fees @@ -122,7 +122,7 @@ impl Solution { } /// JIT score calculation as per CIP38 - pub fn scoring(&self, prices: &auction::Prices) -> Result { + pub fn scoring(&self, prices: &auction::Prices) -> Result { let mut trades = Vec::with_capacity(self.trades.len()); for trade in self.user_trades() { // Solver generated fulfillment does not include the fee in the executed amount @@ -135,11 +135,11 @@ impl Solution { sell: *self .prices .get(&trade.order().sell.token.wrap(self.weth)) - .ok_or(SolutionError::InvalidClearingPrices)?, + .ok_or(error::Solution::InvalidClearingPrices)?, buy: *self .prices .get(&trade.order().buy.token.wrap(self.weth)) - .ok_or(SolutionError::InvalidClearingPrices)?, + .ok_or(error::Solution::InvalidClearingPrices)?, }; let custom_prices = scoring::CustomClearingPrices { sell: match trade.order().side { @@ -147,9 +147,9 @@ impl Solution { .executed() .0 .checked_mul(uniform_prices.sell) - .ok_or(Math::Overflow)? + .ok_or(error::Math::Overflow)? .checked_div(uniform_prices.buy) - .ok_or(Math::DivisionByZero)?, + .ok_or(error::Math::DivisionByZero)?, order::Side::Buy => trade.executed().0, }, buy: match trade.order().side { @@ -157,9 +157,9 @@ impl Solution { order::Side::Buy => { (trade.executed().0) .checked_mul(uniform_prices.buy) - .ok_or(Math::Overflow)? + .ok_or(error::Math::Overflow)? .checked_div(uniform_prices.sell) - .ok_or(Math::DivisionByZero)? + .ok_or(error::Math::DivisionByZero)? + trade.fee().0 } }, @@ -386,30 +386,22 @@ pub mod error { #[error("division by zero")] DivisionByZero, } -} - -#[derive(Debug, thiserror::Error)] -pub enum SolutionError { - #[error("invalid clearing prices")] - InvalidClearingPrices, - #[error(transparent)] - ProtocolFee(#[from] fee::Error), -} -#[derive(Debug, thiserror::Error)] -pub enum ScoringError { - #[error(transparent)] - Solution(#[from] SolutionError), - #[error(transparent)] - Score(#[from] scoring::Error), - #[error(transparent)] - Math(#[from] Math), -} + #[derive(Debug, thiserror::Error)] + pub enum Solution { + #[error("invalid clearing prices")] + InvalidClearingPrices, + #[error(transparent)] + ProtocolFee(#[from] fee::Error), + } -#[derive(Debug, thiserror::Error)] -pub enum Math { - #[error("overflow")] - Overflow, - #[error("division by zero")] - DivisionByZero, + #[derive(Debug, thiserror::Error)] + pub enum Scoring { + #[error(transparent)] + Solution(#[from] Solution), + #[error(transparent)] + Math(#[from] Math), + #[error(transparent)] + Score(#[from] scoring::Error), + } } diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index 1744953dbf..387d2905be 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -1,5 +1,8 @@ use { - super::order::{self, Side}, + super::{ + error::Math, + order::{self, Side}, + }, crate::{ domain::{competition::auction, eth}, util::conv::u256::U256Ext, @@ -174,16 +177,16 @@ impl Trade { .executed .0 .checked_mul(self.custom_price.sell) - .ok_or(super::Math::Overflow)? + .ok_or(Math::Overflow)? .checked_div(self.custom_price.buy) - .ok_or(super::Math::DivisionByZero)?, + .ok_or(Math::DivisionByZero)?, Side::Buy => self .executed .0 .checked_mul(self.custom_price.buy) - .ok_or(super::Math::Overflow)? + .ok_or(Math::Overflow)? .checked_div(self.custom_price.sell) - .ok_or(super::Math::DivisionByZero)?, + .ok_or(Math::DivisionByZero)?, }; let factor = match self.side { Side::Sell => max_volume_factor / (1.0 - max_volume_factor), @@ -267,7 +270,7 @@ pub enum Error { #[error("factor {1} multiplication with {0} failed")] Factor(eth::U256, f64), #[error(transparent)] - Math(#[from] super::Math), + Math(#[from] Math), } lazy_static::lazy_static! { diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 48ecd7a59e..817ae1817e 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -218,7 +218,7 @@ impl Settlement { fn cip38_score( &self, auction: &competition::Auction, - ) -> Result { + ) -> Result { let prices = auction.prices(); self.solutions diff --git a/crates/driver/src/infra/solver/dto/solution.rs b/crates/driver/src/infra/solver/dto/solution.rs index 2ed5bc3772..e53458329f 100644 --- a/crates/driver/src/infra/solver/dto/solution.rs +++ b/crates/driver/src/infra/solver/dto/solution.rs @@ -199,10 +199,10 @@ impl Solutions { weth, ) .map_err(|err| match err { - competition::solution::SolutionError::InvalidClearingPrices => { + competition::solution::error::Solution::InvalidClearingPrices => { super::Error("invalid clearing prices".to_owned()) } - competition::solution::SolutionError::ProtocolFee(err) => { + competition::solution::error::Solution::ProtocolFee(err) => { super::Error(format!("could not incorporate protocol fee: {err}")) } }) From 43c0630f3b798a1f41ed169836b9ea197c94b866 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 6 Mar 2024 12:33:51 +0100 Subject: [PATCH 29/31] score is ether --- crates/driver/src/domain/competition/solution/mod.rs | 2 +- .../driver/src/domain/competition/solution/scoring.rs | 10 +++++----- .../src/domain/competition/solution/settlement.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index a1afd2e2d0..6fd35cfbac 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -122,7 +122,7 @@ impl Solution { } /// JIT score calculation as per CIP38 - pub fn scoring(&self, prices: &auction::Prices) -> Result { + pub fn scoring(&self, prices: &auction::Prices) -> Result { let mut trades = Vec::with_capacity(self.trades.len()); for trade in self.user_trades() { // Solver generated fulfillment does not include the fee in the executed amount diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index 387d2905be..39c1a9fd17 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -36,11 +36,11 @@ impl Scoring { /// Settlement score is valid only if all trade scores are valid. /// /// Denominated in NATIVE token - pub fn score(&self, prices: &auction::Prices) -> Result { + pub fn score(&self, prices: &auction::Prices) -> Result { self.trades .iter() .map(|trade| trade.score(prices)) - .try_fold(eth::TokenAmount(eth::U256::zero()), |acc, score| { + .try_fold(eth::Ether(eth::U256::zero()), |acc, score| { score.map(|score| acc + score) }) } @@ -83,7 +83,7 @@ impl Trade { /// CIP38 score defined as surplus + protocol fee /// /// Denominated in NATIVE token - fn score(&self, prices: &auction::Prices) -> Result { + fn score(&self, prices: &auction::Prices) -> Result { Ok(self.native_surplus(prices)? + self.native_protocol_fee(prices)?) } @@ -134,7 +134,7 @@ impl Trade { /// fees have been applied. /// /// Denominated in NATIVE token - fn native_surplus(&self, prices: &auction::Prices) -> Result { + fn native_surplus(&self, prices: &auction::Prices) -> Result { let surplus = self.surplus_token_price(prices)?.apply( self.surplus() .ok_or(Error::Surplus(self.sell, self.buy))? @@ -215,7 +215,7 @@ impl Trade { /// Protocol fee is defined by fee policies attached to the order. /// /// Denominated in NATIVE token - fn native_protocol_fee(&self, prices: &auction::Prices) -> Result { + fn native_protocol_fee(&self, prices: &auction::Prices) -> Result { let protocol_fee = self .surplus_token_price(prices)? .apply(self.protocol_fee()?.amount); diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 817ae1817e..97c1bad88d 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -218,13 +218,13 @@ impl Settlement { fn cip38_score( &self, auction: &competition::Auction, - ) -> Result { + ) -> Result { let prices = auction.prices(); self.solutions .values() .map(|solution| solution.scoring(&prices)) - .try_fold(eth::TokenAmount(eth::U256::zero()), |acc, score| { + .try_fold(eth::Ether(eth::U256::zero()), |acc, score| { score.map(|score| acc + score) }) } From 8e8f3db297400a0eb7ea0506419125961d5b25d5 Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 6 Mar 2024 13:08:07 +0100 Subject: [PATCH 30/31] apply_factor moved on to TokenAmount --- .../src/domain/competition/solution/fee.rs | 49 +++++++++++-------- .../domain/competition/solution/scoring.rs | 33 +++++-------- .../domain/competition/solution/settlement.rs | 2 +- .../src/domain/competition/solution/trade.rs | 4 +- crates/driver/src/domain/eth/mod.rs | 25 +++++++++- 5 files changed, 68 insertions(+), 45 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 3e2b73e87e..8379b9e974 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -34,6 +34,7 @@ use { }, eth, }, + bigdecimal::Zero, }; impl Fulfillment { @@ -50,7 +51,7 @@ impl Fulfillment { Fee::Static } Some(fee) => { - Fee::Dynamic((fee.0.checked_add(protocol_fee).ok_or(Math::Overflow)?).into()) + Fee::Dynamic((fee.0.checked_add(protocol_fee.0).ok_or(Math::Overflow)?).into()) } }; @@ -63,7 +64,7 @@ impl Fulfillment { order::Side::Sell => order::TargetAmount( self.executed() .0 - .checked_sub(protocol_fee) + .checked_sub(protocol_fee.0) .ok_or(Math::Overflow)?, ), }; @@ -72,7 +73,7 @@ impl Fulfillment { } /// Computed protocol fee in surplus token. - fn protocol_fee(&self, prices: ClearingPrices) -> Result { + fn protocol_fee(&self, prices: ClearingPrices) -> Result { // TODO: support multiple fee policies if self.order().protocol_fees.len() > 1 { return Err(Error::MultipleFeePolicies); @@ -120,7 +121,7 @@ impl Fulfillment { prices: ClearingPrices, factor: f64, max_volume_factor: f64, - ) -> Result { + ) -> Result { let fee_from_surplus = self.fee_from_surplus(limit_sell_amount, limit_buy_amount, prices, factor)?; let fee_from_volume = self.fee_from_volume(prices, max_volume_factor)?; @@ -137,45 +138,53 @@ impl Fulfillment { buy_amount: eth::U256, prices: ClearingPrices, factor: f64, - ) -> Result { + ) -> Result { let surplus = self.surplus_over_reference_price(sell_amount, buy_amount, prices)?; - apply_factor(surplus, factor) + surplus + .apply_factor(factor) + .ok_or(Math::Overflow) + .map_err(Into::into) } /// Computes the volume based fee in surplus token /// /// The volume is defined as a full sell amount (including fees) for buy /// order, or a full buy amount for sell order. - fn fee_from_volume(&self, prices: ClearingPrices, factor: f64) -> Result { + fn fee_from_volume( + &self, + prices: ClearingPrices, + factor: f64, + ) -> Result { let volume = match self.order().side { Side::Buy => self.sell_amount(&prices)?, Side::Sell => self.buy_amount(&prices)?, }; - apply_factor(volume.0, factor) + volume + .apply_factor(factor) + .ok_or(Math::Overflow) + .map_err(Into::into) } /// Returns the protocol fee denominated in the sell token. - fn protocol_fee_in_sell_token(&self, prices: ClearingPrices) -> Result { - let fee = self.protocol_fee(prices)?; + fn protocol_fee_in_sell_token( + &self, + prices: ClearingPrices, + ) -> Result { let fee_in_sell_token = match self.order().side { - Side::Buy => fee, - Side::Sell => fee + Side::Buy => self.protocol_fee(prices)?, + Side::Sell => self + .protocol_fee(prices)? + .0 .checked_mul(prices.buy) .ok_or(Math::Overflow)? .checked_div(prices.sell) - .ok_or(Math::DivisionByZero)?, + .ok_or(Math::DivisionByZero)? + .into(), }; Ok(fee_in_sell_token) } } -fn apply_factor(amount: eth::U256, factor: f64) -> Result { - Ok(amount - .checked_mul(eth::U256::from_f64_lossy(factor * 1000000000000000000.)) - .ok_or(Math::Overflow)? - / 1000000000000000000u128) -} - /// This function adjusts quote amounts to directly compare them with the /// order's limits, ensuring a meaningful comparison for potential price /// improvements. It scales quote amounts when necessary, accounting for quote diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index 39c1a9fd17..74328c3805 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -3,13 +3,7 @@ use { error::Math, order::{self, Side}, }, - crate::{ - domain::{competition::auction, eth}, - util::conv::u256::U256Ext, - }, - bigdecimal::FromPrimitive, - num::CheckedMul, - number::conversions::big_rational_to_u256, + crate::domain::{competition::auction, eth}, }; /// Scoring contains trades with values as they are expected by the settlement @@ -40,7 +34,7 @@ impl Scoring { self.trades .iter() .map(|trade| trade.score(prices)) - .try_fold(eth::Ether(eth::U256::zero()), |acc, score| { + .try_fold(eth::Ether(0.into()), |acc, score| { score.map(|score| acc + score) }) } @@ -166,13 +160,14 @@ impl Trade { .surplus() .ok_or(Error::Surplus(self.sell, self.buy))? .amount; - apply_factor(surplus.into(), factor / (1.0 - factor)) - .ok_or(Error::Factor(surplus.0, *factor))? + surplus + .apply_factor(factor / (1.0 - factor)) + .ok_or(Error::Factor(surplus, *factor))? }, { // Convert the executed amount to surplus token so it can be compared // with the surplus - let executed_in_surplus_token = match self.side { + let executed_in_surplus_token: eth::TokenAmount = match self.side { Side::Sell => self .executed .0 @@ -187,12 +182,14 @@ impl Trade { .ok_or(Math::Overflow)? .checked_div(self.custom_price.sell) .ok_or(Math::DivisionByZero)?, - }; + } + .into(); let factor = match self.side { Side::Sell => max_volume_factor / (1.0 - max_volume_factor), Side::Buy => max_volume_factor / (1.0 + max_volume_factor), }; - apply_factor(executed_in_surplus_token, factor) + executed_in_surplus_token + .apply_factor(factor) .ok_or(Error::Factor(executed_in_surplus_token, factor))? }, )), @@ -208,7 +205,7 @@ impl Trade { let protocol_fee = self.policies.first().map(protocol_fee).transpose(); Ok(eth::Asset { token: self.surplus_token(), - amount: protocol_fee?.unwrap_or(0.into()).into(), + amount: protocol_fee?.unwrap_or(0.into()), }) } @@ -239,12 +236,6 @@ impl Trade { } } -fn apply_factor(amount: eth::U256, factor: f64) -> Option { - let amount = amount.to_big_rational(); - let factor = num::BigRational::from_f64(factor)?; - big_rational_to_u256(&amount.checked_mul(&factor)?).ok() -} - /// Custom clearing prices at which the trade was executed. /// /// These prices differ from uniform clearing prices, in that they are adjusted @@ -268,7 +259,7 @@ pub enum Error { #[error("missing native price for token {0:?}")] MissingPrice(eth::TokenAddress), #[error("factor {1} multiplication with {0} failed")] - Factor(eth::U256, f64), + Factor(eth::TokenAmount, f64), #[error(transparent)] Math(#[from] Math), } diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 97c1bad88d..3090583240 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -224,7 +224,7 @@ impl Settlement { self.solutions .values() .map(|solution| solution.scoring(&prices)) - .try_fold(eth::Ether(eth::U256::zero()), |acc, score| { + .try_fold(eth::Ether(0.into()), |acc, score| { score.map(|score| acc + score) }) } diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index 6728265fd8..33e7ae1eab 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -142,7 +142,7 @@ impl Fulfillment { limit_sell: eth::U256, limit_buy: eth::U256, prices: ClearingPrices, - ) -> Result { + ) -> Result { let executed = self.executed().0; let executed_sell_amount = match self.order().side { Side::Buy => { @@ -200,7 +200,7 @@ impl Fulfillment { .unwrap_or(eth::U256::zero()) } }; - Ok(surplus) + Ok(surplus.into()) } } diff --git a/crates/driver/src/domain/eth/mod.rs b/crates/driver/src/domain/eth/mod.rs index 66492362bb..07ed18abdb 100644 --- a/crates/driver/src/domain/eth/mod.rs +++ b/crates/driver/src/domain/eth/mod.rs @@ -1,6 +1,9 @@ use { - crate::util::Bytes, + crate::util::{conv::u256::U256Ext, Bytes}, + bigdecimal::FromPrimitive, itertools::Itertools, + num::CheckedMul, + number::conversions::big_rational_to_u256, std::collections::{HashMap, HashSet}, }; @@ -177,6 +180,16 @@ impl TokenAddress { #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct TokenAmount(pub U256); +impl TokenAmount { + pub fn apply_factor(&self, factor: f64) -> Option { + let amount = self.0.to_big_rational(); + let factor = num::BigRational::from_f64(factor)?; + big_rational_to_u256(&amount.checked_mul(&factor)?) + .ok() + .map(Self) + } +} + impl From for TokenAmount { fn from(value: U256) -> Self { Self(value) @@ -209,6 +222,16 @@ impl std::ops::AddAssign for TokenAmount { } } +impl num::Zero for TokenAmount { + fn zero() -> Self { + Self(U256::zero()) + } + + fn is_zero(&self) -> bool { + self.0.is_zero() + } +} + impl std::fmt::Display for TokenAmount { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) From 3559c9f89ed21afe9c222ecc5719683872f437fb Mon Sep 17 00:00:00 2001 From: sunce86 Date: Wed, 6 Mar 2024 13:34:31 +0100 Subject: [PATCH 31/31] revert to old, BigRational::from_f64 not work --- crates/driver/src/domain/eth/mod.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/driver/src/domain/eth/mod.rs b/crates/driver/src/domain/eth/mod.rs index 07ed18abdb..5716f3d598 100644 --- a/crates/driver/src/domain/eth/mod.rs +++ b/crates/driver/src/domain/eth/mod.rs @@ -1,9 +1,6 @@ use { - crate::util::{conv::u256::U256Ext, Bytes}, - bigdecimal::FromPrimitive, + crate::util::Bytes, itertools::Itertools, - num::CheckedMul, - number::conversions::big_rational_to_u256, std::collections::{HashMap, HashSet}, }; @@ -182,11 +179,13 @@ pub struct TokenAmount(pub U256); impl TokenAmount { pub fn apply_factor(&self, factor: f64) -> Option { - let amount = self.0.to_big_rational(); - let factor = num::BigRational::from_f64(factor)?; - big_rational_to_u256(&amount.checked_mul(&factor)?) - .ok() - .map(Self) + Some( + (self + .0 + .checked_mul(U256::from_f64_lossy(factor * 1000000000000000000.))? + / 1000000000000000000u128) + .into(), + ) } }