diff --git a/crates/driver/src/domain/competition/mod.rs b/crates/driver/src/domain/competition/mod.rs index 869d51d683..6355119ccb 100644 --- a/crates/driver/src/domain/competition/mod.rs +++ b/crates/driver/src/domain/competition/mod.rs @@ -239,6 +239,7 @@ impl Competition { .user_trades() .map(|trade| trade.order().uid) .collect(); + let has_haircut = solution.has_haircut(); observe::encoding(&id); let settlement = solution .encode( @@ -248,10 +249,10 @@ impl Competition { self.solver.solver_native_token(), ) .await; - (id, orders, settlement) + (id, orders, has_haircut, settlement) }) .collect::>() - .filter_map(|(id, orders, result)| async move { + .filter_map(|(id, orders, has_haircut, result)| async move { match result { Ok(solution) => { self.risk_detector.encoding_succeeded(&orders); @@ -261,8 +262,11 @@ impl Competition { Err(_err) if id.solutions().len() > 1 => None, Err(err) => { self.risk_detector.encoding_failed(&orders); - observe::encoding_failed(self.solver.name(), &id, &err); - notify::encoding_failed(&self.solver, auction.id(), &id, &err); + observe::encoding_failed(self.solver.name(), &id, &err, has_haircut); + // don't notify on errors for solutions with haircut + if !has_haircut { + notify::encoding_failed(&self.solver, auction.id(), &id, &err); + } None } } @@ -362,6 +366,7 @@ impl Competition { // gets picked by the procotol. if let Ok(remaining) = deadline.remaining() { let score_ref = &mut score; + let has_haircut = settlement.has_haircut(); let simulate_on_new_blocks = async move { let mut stream = ethrpc::block_stream::into_stream(self.eth.current_block().clone()); @@ -369,19 +374,22 @@ impl Competition { if let Err(infra::simulator::Error::Revert(err)) = self.simulate_settlement(&settlement).await { - observe::winner_voided(block, &err); + observe::winner_voided(self.solver.name(), block, &err, has_haircut); *score_ref = None; self.settlements .lock() .unwrap() .retain(|s| s.solution().get() != solution_id); - notify::simulation_failed( - &self.solver, - auction.id(), - settlement.solution(), - &infra::simulator::Error::Revert(err), - true, - ); + // Only notify solver if solution doesn't have haircut + if !has_haircut { + notify::simulation_failed( + &self.solver, + auction.id(), + settlement.solution(), + &infra::simulator::Error::Revert(err), + true, + ); + } return; } } diff --git a/crates/driver/src/domain/competition/solution/mod.rs b/crates/driver/src/domain/competition/solution/mod.rs index 46404cc955..0fedc4b27b 100644 --- a/crates/driver/src/domain/competition/solution/mod.rs +++ b/crates/driver/src/domain/competition/solution/mod.rs @@ -517,6 +517,16 @@ impl Solution { ) }) } + + /// Returns true if any trade in this solution has a non-zero haircut fee. + /// Used to determine if simulation failures should suppress solver + /// notifications. + pub fn has_haircut(&self) -> bool { + self.trades.iter().any(|trade| match trade { + Trade::Fulfillment(fulfillment) => !fulfillment.haircut_fee().is_zero(), + Trade::Jit(_) => false, // JIT orders don't have haircut + }) + } } /// Given two solutions returns the factors with diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 63bf90be3c..c7ad4a33d7 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -339,6 +339,11 @@ impl Settlement { .map(|(token, amount)| (token, amount.into())) .collect() } + + /// Returns true if this settlement's solution has any trades with haircut. + pub fn has_haircut(&self) -> bool { + self.solution.has_haircut() + } } /// Should the interactions be internalized? diff --git a/crates/driver/src/infra/observe/mod.rs b/crates/driver/src/infra/observe/mod.rs index 07e1b06a12..8ee025539e 100644 --- a/crates/driver/src/infra/observe/mod.rs +++ b/crates/driver/src/infra/observe/mod.rs @@ -122,11 +122,26 @@ pub fn encoding(id: &solution::Id) { } /// Observe that settlement encoding failed. -pub fn encoding_failed(solver: &solver::Name, id: &solution::Id, err: &solution::Error) { - tracing::info!(?id, ?err, "discarded solution: settlement encoding"); +pub fn encoding_failed( + solver: &solver::Name, + id: &solution::Id, + err: &solution::Error, + has_haircut: bool, +) { + tracing::info!( + ?id, + ?err, + has_haircut, + "discarded solution: settlement encoding" + ); + let reason = if has_haircut { + "SettlementEncodingHaircut" + } else { + "SettlementEncoding" + }; metrics::get() .dropped_solutions - .with_label_values(&[solver.as_str(), "SettlementEncoding"]) + .with_label_values(&[solver.as_str(), reason]) .inc(); } @@ -164,8 +179,27 @@ pub fn score(settlement: &Settlement, score: ð::Ether) { // Observe that the winning settlement started failing upon arrival of a new // block -pub fn winner_voided(block: BlockInfo, err: &simulator::RevertError) { - tracing::warn!(block = block.number, ?err, "solution reverts on new block"); +pub fn winner_voided( + solver: &solver::Name, + block: BlockInfo, + err: &simulator::RevertError, + has_haircut: bool, +) { + tracing::warn!( + block = block.number, + ?err, + has_haircut, + "solution reverts on new block" + ); + let reason = if has_haircut { + "SimulationRevertHaircut" + } else { + "SimulationRevert" + }; + metrics::get() + .dropped_solutions + .with_label_values(&[solver.as_str(), reason]) + .inc(); } pub fn revealing() {