diff --git a/crates/driver/src/domain/competition/solution/fee.rs b/crates/driver/src/domain/competition/solution/fee.rs index 09721ce69d..cccd5ab2f8 100644 --- a/crates/driver/src/domain/competition/solution/fee.rs +++ b/crates/driver/src/domain/competition/solution/fee.rs @@ -25,12 +25,13 @@ use { super::{ error::Math, - trade::{self, ClearingPrices, Fee, Fulfillment}, + trade::{ClearingPrices, Fee, Fulfillment}, }, crate::domain::{ competition::{ order, order::{FeePolicy, Side}, + solution::error::Trade, PriceLimits, }, eth::{self}, @@ -288,7 +289,7 @@ pub enum Error { #[error(transparent)] Math(#[from] Math), #[error(transparent)] - Fulfillment(#[from] trade::Error), + Fulfillment(#[from] Trade), } // todo: should be removed once integration tests are implemented diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index bfa8a75a18..7de8a120de 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -151,7 +151,9 @@ impl Solution { .get(&trade.order().buy.token.wrap(self.weth)) .ok_or(error::Scoring::InvalidClearingPrices)?, }; - let custom_prices = trade.calculate_custom_prices(&uniform_prices)?; + let custom_prices = trade + .calculate_custom_prices(&uniform_prices) + .map_err(error::Scoring::CalculateCustomPrices)?; trades.push(scoring::Trade::new( trade.order().sell, trade.order().buy, @@ -163,7 +165,7 @@ impl Solution { } let scoring = scoring::Scoring::new(trades); - Ok(scoring.score(prices)?) + scoring.score(prices).map_err(error::Scoring::from) } /// Approval interactions necessary for encoding the settlement. @@ -499,7 +501,29 @@ pub mod error { InvalidClearingPrices, #[error(transparent)] Math(#[from] Math), + #[error("failed to calculate custom prices")] + CalculateCustomPrices(#[source] Trade), + #[error("missing native price for token {0:?}")] + MissingPrice(TokenAddress), + } + + impl From for Scoring { + fn from(value: scoring::Error) -> Self { + match value { + scoring::Error::MissingPrice(e) => Self::MissingPrice(e), + scoring::Error::Math(e) => Self::Math(e), + scoring::Error::Scoring(e) => e, + } + } + } + + #[derive(Debug, thiserror::Error)] + pub enum Trade { + #[error("orders with non solver determined gas cost fees are not supported")] + ProtocolFeeOnStaticOrder, + #[error("invalid executed amount")] + InvalidExecutedAmount, #[error(transparent)] - Score(#[from] scoring::Error), + Math(#[from] Math), } } diff --git a/crates/driver/src/domain/competition/solution/scoring.rs b/crates/driver/src/domain/competition/solution/scoring.rs index 848d0375f8..0700765894 100644 --- a/crates/driver/src/domain/competition/solution/scoring.rs +++ b/crates/driver/src/domain/competition/solution/scoring.rs @@ -176,7 +176,7 @@ impl Trade { if !i.is_zero() { current_trade.custom_price = self .calculate_custom_prices(amount) - .map_err(|e| Error::CustomPrice(e.to_string()))?; + .map_err(Error::Scoring)?; } } @@ -419,6 +419,6 @@ pub enum Error { MissingPrice(eth::TokenAddress), #[error(transparent)] Math(#[from] Math), - #[error("failed to calculate custom price {0:?}")] - CustomPrice(String), + #[error("scoring: failed to calculate custom price for the applied fee policy {0:?}")] + Scoring(#[source] error::Scoring), } diff --git a/crates/driver/src/domain/competition/solution/trade.rs b/crates/driver/src/domain/competition/solution/trade.rs index e1b48b4293..734760cfd9 100644 --- a/crates/driver/src/domain/competition/solution/trade.rs +++ b/crates/driver/src/domain/competition/solution/trade.rs @@ -37,7 +37,7 @@ impl Fulfillment { order: competition::Order, executed: order::TargetAmount, fee: Fee, - ) -> Result { + ) -> Result { // If the order is partial, the total executed amount can be smaller than // the target amount. Otherwise, the executed amount must be equal to the target // amount. @@ -54,7 +54,7 @@ impl Fulfillment { executed .0 .checked_add(fee.0) - .ok_or(Error::InvalidExecutedAmount)?, + .ok_or(error::Trade::InvalidExecutedAmount)?, ); match order.partial { order::Partial::Yes { available } => executed_with_fee <= available, @@ -76,7 +76,7 @@ impl Fulfillment { fee, }) } else { - Err(Error::InvalidExecutedAmount) + Err(error::Trade::InvalidExecutedAmount) } } @@ -86,7 +86,7 @@ impl Fulfillment { pub fn calculate_custom_prices( &self, uniform_prices: &ClearingPrices, - ) -> Result { + ) -> Result { Ok(scoring::CustomClearingPrices { sell: match self.order().side { Side::Sell => self @@ -147,7 +147,7 @@ impl Fulfillment { } /// The effective amount that left the user's wallet including all fees. - pub fn sell_amount(&self, prices: &ClearingPrices) -> Result { + pub fn sell_amount(&self, prices: &ClearingPrices) -> Result { let before_fee = match self.order.side { order::Side::Sell => self.executed.0, order::Side::Buy => self @@ -164,7 +164,7 @@ impl Fulfillment { } /// The effective amount the user received after all fees. - pub fn buy_amount(&self, prices: &ClearingPrices) -> Result { + pub fn buy_amount(&self, prices: &ClearingPrices) -> Result { let amount = match self.order.side { order::Side::Buy => self.executed.0, order::Side::Sell => self @@ -187,7 +187,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 => { @@ -206,7 +206,7 @@ impl Fulfillment { // surplus_fee is always expressed in sell token self.surplus_fee() .map(|fee| fee.0) - .ok_or(Error::ProtocolFeeOnStaticOrder)?, + .ok_or(error::Trade::ProtocolFeeOnStaticOrder)?, ) .ok_or(Math::Overflow)?; let surplus = match self.order().side { @@ -279,7 +279,7 @@ pub struct Jit { } impl Jit { - pub fn new(order: order::Jit, executed: order::TargetAmount) -> Result { + pub fn new(order: order::Jit, executed: order::TargetAmount) -> Result { // If the order is partially fillable, the executed amount can be smaller than // the target amount. Otherwise, the executed amount must be equal to the target // amount. @@ -291,7 +291,7 @@ impl Jit { if is_valid { Ok(Self { order, executed }) } else { - Err(Error::InvalidExecutedAmount) + Err(error::Trade::InvalidExecutedAmount) } } @@ -312,13 +312,3 @@ pub struct Execution { /// The total amount being bought. pub buy: eth::Asset, } - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("orders with non solver determined gas cost fees are not supported")] - ProtocolFeeOnStaticOrder, - #[error("invalid executed amount")] - InvalidExecutedAmount, - #[error(transparent)] - Math(#[from] Math), -} diff --git a/crates/driver/src/infra/notify/mod.rs b/crates/driver/src/infra/notify/mod.rs index 4cb725abc1..5709910188 100644 --- a/crates/driver/src/infra/notify/mod.rs +++ b/crates/driver/src/infra/notify/mod.rs @@ -33,7 +33,16 @@ pub fn scoring_failed( solution::error::Scoring::InvalidClearingPrices => { notification::Kind::ScoringFailed(ScoreKind::InvalidClearingPrices) } - solution::error::Scoring::Math(_) | solution::error::Scoring::Score(_) => return, + solution::error::Scoring::Math(_) + | solution::error::Scoring::CalculateCustomPrices( + solution::error::Trade::Math(_) | solution::error::Trade::ProtocolFeeOnStaticOrder, + ) => return, + solution::error::Scoring::CalculateCustomPrices( + solution::error::Trade::InvalidExecutedAmount, + ) => notification::Kind::ScoringFailed(ScoreKind::InvalidExecutedAmount), + solution::error::Scoring::MissingPrice(token) => { + notification::Kind::ScoringFailed(ScoreKind::MissingPrice(*token)) + } }; solver.notify(auction_id, Some(solution_id.clone()), notification); diff --git a/crates/driver/src/infra/notify/notification.rs b/crates/driver/src/infra/notify/notification.rs index 3201a338a6..12db6c7200 100644 --- a/crates/driver/src/infra/notify/notification.rs +++ b/crates/driver/src/infra/notify/notification.rs @@ -51,6 +51,11 @@ pub enum Kind { pub enum ScoreKind { /// No clearing prices are present for all trades. InvalidClearingPrices, + /// The amount executed is invalid: out of range or the fee doesn't match + /// its execution with fee + InvalidExecutedAmount, + /// missing native price for the surplus token + MissingPrice(TokenAddress), } #[derive(Debug)] diff --git a/crates/driver/src/infra/solver/dto/notification.rs b/crates/driver/src/infra/solver/dto/notification.rs index c5e671a90e..565be2a027 100644 --- a/crates/driver/src/infra/solver/dto/notification.rs +++ b/crates/driver/src/infra/solver/dto/notification.rs @@ -2,7 +2,7 @@ use { crate::{ domain::{ competition::{auction, solution}, - eth, + eth::{self}, }, infra::notify, util::serialize, @@ -38,9 +38,7 @@ impl Notification { succeeded_once, } } - notify::Kind::ScoringFailed(notify::ScoreKind::InvalidClearingPrices) => { - Kind::InvalidClearingPrices - } + notify::Kind::ScoringFailed(scoring) => scoring.into(), notify::Kind::NonBufferableTokensUsed(tokens) => Kind::NonBufferableTokensUsed { tokens: tokens.into_iter().map(|token| token.0 .0).collect(), }, @@ -67,6 +65,18 @@ impl Notification { } } +impl From for Kind { + fn from(value: notify::ScoreKind) -> Self { + match value { + notify::ScoreKind::InvalidClearingPrices => Kind::InvalidClearingPrices, + notify::ScoreKind::InvalidExecutedAmount => Kind::InvalidExecutedAmount, + notify::ScoreKind::MissingPrice(token_address) => Kind::MissingPrice { + token_address: token_address.into(), + }, + } + } +} + #[serde_as] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -108,6 +118,11 @@ pub enum Kind { succeeded_once: bool, }, InvalidClearingPrices, + #[serde(rename_all = "camelCase")] + MissingPrice { + token_address: eth::H160, + }, + InvalidExecutedAmount, NonBufferableTokensUsed { tokens: BTreeSet, }, diff --git a/crates/shared/src/http_solver/model.rs b/crates/shared/src/http_solver/model.rs index f12149b7da..67c955bb5f 100644 --- a/crates/shared/src/http_solver/model.rs +++ b/crates/shared/src/http_solver/model.rs @@ -387,6 +387,12 @@ pub enum SolverRejectionReason { /// Not all trades have clearing prices InvalidClearingPrices, + /// Invalid executed amount + InvalidExecutedAmount, + + /// Missing price for the token address + MissingPrice(H160), + /// Solver balance too low to cover the execution costs. SolverAccountInsufficientBalance(U256), diff --git a/crates/solvers-dto/src/notification.rs b/crates/solvers-dto/src/notification.rs index 860d51e78a..a7b1d1da65 100644 --- a/crates/solvers-dto/src/notification.rs +++ b/crates/solvers-dto/src/notification.rs @@ -40,6 +40,11 @@ pub enum Kind { succeeded_once: bool, }, InvalidClearingPrices, + #[serde(rename_all = "camelCase")] + MissingPrice { + token_address: H160, + }, + InvalidExecutedAmount, NonBufferableTokensUsed { tokens: BTreeSet, }, diff --git a/crates/solvers/openapi.yml b/crates/solvers/openapi.yml index 03ca46b795..25d988e21b 100644 --- a/crates/solvers/openapi.yml +++ b/crates/solvers/openapi.yml @@ -75,6 +75,8 @@ paths: duplicatedSolutionId, simulationFailed, invalidClearingPrices, + missingPrice, + invalidExecutedAmount, nonBufferableTokensUsed, solverAccountInsufficientBalance, success, @@ -226,13 +228,13 @@ components: description: | The trading side of the order. type: string - enum: [sell, buy] + enum: [ sell, buy ] OrderClass: description: | How the CoW Protocol order was classified. type: string - enum: [market, limit, liquidity] + enum: [ market, limit, liquidity ] AppData: description: | @@ -245,19 +247,19 @@ components: description: | Where should the sell token be drawn from? type: string - enum: [erc20, internal, external] + enum: [ erc20, internal, external ] BuyTokenBalance: description: | Where should the buy token be transferred to? type: string - enum: [erc20, internal] + enum: [ erc20, internal ] SigningScheme: description: | How was the order signed? type: string - enum: [eip712, ethSign, preSign, eip1271] + enum: [ eip712, ethSign, preSign, eip1271 ] Signature: description: | @@ -320,7 +322,7 @@ components: properties: kind: type: string - enum: ["surplus"] + enum: [ "surplus" ] maxVolumeFactor: description: Never charge more than that percentage of the order volume. type: number @@ -354,7 +356,7 @@ components: properties: kind: type: string - enum: ["volume"] + enum: [ "volume" ] factor: description: The fraction of the order's volume that the protocol will request from the solver after settling the order. type: number @@ -388,7 +390,7 @@ components: properties: kind: type: string - enum: [constantProduct] + enum: [ constantProduct ] tokens: description: | A mapping of token address to its reserve amounts. @@ -412,7 +414,7 @@ components: properties: kind: type: string - enum: [weightedProduct] + enum: [ weightedProduct ] tokens: description: | A mapping of token address to its reserve amounts with weights. @@ -432,7 +434,7 @@ components: $ref: "#/components/schemas/Decimal" version: type: string - enum: ["v0", "v3Plus"] + enum: [ "v0", "v3Plus" ] balancer_pool_id: $ref: "#/components/schemas/BalancerPoolId" @@ -449,7 +451,7 @@ components: properties: kind: type: string - enum: [stable] + enum: [ stable ] tokens: description: | A mapping of token address to token balance and scaling rate. @@ -486,7 +488,7 @@ components: properties: kind: type: string - enum: [concentratedLiquidity] + enum: [ concentratedLiquidity ] tokens: type: array items: @@ -522,7 +524,7 @@ components: properties: kind: type: string - enum: [limitOrder] + enum: [ limitOrder ] makerToken: $ref: "#/components/schemas/Token" takerToken: @@ -684,7 +686,7 @@ components: properties: kind: type: string - enum: [fulfillment] + enum: [ fulfillment ] order: description: | A reference by UID of the order to execute in a solution. The order @@ -713,7 +715,7 @@ components: properties: kind: type: string - enum: [jit] + enum: [ jit ] executedAmount: description: | The amount of the order that was executed. This is denoted in @@ -747,7 +749,7 @@ components: properties: kind: type: string - enum: [liquidity] + enum: [ liquidity ] id: description: | The ID of executed liquidity provided in the auction input. @@ -793,7 +795,7 @@ components: properties: kind: type: string - enum: [custom] + enum: [ custom ] target: $ref: "#/components/schemas/Address" value: diff --git a/crates/solvers/src/api/routes/notify/dto/mod.rs b/crates/solvers/src/api/routes/notify/dto/mod.rs index bd873b460b..c7edbca38a 100644 --- a/crates/solvers/src/api/routes/notify/dto/mod.rs +++ b/crates/solvers/src/api/routes/notify/dto/mod.rs @@ -36,6 +36,14 @@ pub fn to_domain( solvers_dto::notification::Kind::InvalidClearingPrices => { notification::Kind::ScoringFailed(notification::ScoreKind::InvalidClearingPrices) } + solvers_dto::notification::Kind::MissingPrice { token_address } => { + notification::Kind::ScoringFailed(notification::ScoreKind::MissingPrice( + (*token_address).into(), + )) + } + solvers_dto::notification::Kind::InvalidExecutedAmount => { + notification::Kind::ScoringFailed(notification::ScoreKind::InvalidExecutedAmount) + } solvers_dto::notification::Kind::NonBufferableTokensUsed { tokens } => { notification::Kind::NonBufferableTokensUsed( tokens diff --git a/crates/solvers/src/boundary/legacy.rs b/crates/solvers/src/boundary/legacy.rs index ebf2a859bc..d6faf08037 100644 --- a/crates/solvers/src/boundary/legacy.rs +++ b/crates/solvers/src/boundary/legacy.rs @@ -605,6 +605,12 @@ fn to_boundary_auction_result(notification: ¬ification::Notification) -> (i64 Kind::ScoringFailed(ScoreKind::InvalidClearingPrices) => { AuctionResult::Rejected(SolverRejectionReason::InvalidClearingPrices) } + Kind::ScoringFailed(ScoreKind::InvalidExecutedAmount) => { + AuctionResult::Rejected(SolverRejectionReason::InvalidExecutedAmount) + } + Kind::ScoringFailed(ScoreKind::MissingPrice(token_address)) => { + AuctionResult::Rejected(SolverRejectionReason::MissingPrice(token_address.0)) + } Kind::NonBufferableTokensUsed(tokens) => { AuctionResult::Rejected(SolverRejectionReason::NonBufferableTokensUsed( tokens.iter().map(|token| token.0).collect(), diff --git a/crates/solvers/src/domain/notification.rs b/crates/solvers/src/domain/notification.rs index a04c55463c..83b255931d 100644 --- a/crates/solvers/src/domain/notification.rs +++ b/crates/solvers/src/domain/notification.rs @@ -56,4 +56,6 @@ pub enum Settlement { #[derive(Debug)] pub enum ScoreKind { InvalidClearingPrices, + InvalidExecutedAmount, + MissingPrice(TokenAddress), }