Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rank by surplus driver V3 #2448

Merged
merged 32 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/driver/src/boundary/settlement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
49 changes: 48 additions & 1 deletion crates/driver/src/domain/competition/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ use {
time,
},
infra::{self, blockchain, observe, Ethereum},
util,
util::{self, conv::u256::U256Ext},
},
futures::future::{join_all, BoxFuture, FutureExt, Shared},
itertools::Itertools,
num::BigRational,
std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
Expand Down Expand Up @@ -112,6 +113,32 @@ impl Auction {
pub fn score_cap(&self) -> Score {
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
.0
.iter()
.filter_map(|(address, token)| {
token.price.map(|price| {
(
*address,
auction::NormalizedPrice(price.0 .0.to_big_rational() / &*UNIT),
)
})
})
.collect::<HashMap<_, _>>();

// Add the buy eth address
prices.insert(
eth::ETH_TOKEN,
auction::NormalizedPrice(BigRational::from_integer(1.into())),
);
prices
}
}

#[derive(Clone)]
Expand Down Expand Up @@ -418,6 +445,26 @@ impl From<eth::U256> 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<BigRational> 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<eth::TokenAddress, NormalizedPrice>;

#[derive(Debug, Clone, Copy)]
pub struct Id(pub i64);

Expand Down
1 change: 1 addition & 0 deletions crates/driver/src/domain/competition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use {
pub mod auction;
pub mod order;
pub mod score;
pub mod settled;
pub mod solution;

pub use {
Expand Down
275 changes: 275 additions & 0 deletions crates/driver/src/domain/competition/settled/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
use {
super::{
auction,
order::{self, Side},
},
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.
#[derive(Debug, Clone)]
pub struct Settlement {
trades: Vec<Trade>,
}

impl Settlement {
pub fn new(trades: Vec<Trade>) -> Self {
Self { trades }
}

/// 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 valid only if all trade scores are valid.
///
/// Denominated in NATIVE token
pub fn score(&self, prices: &auction::NormalizedPrices) -> Result<eth::TokenAmount, Error> {
self.trades
.iter()
.map(|trade| trade.score(prices))
.try_fold(eth::TokenAmount(eth::U256::zero()), |acc, score| {
score.map(|score| acc + score)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential overflow of U256

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think overflow here is not realistic. Also I think we don't want to cover all theoretically possible places with checked functions, but rather the ones that can realistically happen.

})
}
}

#[derive(Debug, Clone)]
pub struct Trade {
sell: eth::Asset,
buy: eth::Asset,
side: Side,
executed: order::TargetAmount,
custom_price: CustomClearingPrices,
policies: Vec<order::FeePolicy>,
}

impl Trade {
pub fn new(
sell: eth::Asset,
buy: eth::Asset,
side: Side,
executed: order::TargetAmount,
custom_price: CustomClearingPrices,
policies: Vec<order::FeePolicy>,
) -> Self {
Self {
sell,
buy,
side,
executed,
custom_price,
policies,
}
}

/// CIP38 score defined as surplus + protocol fee
///
/// Denominated in NATIVE token
pub fn score(&self, prices: &auction::NormalizedPrices) -> Result<eth::TokenAmount, Error> {
Ok(self.native_surplus(prices)? + self.native_protocol_fee(prices)?)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential sum overflow

}

/// Surplus based on custom clearing prices returns the surplus after all
/// fees have been applied.
///
/// Denominated in SURPLUS token
fn surplus(&self) -> Option<eth::Asset> {
match self.side {
Side::Buy => {
// scale limit sell to support partially fillable orders
let limit_sell = self
.sell
.amount
.0
.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
.0
.checked_mul(self.custom_price.buy)?
.checked_div(self.custom_price.sell)?,
)
}
Side::Sell => {
// scale limit buy to support partially fillable orders
let limit_buy = self
.executed
.0
.checked_mul(self.buy.amount.into())?
.checked_div(self.sell.amount.into())?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: For consistency I would have expected a scaled limit buy amount.

Suggested change
let limit_buy = self
.executed
.0
.checked_mul(self.buy.amount.into())?
.checked_div(self.sell.amount.into())?;
let limit_buy = self
.buy
.amount
.0
.checked_mul(self.executed.into())?
.checked_div(self.sell.amount.into())?;

// difference between executed amount converted to buy token and limit buy
self.executed
.0
.checked_mul(self.custom_price.sell)?
.checked_div(self.custom_price.buy)?
.checked_sub(limit_buy)
}
}
.map(|surplus| eth::Asset {
token: self.surplus_token(),
amount: surplus.into(),
})
}

/// Surplus based on custom clearing prices returns the surplus after all
/// fees have been applied.
///
/// Denominated in NATIVE token
fn native_surplus(
&self,
prices: &auction::NormalizedPrices,
) -> Result<eth::TokenAmount, Error> {
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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential multiplication overflow

.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) -> Result<eth::Asset, Error> {
// TODO: support multiple fee policies
if self.policies.len() > 1 {
return Err(Error::MultipleFeePolicies);
}

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)
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
let executed_in_surplus_token = match self.side {
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),
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))?
},
)),
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.
///
/// Denominated in NATIVE token
fn native_protocol_fee(
&self,
prices: &auction::NormalizedPrices,
) -> Result<eth::TokenAmount, Error> {
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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchecked multiplication

.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: &auction::NormalizedPrices,
) -> Result<auction::NormalizedPrice, Error> {
prices
.get(&self.surplus_token())
.cloned()
.ok_or(Error::MissingPrice(self.surplus_token()))
}
}

fn apply_factor(amount: eth::U256, factor: f64) -> Option<eth::U256> {
Some(
amount.checked_mul(eth::U256::from_f64_lossy(factor * 1000000000000000000.))?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could multiply using BigRational numbers. That way you should have accurate results without having to multiply the factor with a huge number.
Since I see you defining that function twice it might be a good idea to have that as a general helper function in our number crate.

/ 1000000000000000000u128,
)
}

/// 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 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),
#[error("overflow error while calculating protocol fee")]
Overflow,
#[error("division by zero error while calculating protocol fee")]
DivisionByZero,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid blindly adding a new error type for every potential call that could fail and instead ask us if we can group them in some sensible way. What is the advantage of having these strongly typed subtypes? I believe it's to then handle different types of errors differently (how are we realistically going to handle a TypeConversion differently from a DivisionByZero?)

Looking at Factor for instance, the only way this can happen is if the multiplication overflows (so really it's the same as Error::Overflow). I wonder if even the last three should be combined to something like Math(anyhow::Error).

I think by and large there are three types of error here:

  1. Math error (overflow, underflow, div by 0, etc)
  2. Malformed auctions (ie. missing price, type conversions)
  3. Limitations of the current implementation (multiple fee policies, unsupported policies)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Factor also happens when the factor = 1.0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did some cleanup, not sure if good enough

}
Loading
Loading