Skip to content

Commit

Permalink
Driver sends quote response with JIT orders (#3103)
Browse files Browse the repository at this point in the history
# Description
Closes task n3 from #3082
by start sending the new `/quote` response with the updated fields that
include clearing prices instead of the executed amount, jit orders and
pre-interactions.

# Changes

- [ ] Updated driver's `/quote` response which is now supported by the
trade verifier.

## How to test
New driver's and services' e2e tests.
  • Loading branch information
squadgazzz authored Dec 2, 2024
1 parent eb98aaf commit 1413398
Show file tree
Hide file tree
Showing 23 changed files with 723 additions and 88 deletions.
1 change: 1 addition & 0 deletions crates/driver/src/domain/competition/order/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ pub struct Jit {
pub buy: eth::Asset,
pub receiver: eth::Address,
pub valid_to: util::Timestamp,
pub partially_fillable: bool,
pub app_data: AppData,
pub side: Side,
pub sell_token_balance: SellTokenBalance,
Expand Down
11 changes: 11 additions & 0 deletions crates/driver/src/domain/competition/order/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ pub enum Scheme {
PreSign,
}

impl Scheme {
pub fn to_boundary_scheme(&self) -> model::signature::SigningScheme {
match self {
Scheme::Eip712 => model::signature::SigningScheme::Eip712,
Scheme::EthSign => model::signature::SigningScheme::EthSign,
Scheme::Eip1271 => model::signature::SigningScheme::Eip1271,
Scheme::PreSign => model::signature::SigningScheme::PreSign,
}
}
}

pub fn domain_separator(
chain_id: chain::Id,
verifying_contract: eth::ContractAddress,
Expand Down
14 changes: 7 additions & 7 deletions crates/driver/src/domain/competition/solution/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ pub fn tx(
let mut native_unwrap = eth::TokenAmount(eth::U256::zero());

// Encode uniform clearing price vector
for asset in solution
for (token, amount) in solution
.clearing_prices()
.iter()
.sorted_by_cached_key(|asset| asset.token)
.into_iter()
.sorted_by_cached_key(|(token, _amount)| *token)
{
tokens.push(asset.token.into());
clearing_prices.push(asset.amount.into());
tokens.push(token.into());
clearing_prices.push(amount);
}

// Encode trades with custom clearing prices
Expand Down Expand Up @@ -312,7 +312,7 @@ struct Flags {
buy_token_balance: order::BuyTokenBalance,
}

mod codec {
pub mod codec {
use crate::domain::{competition::order, eth};

// cf. https://github.com/cowprotocol/contracts/blob/v1.5.0/src/contracts/libraries/GPv2Trade.sol#L16
Expand Down Expand Up @@ -392,7 +392,7 @@ mod codec {
)
}

pub(super) fn signature(signature: &order::Signature) -> super::Bytes<Vec<u8>> {
pub fn signature(signature: &order::Signature) -> super::Bytes<Vec<u8>> {
match signature.scheme {
order::signature::Scheme::Eip712 | order::signature::Scheme::EthSign => {
signature.data.clone()
Expand Down
27 changes: 13 additions & 14 deletions crates/driver/src/domain/competition/solution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ impl Solution {
&self.interactions
}

pub fn pre_interactions(&self) -> &[eth::Interaction] {
&self.pre_interactions
}

/// The solver which generated this solution.
pub fn solver(&self) -> &Solver {
&self.solver
Expand Down Expand Up @@ -401,11 +405,8 @@ impl Solution {
///
/// The rule which relates two prices for tokens X and Y is:
/// amount_x * price_x = amount_y * price_y
pub fn clearing_prices(&self) -> Vec<eth::Asset> {
let prices = self.prices.iter().map(|(&token, &amount)| eth::Asset {
token,
amount: amount.into(),
});
pub fn clearing_prices(&self) -> Prices {
let prices = self.prices.clone();

if self.user_trades().any(|trade| trade.order().buys_eth()) {
// The solution contains an order which buys ETH. Solvers only produce solutions
Expand All @@ -418,28 +419,26 @@ impl Solution {
// If no order trades WETH, the WETH price is not necessary, only the ETH
// price is needed. Remove the unneeded WETH price, which slightly reduces
// gas used by the settlement.
let mut prices = if self.user_trades().all(|trade| {
let mut prices: Prices = if self.user_trades().all(|trade| {
trade.order().sell.token != self.weth.0 && trade.order().buy.token != self.weth.0
}) {
prices
.filter(|price| price.token != self.weth.0)
.collect_vec()
.into_iter()
.filter(|(token, _price)| *token != self.weth.0)
.collect()
} else {
prices.collect_vec()
prices
};

// Add a clearing price for ETH equal to WETH.
prices.push(eth::Asset {
token: eth::ETH_TOKEN,
amount: self.prices[&self.weth.into()].to_owned().into(),
});
prices.insert(eth::ETH_TOKEN, self.prices[&self.weth.into()].to_owned());

return prices;
}

// TODO: We should probably filter out all unused prices to save gas.

prices.collect_vec()
prices
}

/// Clearing price for the given token.
Expand Down
4 changes: 2 additions & 2 deletions crates/driver/src/domain/competition/solution/settlement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ impl Settlement {
pub fn prices(&self) -> HashMap<eth::TokenAddress, eth::TokenAmount> {
self.solution
.clearing_prices()
.iter()
.map(|asset| (asset.token, asset.amount))
.into_iter()
.map(|(token, amount)| (token, amount.into()))
.collect()
}
}
Expand Down
54 changes: 23 additions & 31 deletions crates/driver/src/domain/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,52 +13,37 @@ use {
blockchain::{self, Ethereum},
solver::{self, Solver},
},
util::{self, conv::u256::U256Ext},
util,
},
anyhow::Context,
chrono::Utc,
num::CheckedDiv,
std::{collections::HashSet, iter, ops::Mul},
std::{
collections::{HashMap, HashSet},
iter,
},
};

/// A quote describing the expected outcome of an order.
#[derive(Debug)]
pub struct Quote {
/// The amount that can be bought if this was a sell order, or sold if this
/// was a buy order.
pub amount: eth::U256,
pub clearing_prices: HashMap<eth::H160, eth::U256>,
pub pre_interactions: Vec<eth::Interaction>,
pub interactions: Vec<eth::Interaction>,
pub solver: eth::Address,
pub gas: Option<eth::Gas>,
/// Which `tx.origin` is required to make the quote simulation pass.
pub tx_origin: Option<eth::Address>,
pub jit_orders: Vec<solution::trade::Jit>,
}

impl Quote {
fn new(eth: &Ethereum, order: &Order, solution: competition::Solution) -> Result<Self, Error> {
let sell_price = solution
.clearing_price(order.tokens.sell)
.ok_or(QuotingFailed::ClearingSellMissing)?
.to_big_rational();
let buy_price = solution
.clearing_price(order.tokens.buy)
.ok_or(QuotingFailed::ClearingBuyMissing)?
.to_big_rational();
let order_amount = order.amount.0.to_big_rational();

let amount = match order.side {
order::Side::Sell => order_amount
.mul(sell_price)
.checked_div(&buy_price)
.context("div by zero: buy price")?,
order::Side::Buy => order_amount
.mul(&buy_price)
.checked_div(&sell_price)
.context("div by zero: sell price")?,
};

fn new(eth: &Ethereum, solution: competition::Solution) -> Result<Self, Error> {
Ok(Self {
amount: eth::U256::from_big_rational(&amount)?,
clearing_prices: solution
.clearing_prices()
.into_iter()
.map(|(token, amount)| (token.into(), amount))
.collect(),
pre_interactions: solution.pre_interactions().to_vec(),
interactions: solution
.interactions()
.iter()
Expand All @@ -70,6 +55,14 @@ impl Quote {
solver: solution.solver().address(),
gas: solution.gas(),
tx_origin: *solution.solver().quote_tx_origin(),
jit_orders: solution
.trades()
.iter()
.filter_map(|trade| match trade {
solution::Trade::Jit(jit) => Some(jit.clone()),
_ => None,
})
.collect(),
})
}
}
Expand Down Expand Up @@ -110,7 +103,6 @@ impl Order {
let solutions = solver.solve(&auction, &liquidity).await?;
Quote::new(
eth,
self,
// TODO(#1468): choose the best solution in the future, but for now just pick the
// first solution
solutions
Expand Down
101 changes: 87 additions & 14 deletions crates/driver/src/infra/api/routes/quote/dto/quote.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
use {
crate::{
domain::{eth, quote},
domain::{self, competition::solution::encoding::codec, eth, quote},
util::serialize,
},
model::{
order::{BuyTokenDestination, SellTokenSource},
signature::SigningScheme,
},
serde::Serialize,
serde_with::serde_as,
std::collections::HashMap,
};

impl Quote {
pub fn new(quote: &quote::Quote) -> Self {
pub fn new(quote: quote::Quote) -> Self {
Self {
amount: quote.amount,
interactions: quote
.interactions
.iter()
.map(|interaction| Interaction {
target: interaction.target.into(),
value: interaction.value.into(),
call_data: interaction.call_data.clone().into(),
})
.collect(),
clearing_prices: quote.clearing_prices,
pre_interactions: quote.pre_interactions.into_iter().map(Into::into).collect(),
interactions: quote.interactions.into_iter().map(Into::into).collect(),
solver: quote.solver.0,
gas: quote.gas.map(|gas| gas.0.as_u64()),
tx_origin: quote.tx_origin.map(|addr| addr.0),
jit_orders: quote.jit_orders.into_iter().map(Into::into).collect(),
}
}
}
Expand All @@ -31,14 +30,16 @@ impl Quote {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Quote {
#[serde_as(as = "serialize::U256")]
amount: eth::U256,
#[serde_as(as = "HashMap<_, serialize::U256>")]
clearing_prices: HashMap<eth::H160, eth::U256>,
pre_interactions: Vec<Interaction>,
interactions: Vec<Interaction>,
solver: eth::H160,
#[serde(skip_serializing_if = "Option::is_none")]
gas: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
tx_origin: Option<eth::H160>,
jit_orders: Vec<JitOrder>,
}

#[serde_as]
Expand All @@ -51,3 +52,75 @@ struct Interaction {
#[serde_as(as = "serialize::Hex")]
call_data: Vec<u8>,
}

impl From<eth::Interaction> for Interaction {
fn from(interaction: eth::Interaction) -> Self {
Self {
target: interaction.target.into(),
value: interaction.value.into(),
call_data: interaction.call_data.into(),
}
}
}

#[serde_as]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct JitOrder {
buy_token: eth::H160,
sell_token: eth::H160,
#[serde_as(as = "serialize::U256")]
sell_amount: eth::U256,
#[serde_as(as = "serialize::U256")]
buy_amount: eth::U256,
#[serde_as(as = "serialize::U256")]
executed_amount: eth::U256,
receiver: eth::H160,
partially_fillable: bool,
valid_to: u32,
#[serde_as(as = "serialize::Hex")]
app_data: [u8; 32],
side: Side,
sell_token_source: SellTokenSource,
buy_token_destination: BuyTokenDestination,
#[serde_as(as = "serialize::Hex")]
signature: Vec<u8>,
signing_scheme: SigningScheme,
}

impl From<domain::competition::solution::trade::Jit> for JitOrder {
fn from(jit: domain::competition::solution::trade::Jit) -> Self {
Self {
sell_token: jit.order().sell.token.into(),
buy_token: jit.order().buy.token.into(),
sell_amount: jit.order().sell.amount.into(),
buy_amount: jit.order().buy.amount.into(),
executed_amount: jit.executed().into(),
receiver: jit.order().receiver.into(),
partially_fillable: jit.order().partially_fillable,
valid_to: jit.order().valid_to.into(),
app_data: jit.order().app_data.into(),
side: jit.order().side.into(),
sell_token_source: jit.order().sell_token_balance.into(),
buy_token_destination: jit.order().buy_token_balance.into(),
signature: codec::signature(&jit.order().signature).into(),
signing_scheme: jit.order().signature.scheme.to_boundary_scheme(),
}
}
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
enum Side {
Sell,
Buy,
}

impl From<domain::competition::order::Side> for Side {
fn from(side: domain::competition::order::Side) -> Self {
match side {
domain::competition::order::Side::Sell => Side::Sell,
domain::competition::order::Side::Buy => Side::Buy,
}
}
}
2 changes: 1 addition & 1 deletion crates/driver/src/infra/api/routes/quote/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async fn route(
)
.await;
observe::quoted(state.solver().name(), &order, &quote);
Ok(axum::response::Json(dto::Quote::new(&quote?)))
Ok(axum::response::Json(dto::Quote::new(quote?)))
};

handle_request
Expand Down
5 changes: 4 additions & 1 deletion crates/driver/src/infra/solver/dto/solution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ impl Solutions {
token: jit.order.buy_token.into(),
},
receiver: jit.order.receiver.into(),
partially_fillable: jit.order.partially_fillable,
valid_to: jit.order.valid_to.into(),
app_data: jit.order.app_data.into(),
side: match jit.order.kind {
Expand Down Expand Up @@ -286,6 +287,8 @@ struct JitOrder {
sell_amount: eth::U256,
#[serde_as(as = "serialize::U256")]
buy_amount: eth::U256,
#[serde(default)]
partially_fillable: bool,
valid_to: u32,
#[serde_as(as = "serialize::Hex")]
app_data: [u8; order::APP_DATA_LEN],
Expand All @@ -312,7 +315,7 @@ impl JitOrder {
Kind::Sell => OrderKind::Sell,
Kind::Buy => OrderKind::Buy,
},
partially_fillable: false,
partially_fillable: self.partially_fillable,
sell_token_balance: match self.sell_token_balance {
SellTokenBalance::Erc20 => SellTokenSource::Erc20,
SellTokenBalance::Internal => SellTokenSource::Internal,
Expand Down
Loading

0 comments on commit 1413398

Please sign in to comment.