diff --git a/crates/autopilot/src/domain/competition/mod.rs b/crates/autopilot/src/domain/competition/mod.rs new file mode 100644 index 0000000000..b9b002a583 --- /dev/null +++ b/crates/autopilot/src/domain/competition/mod.rs @@ -0,0 +1,66 @@ +use { + super::{auction, eth}, + crate::domain, + number::nonzero::U256 as NonZeroU256, + std::collections::HashMap, +}; + +type SolutionId = u64; + +pub struct Solution { + id: SolutionId, + account: eth::Address, + score: NonZeroU256, + orders: HashMap, + // uniform prices for all tokens + prices: HashMap, +} + +impl Solution { + pub fn new( + id: SolutionId, + account: eth::Address, + score: NonZeroU256, + orders: HashMap, + prices: HashMap, + ) -> Self { + Self { + id, + account, + score, + orders, + prices, + } + } + + pub fn id(&self) -> SolutionId { + self.id + } + + pub fn account(&self) -> eth::Address { + self.account + } + + pub fn score(&self) -> NonZeroU256 { + self.score + } + + pub fn order_ids(&self) -> impl Iterator { + self.orders.keys() + } + + pub fn orders(&self) -> &HashMap { + &self.orders + } + + pub fn prices(&self) -> &HashMap { + &self.prices + } +} + +pub struct TradedAmounts { + /// The effective amount that left the user's wallet including all fees. + pub sell: eth::TokenAmount, + /// The effective amount the user received after all fees. + pub buy: eth::TokenAmount, +} diff --git a/crates/autopilot/src/domain/eth/mod.rs b/crates/autopilot/src/domain/eth/mod.rs index 47f3fa19a6..4805f4f49f 100644 --- a/crates/autopilot/src/domain/eth/mod.rs +++ b/crates/autopilot/src/domain/eth/mod.rs @@ -1,6 +1,10 @@ use derive_more::{From, Into}; pub use primitive_types::{H160, U256}; +/// An address. Can be an EOA or a smart contract address. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, From, Into)] +pub struct Address(pub H160); + /// An ERC20 token address. /// /// https://eips.ethereum.org/EIPS/eip-20 diff --git a/crates/autopilot/src/domain/mod.rs b/crates/autopilot/src/domain/mod.rs index 32147c619e..d1fff9e10a 100644 --- a/crates/autopilot/src/domain/mod.rs +++ b/crates/autopilot/src/domain/mod.rs @@ -1,4 +1,5 @@ pub mod auction; +pub mod competition; pub mod eth; pub mod fee; pub mod quote; diff --git a/crates/autopilot/src/run_loop.rs b/crates/autopilot/src/run_loop.rs index 048fe15935..7caf6011e1 100644 --- a/crates/autopilot/src/run_loop.rs +++ b/crates/autopilot/src/run_loop.rs @@ -1,7 +1,12 @@ use { crate::{ database::competition::Competition, - domain::{self, auction::order::Class, OrderUid}, + domain::{ + self, + auction::{self, order::Class, InvalidPrice}, + competition::{self}, + OrderUid, + }, infra::{ self, solvers::dto::{reveal, settle, solve}, @@ -20,7 +25,7 @@ use { SolverSettlement, }, number::nonzero::U256 as NonZeroU256, - primitive_types::{H160, H256, U256}, + primitive_types::{H160, H256}, rand::seq::SliceRandom, shared::token_list::AutoUpdatingTokenList, std::{ @@ -121,16 +126,16 @@ impl RunLoop { // Shuffle so that sorting randomly splits ties. solutions.shuffle(&mut rand::thread_rng()); - solutions.sort_unstable_by_key(|participant| participant.solution.score); + solutions.sort_unstable_by_key(|participant| participant.solution.score()); solutions }; let competition_simulation_block = self.eth.current_block().borrow().number; // TODO: Keep going with other solutions until some deadline. if let Some(Participant { driver, solution }) = solutions.last() { - tracing::info!(driver = %driver.name, solution = %solution.id, "winner"); + tracing::info!(driver = %driver.name, solution = %solution.id(), "winner"); - let revealed = match self.reveal(driver, auction_id, solution.id).await { + let revealed = match self.reveal(driver, auction_id, solution.id()).await { Ok(result) => { Metrics::reveal_ok(driver); result @@ -146,16 +151,16 @@ impl RunLoop { self.persistence .store_order_events(order_uids, OrderEventLabel::Considered); - let winner = solution.account; - let winning_score = solution.score.get(); + let winner = solution.account().into(); + let winning_score = solution.score().get(); let reference_score = solutions .iter() .nth_back(1) - .map(|participant| participant.solution.score.get()) + .map(|participant| participant.solution.score().get()) .unwrap_or_default(); let participants = solutions .iter() - .map(|participant| participant.solution.account) + .map(|participant| participant.solution.account().into()) .collect::>(); let mut prices = BTreeMap::new(); @@ -215,8 +220,8 @@ impl RunLoop { let is_winner = solutions.len() - index == 1; let mut settlement = SolverSettlement { solver: participant.driver.name.clone(), - solver_address: participant.solution.account, - score: Some(Score::Solver(participant.solution.score.get())), + solver_address: participant.solution.account().0, + score: Some(Score::Solver(participant.solution.score().get())), ranking: solutions.len() - index, orders: participant .solution @@ -224,15 +229,15 @@ impl RunLoop { .iter() .map(|(id, order)| Order::Colocated { id: (*id).into(), - sell_amount: order.sell_amount, - buy_amount: order.buy_amount, + sell_amount: order.sell.into(), + buy_amount: order.buy.into(), }) .collect(), clearing_prices: participant .solution - .clearing_prices + .prices() .iter() - .map(|(token, price)| (*token, *price)) + .map(|(token, price)| (token.0, price.get().into())) .collect(), call_data: None, uninternalized_call_data: None, @@ -285,10 +290,11 @@ impl RunLoop { tracing::warn!(?err, driver = %driver.name, "settlement failed"); } } + let solution_uids = solution.order_ids().copied().collect::>(); let unsettled_orders: HashSet<_> = solutions .iter() - .flat_map(|p| p.solution.orders.keys()) - .filter(|uid| !solution.orders.contains_key(uid)) + .flat_map(|p| p.solution.order_ids()) + .filter(|uid| !solution_uids.contains(uid)) .collect(); Metrics::matched_unsettled(driver, unsettled_orders); } @@ -355,7 +361,7 @@ impl RunLoop { &self, driver: &infra::Driver, request: &solve::Request, - ) -> Result>, SolveError> { + ) -> Result>, SolveError> { let response = tokio::time::timeout(self.solve_deadline, driver.solve(request)) .await .map_err(|_| SolveError::Timeout)? @@ -368,17 +374,31 @@ impl RunLoop { .solutions .into_iter() .map(|solution| { - Ok(Solution { - id: solution.solution_id, - account: solution.submission_address, - score: NonZeroU256::new(solution.score).ok_or(ZeroScoreError)?, - orders: solution - .orders - .into_iter() - .map(|(o, amounts)| (o.into(), amounts)) - .collect(), - clearing_prices: solution.clearing_prices, - }) + let mut prices = HashMap::new(); + for (token, price) in solution.clearing_prices.into_iter() { + prices.insert(token.into(), auction::Price::new(price.into())?); + } + let orders = solution + .orders + .into_iter() + .map(|(o, amounts)| { + ( + o.into(), + competition::TradedAmounts { + sell: amounts.sell_amount.into(), + buy: amounts.buy_amount.into(), + }, + ) + }) + .collect(); + + Ok(competition::Solution::new( + solution.solution_id, + solution.submission_address.into(), + NonZeroU256::new(solution.score).ok_or(ZeroScoreError)?, + orders, + prices, + )) }) .collect()) } @@ -410,7 +430,7 @@ impl RunLoop { async fn settle( &self, driver: &infra::Driver, - solved: &Solution, + solved: &competition::Solution, auction_id: i64, ) -> Result<(), SettleError> { let order_ids = solved.order_ids().copied().collect(); @@ -418,14 +438,14 @@ impl RunLoop { .store_order_events(order_ids, OrderEventLabel::Executing); let request = settle::Request { - solution_id: solved.id, + solution_id: solved.id(), }; let tx_hash = self .wait_for_settlement(driver, auction_id, request) .await?; *self.in_flight_orders.lock().await = Some(InFlightOrders { tx_hash, - orders: solved.orders.keys().copied().collect(), + orders: solved.order_ids().copied().collect(), }); tracing::debug!(?tx_hash, "solution settled"); @@ -533,25 +553,7 @@ pub struct InFlightOrders { struct Participant<'a> { driver: &'a infra::Driver, - solution: Solution, -} - -struct Solution { - id: u64, - account: H160, - score: NonZeroU256, - orders: HashMap, - clearing_prices: HashMap, -} - -impl Solution { - pub fn order_ids(&self) -> impl Iterator { - self.orders.keys() - } - - pub fn orders(&self) -> &HashMap { - &self.orders - } + solution: competition::Solution, } #[derive(Debug, thiserror::Error)] @@ -564,6 +566,14 @@ enum SolveError { Failure(anyhow::Error), } +#[derive(Debug, thiserror::Error)] +enum SolutionError { + #[error(transparent)] + ZeroScore(#[from] ZeroScoreError), + #[error(transparent)] + InvalidPrice(#[from] InvalidPrice), +} + #[derive(Debug, thiserror::Error)] #[error("the solver proposed a 0-score solution")] struct ZeroScoreError; @@ -654,10 +664,14 @@ impl Metrics { .inc(); } - fn solution_err(driver: &infra::Driver, _: &ZeroScoreError) { + fn solution_err(driver: &infra::Driver, err: &SolutionError) { + let label = match err { + SolutionError::ZeroScore(_) => "zero_score", + SolutionError::InvalidPrice(_) => "invalid_price", + }; Self::get() .solutions - .with_label_values(&[&driver.name, "zero_score"]) + .with_label_values(&[&driver.name, label]) .inc(); }