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

Driver: Include non-surplus-capturing-jit-orders in the driver /solve #3048

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ components:
enum: ["erc20", "internal"]
class:
type: string
enum: ["market", "limit", "liquidity"]
enum: ["market", "limit"]
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
appData:
description: 32 bytes encoded as hex with `0x` prefix.
type: string
Expand Down
67 changes: 44 additions & 23 deletions crates/driver/src/domain/competition/solution/settlement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ use {
super::{encoding, trade::ClearingPrices, Error, Solution},
crate::{
domain::{
competition::{self, auction, order, solution},
competition::{
self,
auction,
order::{self},
solution::{self, error, Trade},
},
eth,
},
infra::{blockchain::Ethereum, observe, solver::ManageNativeToken, Simulator},
Expand Down Expand Up @@ -254,30 +259,46 @@ impl Settlement {

/// The settled user orders with their in/out amounts.
pub fn orders(&self) -> HashMap<order::Uid, competition::Amounts> {
let log_err = |trade: &Trade, err: error::Math, kind: &str| -> eth::TokenAmount {
// This should never happen, returning 0 is better than panicking, but we
// should still alert.
let msg = format!("could not compute {kind}");
tracing::error!(?trade, prices=?self.solution.prices, ?err, msg);
0.into()
};
let mut acc: HashMap<order::Uid, competition::Amounts> = HashMap::new();
for trade in self.solution.user_trades() {
let prices = ClearingPrices {
sell: self.solution.prices[&trade.order().sell.token.wrap(self.solution.weth)],
buy: self.solution.prices[&trade.order().buy.token.wrap(self.solution.weth)],
};
let order = competition::Amounts {
side: trade.order().side,
sell: trade.order().sell,
buy: trade.order().buy,
executed_sell: trade.sell_amount(&prices).unwrap_or_else(|err| {
// This should never happen, returning 0 is better than panicking, but we
// should still alert.
tracing::error!(?trade, prices=?self.solution.prices, ?err, "could not compute sell_amount");
0.into()
}),
executed_buy: trade.buy_amount(&prices).unwrap_or_else(|err| {
// This should never happen, returning 0 is better than panicking, but we
// should still alert.
tracing::error!(?trade, prices=?self.solution.prices, ?err, "could not compute buy_amount");
0.into()
}),
for trade in &self.solution.trades {
let order = match trade {
Trade::Fulfillment(_) => {
let prices = ClearingPrices {
sell: self.solution.prices[&trade.sell().token.wrap(self.solution.weth)],
buy: self.solution.prices[&trade.buy().token.wrap(self.solution.weth)],
sunce86 marked this conversation as resolved.
Show resolved Hide resolved
};
competition::Amounts {
side: trade.side(),
sell: trade.sell(),
buy: trade.buy(),
executed_sell: trade
.sell_amount(&prices)
.unwrap_or_else(|err| log_err(trade, err, "executed_sell")),
executed_buy: trade
.buy_amount(&prices)
.unwrap_or_else(|err| log_err(trade, err, "executed_buy")),
}
}
Trade::Jit(jit) => competition::Amounts {
side: trade.side(),
sell: trade.sell(),
buy: trade.buy(),
executed_sell: jit
.executed_sell()
.unwrap_or_else(|err| log_err(trade, err, "executed_sell")),
executed_buy: jit
.executed_buy()
.unwrap_or_else(|err| log_err(trade, err, "executed_buy")),
},
m-lord-renkse marked this conversation as resolved.
Show resolved Hide resolved
};
acc.insert(trade.order().uid, order);
acc.insert(trade.uid(), order);
}
acc
}
Expand Down
54 changes: 51 additions & 3 deletions crates/driver/src/domain/competition/solution/trade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
domain::{
competition::{
self,
order::{self, FeePolicy, SellAmount, Side, TargetAmount},
order::{self, FeePolicy, SellAmount, Side, TargetAmount, Uid},
solution::error::{self, Math},
},
eth::{self, Asset},
Expand All @@ -19,6 +19,13 @@ pub enum Trade {
}

impl Trade {
pub fn uid(&self) -> Uid {
match self {
Trade::Fulfillment(fulfillment) => fulfillment.order().uid,
Trade::Jit(jit) => jit.order().uid,
}
}

pub fn side(&self) -> Side {
match self {
Trade::Fulfillment(fulfillment) => fulfillment.order().side,
Expand Down Expand Up @@ -62,7 +69,10 @@ impl Trade {
}

/// The effective amount that left the user's wallet including all fees.
fn sell_amount(&self, prices: &ClearingPrices) -> Result<eth::TokenAmount, error::Math> {
pub(crate) fn sell_amount(
&self,
prices: &ClearingPrices,
) -> Result<eth::TokenAmount, error::Math> {
let before_fee = match self.side() {
order::Side::Sell => self.executed().0,
order::Side::Buy => self
Expand All @@ -81,7 +91,10 @@ impl Trade {
/// The effective amount the user received after all fees.
///
/// Settlement contract uses `ceil` division for buy amount calculation.
fn buy_amount(&self, prices: &ClearingPrices) -> Result<eth::TokenAmount, error::Math> {
pub(crate) fn buy_amount(
&self,
prices: &ClearingPrices,
) -> Result<eth::TokenAmount, error::Math> {
let amount = match self.side() {
order::Side::Buy => self.executed().0,
order::Side::Sell => self
Expand Down Expand Up @@ -409,6 +422,41 @@ impl Jit {
self.executed
}

pub fn executed_buy(&self) -> Result<eth::TokenAmount, Math> {
Ok(match self.order().side {
Side::Buy => self.executed().into(),
Side::Sell => (self
.executed()
.0
.checked_add(self.fee().0)
.ok_or(Math::Overflow)?)
.checked_mul(self.order.buy.amount.0)
.ok_or(Math::Overflow)?
.checked_ceil_div(&self.order.sell.amount.0)
.ok_or(Math::DivisionByZero)?
.into(),
})
}

pub fn executed_sell(&self) -> Result<eth::TokenAmount, Math> {
Ok(match self.order().side {
Side::Buy => self
.executed()
.0
.checked_mul(self.order.sell.amount.0)
.ok_or(Math::Overflow)?
.checked_div(self.order.buy.amount.0)
.ok_or(Math::DivisionByZero)?
.into(),
Side::Sell => self
.executed()
.0
.checked_add(self.fee().0)
.ok_or(Math::Overflow)?
.into(),
})
}

pub fn fee(&self) -> order::SellAmount {
self.fee
}
Expand Down
20 changes: 18 additions & 2 deletions crates/driver/src/tests/cases/jit_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
ab_order,
ab_solution,
test_solver,
ExpectedOrderAmounts,
Test,
},
},
Expand Down Expand Up @@ -54,14 +55,26 @@ struct TestCase {

#[cfg(test)]
async fn protocol_fee_test_case(test_case: TestCase) {
let test_name = format!("JIT Order: {:?}", test_case.order.side);
let test_name = format!("JIT Order: {:?}", test_case.solution.jit_order.order.side);
// Adjust liquidity pools so that the order is executable at the amounts
// expected from the solver.
let quote = ab_liquidity_quote()
.sell_amount(test_case.execution.solver.sell)
.buy_amount(test_case.execution.solver.buy);
let pool = ab_adjusted_pool(quote);
let solver_fee = test_case.execution.driver.sell / 100;
// Amounts expected to be returned by the driver after fee processing
let jit_order_expected_amounts = if test_case.is_surplus_capturing_jit_order {
ExpectedOrderAmounts {
sell: test_case.execution.solver.sell,
buy: test_case.execution.solver.buy,
}
} else {
ExpectedOrderAmounts {
sell: test_case.solution.jit_order.order.sell_amount,
buy: test_case.solution.jit_order.order.buy_amount,
}
};

let jit_order = setup::JitOrder {
order: ab_order()
Expand All @@ -70,7 +83,8 @@ async fn protocol_fee_test_case(test_case: TestCase) {
.buy_amount(test_case.solution.jit_order.order.buy_amount)
.solver_fee(Some(solver_fee))
.side(test_case.solution.jit_order.order.side)
.no_surplus(),
.no_surplus()
.expected_amounts(jit_order_expected_amounts),
};

let order = ab_order()
Expand All @@ -83,6 +97,7 @@ async fn protocol_fee_test_case(test_case: TestCase) {
.no_surplus();

let solver = test_solver();

let test: Test = tests::setup()
.name(test_name)
.pool(pool)
Expand All @@ -104,6 +119,7 @@ async fn protocol_fee_test_case(test_case: TestCase) {
result.score(),
test_case.solution.expected_score,
));
result.jit_orders(&[jit_order]);
}

#[tokio::test]
Expand Down
39 changes: 39 additions & 0 deletions crates/driver/src/tests/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use {
DEFAULT_SURPLUS_FACTOR,
ETH_ORDER_AMOUNT,
},
hex_address,
setup::blockchain::{Blockchain, Trade},
},
},
Expand Down Expand Up @@ -1224,6 +1225,44 @@ impl<'a> SolveOk<'a> {
assert!(self.solutions().is_empty());
}

/// Check that the solution contains the expected JIT orders.
pub fn jit_orders(self, jit_orders: &[JitOrder]) -> Self {
let solution = self.solution();
assert!(solution.get("orders").is_some());
let trades = serde_json::from_value::<HashMap<String, serde_json::Value>>(
solution.get("orders").unwrap().clone(),
)
.unwrap();

// Since JIT orders don't have UID at creation time, we need to search for
// matching token pair
for expected in jit_orders.iter() {
let exist = trades
.values()
.any(|trade| self.trade_matches(trade, expected));
assert!(exist, "JIT order {expected:?} not found");
m-lord-renkse marked this conversation as resolved.
Show resolved Hide resolved
}
self
}

/// Find for a JIT order, given specific token pair and buy/sell amount,
/// return true if the JIT order was found
fn trade_matches(&self, trade: &serde_json::Value, expected: &JitOrder) -> bool {
let u256 =
|value: &serde_json::Value| eth::U256::from_dec_str(value.as_str().unwrap()).unwrap();
let sell_token = trade.get("sellToken").unwrap().to_string();
let sell_token = sell_token.trim_matches('"');
let buy_token = trade.get("buyToken").unwrap().to_string();
let buy_token = buy_token.trim_matches('"');
let sell_amount = u256(trade.get("executedSell").unwrap());
let buy_amount = u256(trade.get("executedBuy").unwrap());

sell_token == hex_address(self.blockchain.get_token(expected.order.sell_token))
&& buy_token == hex_address(self.blockchain.get_token(expected.order.buy_token))
&& expected.order.expected_amounts.clone().unwrap().sell == sell_amount
&& expected.order.expected_amounts.clone().unwrap().buy == buy_amount
}

/// Check that the solution contains the expected orders.
pub fn orders(self, orders: &[Order]) -> Self {
let solution = self.solution();
Expand Down
39 changes: 25 additions & 14 deletions crates/driver/src/tests/setup/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,20 +197,22 @@ impl Solver {
})
},
));
prices_json.insert(
let previous_value = prices_json.insert(
config
.blockchain
.get_token_wrapped(fulfillment.quoted_order.order.sell_token),
fulfillment.execution.buy.to_string(),
);
prices_json.insert(
assert_eq!(previous_value, None, "existing price overwritten");
let previous_value = prices_json.insert(
config
.blockchain
.get_token_wrapped(fulfillment.quoted_order.order.buy_token),
(fulfillment.execution.sell
- fulfillment.quoted_order.order.surplus_fee())
.to_string(),
);
assert_eq!(previous_value, None, "existing price overwritten");
{
// trades have optional field `fee`
let order = if config.quote {
Expand Down Expand Up @@ -270,18 +272,27 @@ impl Solver {
}).collect_vec(),
})
}));
prices_json.insert(
config
.blockchain
.get_token_wrapped(jit.quoted_order.order.sell_token),
jit.execution.buy.to_string(),
);
prices_json.insert(
config
.blockchain
.get_token_wrapped(jit.quoted_order.order.buy_token),
(jit.execution.sell - jit.quoted_order.order.surplus_fee()).to_string(),
);
// Skipping the prices for JIT orders (non-surplus-capturing)
if config
m-lord-renkse marked this conversation as resolved.
Show resolved Hide resolved
.expected_surplus_capturing_jit_order_owners
.contains(&jit.quoted_order.order.owner)
{
let previous_value = prices_json.insert(
config
.blockchain
.get_token_wrapped(jit.quoted_order.order.sell_token),
jit.execution.buy.to_string(),
);
assert_eq!(previous_value, None, "existing price overwritten");
let previous_value = prices_json.insert(
config
.blockchain
.get_token_wrapped(jit.quoted_order.order.buy_token),
(jit.execution.sell - jit.quoted_order.order.surplus_fee())
.to_string(),
);
assert_eq!(previous_value, None, "existing price overwritten");
}
{
let executed_amount = match jit.quoted_order.order.executed {
Some(executed) => executed.to_string(),
Expand Down
Loading