Skip to content

Commit

Permalink
driver: sort orders by likelihood of fulfillment (#1678)
Browse files Browse the repository at this point in the history
Progress on #1672.

Sort orders in the driver by likelihood of fulfillment before passing
them to the solver.

### Test Plan

Added an automated test.
  • Loading branch information
cowfee authored Jul 18, 2023
1 parent 731d4aa commit b2e653c
Show file tree
Hide file tree
Showing 24 changed files with 564 additions and 260 deletions.
2 changes: 1 addition & 1 deletion crates/autopilot/src/driver_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub mod solve {
#[serde(rename_all = "camelCase")]
pub struct Token {
pub address: H160,
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde_as(as = "Option<DecimalU256>")]
pub price: Option<U256>,
pub trusted: bool,
pub decimals: Option<u8>,
Expand Down
34 changes: 17 additions & 17 deletions crates/driver/src/boundary/settlement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ impl Settlement {

let mut settlement = solver::settlement::Settlement::new(
solution
.prices()?
.clearing_prices()?
.into_iter()
.map(|asset| (asset.token.into(), asset.amount))
.map(|asset| (asset.token.into(), asset.amount.into()))
.collect(),
);

for trade in &solution.trades {
for trade in solution.trades() {
let (boundary_order, execution) = match trade {
competition::solution::Trade::Fulfillment(trade) => {
// TODO: The `http_solver` module filters out orders with 0
Expand Down Expand Up @@ -130,13 +130,13 @@ impl Settlement {
}

let slippage_calculator = SlippageCalculator {
relative: to_big_decimal(solution.solver.slippage().relative.clone()),
absolute: solution.solver.slippage().absolute.map(Into::into),
relative: to_big_decimal(solution.solver().slippage().relative.clone()),
absolute: solution.solver().slippage().absolute.map(Into::into),
};
let external_prices = ExternalPrices::try_from_auction_prices(
native_token.address(),
auction
.tokens
.tokens()
.iter()
.filter_map(|token| {
token
Expand All @@ -147,7 +147,7 @@ impl Settlement {
)?;
let slippage_context = slippage_calculator.context(&external_prices);

for interaction in &solution.interactions {
for interaction in solution.interactions() {
let boundary_interaction = to_boundary_interaction(
&slippage_context,
settlement_contract.address().into(),
Expand All @@ -161,8 +161,8 @@ impl Settlement {

Ok(Self {
inner: settlement,
solver: solution.solver.address(),
risk: solution.risk,
solver: solution.solver().address(),
risk: solution.risk(),
})
}

Expand Down Expand Up @@ -204,7 +204,7 @@ impl Settlement {
let prices = ExternalPrices::try_from_auction_prices(
eth.contracts().weth().address(),
auction
.tokens
.tokens()
.iter()
.filter_map(|token| {
token
Expand All @@ -213,7 +213,7 @@ impl Settlement {
})
.collect(),
)?;
let gas_price = u256_to_big_rational(&auction.gas_price.effective().into());
let gas_price = u256_to_big_rational(&auction.gas_price().effective().into());
let inputs = solver::objective_value::Inputs::from_settlement(
&self.inner,
&prices,
Expand All @@ -239,8 +239,8 @@ fn to_boundary_order(order: &competition::Order) -> Order {
data: OrderData {
sell_token: order.sell.token.into(),
buy_token: order.buy.token.into(),
sell_amount: order.sell.amount,
buy_amount: order.buy.amount,
sell_amount: order.sell.amount.into(),
buy_amount: order.buy.amount.into(),
fee_amount: order.fee.user.into(),
receiver: order.receiver.map(Into::into),
valid_to: order.valid_to.into(),
Expand Down Expand Up @@ -319,8 +319,8 @@ fn to_boundary_jit_order(domain: &DomainSeparator, order: &order::Jit) -> Order
sell_token: order.sell.token.into(),
buy_token: order.buy.token.into(),
receiver: Some(order.receiver.into()),
sell_amount: order.sell.amount,
buy_amount: order.buy.amount,
sell_amount: order.sell.amount.into(),
buy_amount: order.buy.amount.into(),
valid_to: order.valid_to.into(),
app_data: AppDataHash(order.app_data.into()),
fee_amount: order.fee.into(),
Expand Down Expand Up @@ -407,11 +407,11 @@ pub fn to_boundary_interaction(

let input = liquidity::MaxInput(eth::Asset {
token: boundary_execution.input_max.token.into(),
amount: boundary_execution.input_max.amount,
amount: boundary_execution.input_max.amount.into(),
});
let output = liquidity::ExactOutput(eth::Asset {
token: boundary_execution.output.token.into(),
amount: boundary_execution.output.amount,
amount: boundary_execution.output.amount.into(),
});

let interaction = match &liquidity.liquidity.kind {
Expand Down
139 changes: 125 additions & 14 deletions crates/driver/src/domain/competition/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use {
competition::{self, solution},
eth,
},
std::collections::HashMap,
thiserror::Error,
};

Expand All @@ -11,25 +12,112 @@ use {
/// solving them.
#[derive(Debug)]
pub struct Auction {
/// [`None`] if this auction applies to a quote.
pub id: Option<Id>,
pub tokens: Vec<Token>,
pub orders: Vec<competition::Order>,
pub gas_price: eth::GasPrice,
pub deadline: Deadline,
/// See the [`Self::id`] method.
id: Option<Id>,
/// See the [`Self::orders`] method.
orders: Vec<competition::Order>,
/// The tokens that are used in the orders of this auction.
tokens: Tokens,
gas_price: eth::GasPrice,
deadline: Deadline,
}

impl Auction {
pub fn is_trusted(&self, token: eth::TokenAddress) -> bool {
self.tokens
.iter()
.find(|t| t.address == token)
.map(|token| token.trusted)
.unwrap_or(false)
pub fn new(
id: Option<Id>,
mut orders: Vec<competition::Order>,
tokens: impl Iterator<Item = Token>,
gas_price: eth::GasPrice,
deadline: Deadline,
weth: eth::WethAddress,
) -> Result<Self, InvalidTokens> {
let tokens = Tokens(tokens.map(|token| (token.address, token)).collect());

// Ensure that tokens are included for each order.
if !orders.iter().all(|order| {
tokens.0.contains_key(&order.buy.token.wrap(weth))
&& tokens.0.contains_key(&order.sell.token.wrap(weth))
}) {
return Err(InvalidTokens);
}

// Sort orders such that most likely to be fulfilled come first.
orders.sort_by_key(|order| {
// Market orders are preferred over limit orders, as the expectation is that
// they should be immediately fulfillable. Liquidity orders come last, as they
// are the most niche and rarely used.
let class = match order.kind {
competition::order::Kind::Market => 2,
competition::order::Kind::Limit { .. } => 1,
competition::order::Kind::Liquidity => 0,
};
std::cmp::Reverse((
class,
// If the orders are of the same kind, then sort by likelihood of fulfillment
// based on token prices.
order.likelihood(&tokens),
))
});

// TODO Filter out orders based on user balance

Ok(Self {
id,
orders,
tokens,
gas_price,
deadline,
})
}

/// [`None`] if this auction applies to a quote. See
/// [`crate::domain::quote`].
pub fn id(&self) -> Option<Id> {
self.id
}

/// The orders for the auction. The orders are sorted such that those which
/// are more likely to be fulfilled come before less likely orders.
pub fn orders(&self) -> &[competition::Order] {
&self.orders
}

/// The tokens used in the auction.
pub fn tokens(&self) -> &Tokens {
&self.tokens
}

pub fn gas_price(&self) -> eth::GasPrice {
self.gas_price
}

pub fn deadline(&self) -> Deadline {
self.deadline
}
}

#[derive(Debug)]
/// The tokens that are used in an auction.
#[derive(Debug, Default)]
pub struct Tokens(HashMap<eth::TokenAddress, Token>);

impl Tokens {
pub fn get(&self, address: eth::TokenAddress) -> Token {
self.0.get(&address).cloned().unwrap_or(Token {
decimals: None,
symbol: None,
address,
price: None,
available_balance: Default::default(),
trusted: false,
})
}

pub fn iter(&self) -> impl Iterator<Item = &Token> {
self.0.values()
}
}

#[derive(Debug, Clone)]
pub struct Token {
pub decimals: Option<u8>,
pub symbol: Option<String>,
Expand All @@ -45,7 +133,22 @@ pub struct Token {
/// The price of a token in wei. This represents how much wei is needed to buy
/// 10**18 of another token.
#[derive(Debug, Clone, Copy)]
pub struct Price(pub eth::Ether);
pub struct Price(eth::Ether);

impl Price {
pub fn new(value: eth::Ether) -> Result<Self, InvalidPrice> {
if value.0.is_zero() {
Err(InvalidPrice)
} else {
Ok(Self(value))
}
}

/// Apply this price to some token amount, converting that token into ETH.
pub fn apply(self, amount: eth::TokenAmount) -> eth::Ether {
(amount.0 * self.0 .0).into()
}
}

impl From<Price> for eth::U256 {
fn from(value: Price) -> Self {
Expand Down Expand Up @@ -121,3 +224,11 @@ pub struct DeadlineExceeded;
#[derive(Debug, Error)]
#[error("invalid auction id")]
pub struct InvalidId;

#[derive(Debug, Error)]
#[error("invalid auction tokens")]
pub struct InvalidTokens;

#[derive(Debug, Error)]
#[error("price cannot be zero")]
pub struct InvalidPrice;
10 changes: 5 additions & 5 deletions crates/driver/src/domain/competition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ impl Competition {
.liquidity
.fetch(
&auction
.orders
.orders()
.iter()
.filter_map(|order| match order.kind {
order::Kind::Market | order::Kind::Limit { .. } => {
Expand All @@ -66,13 +66,13 @@ impl Competition {
// Fetch the solutions from the solver.
let solutions = self
.solver
.solve(auction, &liquidity, auction.deadline.timeout()?)
.solve(auction, &liquidity, auction.deadline().timeout()?)
.await?;

// Empty solutions aren't useful, so discard them.
let solutions = solutions.into_iter().filter(|solution| {
if solution.is_empty() {
observe::empty_solution(self.solver.name(), solution.id);
observe::empty_solution(self.solver.name(), solution.id());
false
} else {
true
Expand All @@ -81,9 +81,9 @@ impl Competition {

// Encode the solutions into settlements.
let settlements = join_all(solutions.map(|solution| async move {
observe::encoding(self.solver.name(), solution.id);
observe::encoding(self.solver.name(), solution.id());
(
solution.id,
solution.id(),
solution.encode(auction, &self.eth, &self.simulator).await,
)
}))
Expand Down
36 changes: 34 additions & 2 deletions crates/driver/src/domain/competition/order/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use crate::{
domain::eth,
infra::{blockchain, Ethereum},
util::{self, Bytes},
util::{self, conv, Bytes},
};

pub mod signature;

pub use signature::Signature;
use {super::auction, bigdecimal::Zero, num::CheckedDiv};

/// An order in the auction.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -70,6 +71,18 @@ impl From<TargetAmount> for eth::U256 {
}
}

impl From<eth::TokenAmount> for TargetAmount {
fn from(value: eth::TokenAmount) -> Self {
Self(value.0)
}
}

impl From<TargetAmount> for eth::TokenAmount {
fn from(value: TargetAmount) -> Self {
Self(value.0)
}
}

/// Order fee denominated in the sell token.
#[derive(Debug, Default, Clone)]
pub struct Fee {
Expand Down Expand Up @@ -129,7 +142,7 @@ impl Order {
pub fn solver_sell(&self) -> eth::Asset {
if let Kind::Limit { surplus_fee } = self.kind {
eth::Asset {
amount: self.sell.amount - surplus_fee.0,
amount: (self.sell.amount.0 - surplus_fee.0).into(),
token: self.sell.token,
}
} else {
Expand Down Expand Up @@ -159,6 +172,25 @@ impl Order {
pub fn solver_determines_fee(&self) -> bool {
self.is_partial() && matches!(self.kind, Kind::Limit { .. })
}

/// The likelihood that this order will be fulfilled, based on token prices.
/// A larger value means that the order is more likely to be fulfilled.
/// This is used to prioritize orders when solving.
pub fn likelihood(&self, tokens: &auction::Tokens) -> num::BigRational {
match (
tokens.get(self.buy.token).price,
tokens.get(self.sell.token).price,
) {
(Some(buy_price), Some(sell_price)) => {
let buy = buy_price.apply(self.buy.amount);
let sell = sell_price.apply(self.sell.amount);
conv::u256::to_big_rational(buy.0)
.checked_div(&conv::u256::to_big_rational(sell.0))
.unwrap_or_else(num::BigRational::zero)
}
_ => num::BigRational::zero(),
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down
Loading

0 comments on commit b2e653c

Please sign in to comment.