Skip to content
Open
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
3 changes: 2 additions & 1 deletion crates/database/src/jit_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ NULL AS onchain_user,
NULL AS onchain_placement_error,
COALESCE((SELECT SUM(executed_fee) FROM order_execution oe WHERE oe.order_uid = o.uid), 0) as executed_fee,
COALESCE((SELECT executed_fee_token FROM order_execution oe WHERE oe.order_uid = o.uid LIMIT 1), o.sell_token) as executed_fee_token, -- TODO surplus token
NULL AS full_app_data
NULL AS full_app_data,
(SELECT CASE WHEN COUNT(*) = COUNT(t.gas_cost) THEN SUM(t.gas_cost) END FROM trades t WHERE t.order_uid = o.uid) as gas_cost
"#;

pub const FROM: &str = "jit_orders o";
Expand Down
59 changes: 57 additions & 2 deletions crates/database/src/order_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,12 @@ mod tests {
super::*,
crate::{
byte_array::ByteArray,
events::EventIndex,
events::{Event, EventIndex, Settlement, Trade},
onchain_broadcasted_orders::{OnchainOrderPlacement, insert_onchain_order},
},
chrono::{DateTime, Duration, Utc},
futures::StreamExt,
sqlx::Connection,
sqlx::{Connection, types::BigDecimal},
};

type Data = ([u8; 56], Address, DateTime<Utc>);
Expand Down Expand Up @@ -360,5 +360,60 @@ mod tests {
// Unrelated address returns nothing.
let none = user_orders(&mut db, &ByteArray([0xabu8; 20]), 0, Some(100)).await;
assert!(none.is_empty());

// One fill per arm of the union. uid_a and uid_b are in `orders` so
// they split the 1000; uid_c only provides liquidity.
let event = |log_index| EventIndex {
block_number: 0,
log_index,
};
let fill = |log_index, order_uid| {
(
event(log_index),
Event::Trade(Trade {
order_uid,
..Default::default()
}),
)
};
crate::events::append(
&mut db,
&[
fill(0, uid_a),
fill(1, uid_b),
fill(2, uid_c),
(event(3), Event::Settlement(Settlement::default())),
],
)
.await
.unwrap();
crate::trades::attribute_gas_cost(
&mut db,
event(3),
BigDecimal::from(100),
BigDecimal::from(10),
&[],
)
.await
.unwrap();
let gas_costs = super::user_orders(&mut db, &owner, 0, Some(100))
.map(|order| {
let order = order.unwrap();
(order.uid, order.gas_cost)
})
.collect::<Vec<_>>()
.await;
assert_eq!(
gas_costs,
vec![
(uid_a, Some(BigDecimal::from(500))),
// In both tables; the union keeps the `orders` row.
(uid_b, Some(BigDecimal::from(500))),
// Read through the `jit_orders` arm.
(uid_c, Some(BigDecimal::from(0))),
(uid_d, None),
(uid_e, None),
]
);
}
}
130 changes: 127 additions & 3 deletions crates/database/src/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,9 @@ pub struct FullOrder {
pub executed_fee: BigDecimal,
pub executed_fee_token: Address,
pub full_app_data: Option<Vec<u8>>,
/// Share of its settlements' gas costs in native token wei, summed across
/// fills. `None` unless it has fills and every fill's cost is known.
pub gas_cost: Option<BigDecimal>,
}

impl FullOrder {
Expand Down Expand Up @@ -655,7 +658,8 @@ array(Select (p.target, p.value, p.data) from interactions p where p.order_uid =
(SELECT onchain_o.placement_error from onchain_placed_orders onchain_o where onchain_o.uid = o.uid limit 1) as onchain_placement_error,
COALESCE((SELECT SUM(executed_fee) FROM order_execution oe WHERE oe.order_uid = o.uid), 0) as executed_fee,
COALESCE((SELECT executed_fee_token FROM order_execution oe WHERE oe.order_uid = o.uid LIMIT 1), o.sell_token) as executed_fee_token, -- TODO surplus token
(SELECT full_app_data FROM app_data ad WHERE o.app_data = ad.contract_app_data LIMIT 1) as full_app_data
(SELECT full_app_data FROM app_data ad WHERE o.app_data = ad.contract_app_data LIMIT 1) as full_app_data,
(SELECT CASE WHEN COUNT(*) = COUNT(t.gas_cost) THEN SUM(t.gas_cost) END FROM trades t WHERE t.order_uid = o.uid) as gas_cost
"#;

pub const FROM: &str = "orders o";
Expand Down Expand Up @@ -828,7 +832,8 @@ pub fn solvable_orders(
NULL AS onchain_placement_error,
COALESCE(fee_agg.executed_fee,0) AS executed_fee,
COALESCE(fee_agg.executed_fee_token, lo.sell_token) AS executed_fee_token,
ad.full_app_data
ad.full_app_data,
NULL::numeric AS gas_cost
FROM live_orders lo
LEFT JOIN LATERAL (
SELECT NOT signed AS unsigned
Expand Down Expand Up @@ -958,7 +963,8 @@ SELECT
opo.onchain_placement_error,
COALESCE(fee_agg.executed_fee,0) AS executed_fee,
COALESCE(fee_agg.executed_fee_token, so.sell_token) AS executed_fee_token,
ad.full_app_data
ad.full_app_data,
NULL::numeric AS gas_cost
FROM selected_orders so
LEFT JOIN LATERAL (
SELECT NOT signed AS unsigned
Expand Down Expand Up @@ -1196,6 +1202,8 @@ mod tests {
assert_eq!(order.settlement_contract, full_order.settlement_contract);
assert_eq!(order.sell_token_balance, full_order.sell_token_balance);
assert_eq!(order.buy_token_balance, full_order.buy_token_balance);
// Never filled, so it has no gas cost rather than one of zero.
assert_eq!(full_order.gas_cost, None);
}

#[tokio::test]
Expand Down Expand Up @@ -2365,6 +2373,122 @@ mod tests {
}
}

async fn two_orders(db: &mut PgTransaction<'_>) -> (OrderUid, OrderUid) {
let (order_a, order_b) = (ByteArray([1; 56]), ByteArray([2; 56]));
for uid in [order_a, order_b] {
insert_order(
db,
&Order {
uid,
..Default::default()
},
)
.await
.unwrap();
}
(order_a, order_b)
}

async fn fill(db: &mut PgTransaction<'_>, order_uid: OrderUid, log_index: i64) {
crate::events::append(
db,
&[(
EventIndex {
block_number: 0,
log_index,
},
Event::Trade(Trade {
order_uid,
..Default::default()
}),
)],
)
.await
.unwrap();
}

/// Attributes `gas_used` to the settled trades at a gas price of 10;
/// `None` leaves the settlement unattributed.
async fn settle(db: &mut PgTransaction<'_>, log_index: i64, tx: u8, gas_used: Option<u64>) {
crate::events::append(
db,
&[(
EventIndex {
block_number: 0,
log_index,
},
Event::Settlement(Settlement {
transaction_hash: ByteArray([tx; 32]),
..Default::default()
}),
)],
)
.await
.unwrap();
if let Some(gas_used) = gas_used {
crate::trades::attribute_gas_cost(
db,
EventIndex {
block_number: 0,
log_index,
},
BigDecimal::from(gas_used),
BigDecimal::from(10),
&[],
)
.await
.unwrap();
}
}

async fn order_gas(db: &mut PgConnection, uid: OrderUid) -> Option<BigDecimal> {
single_full_order_with_quote(db, &uid)
.await
.unwrap()
.unwrap()
.full_order
.gas_cost
}

/// An order's gas cost sums its share of each settlement that filled it,
/// and becomes unknown as soon as any fill is unattributed.
#[tokio::test]
#[ignore]
async fn postgres_order_gas_cost_across_fills() {
let mut db = PgConnection::connect("postgresql://").await.unwrap();
let mut db = db.begin().await.unwrap();
crate::clear_DANGER_(&mut db).await.unwrap();
let (order_a, order_b) = two_orders(&mut db).await;

// 100 gas at price 10, split over this settlement's two trades.
fill(&mut db, order_a, 0).await;
fill(&mut db, order_b, 1).await;
settle(&mut db, 2, 0, Some(100)).await;
assert_eq!(order_gas(&mut db, order_a).await, Some(500.into()));
assert_eq!(order_gas(&mut db, order_b).await, Some(500.into()));

// order_a fills again, alone, so it adds that settlement's whole 3000.
fill(&mut db, order_a, 3).await;
settle(&mut db, 4, 1, Some(300)).await;
let mut batch = many_full_orders_with_quotes(&mut db, &[order_a, order_b])
.await
.unwrap();
batch.sort_by_key(|order| order.full_order.uid.0);
assert_eq!(
batch
.iter()
.map(|order| (order.full_order.uid, order.full_order.gas_cost.clone()))
.collect::<Vec<_>>(),
vec![(order_a, Some(3500.into())), (order_b, Some(500.into()))]
);

// A fill that was never attributed makes order_a's total unknown.
fill(&mut db, order_a, 5).await;
settle(&mut db, 6, 2, None).await;
assert!(order_gas(&mut db, order_a).await.is_none());
assert_eq!(order_gas(&mut db, order_b).await, Some(500.into()));
}

#[tokio::test]
#[ignore]
async fn postgres_latest_settlement_block() {
Expand Down
56 changes: 56 additions & 0 deletions crates/database/src/trades.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub struct TradesQueryRow {
pub sell_token: Address,
pub tx_hash: Option<TransactionHash>,
pub auction_id: Option<AuctionId>,
/// Share of the settlement's gas cost in native token wei, as attributed
/// by [`attribute_gas_cost`]: `NULL` until then, forever for settlements
/// observed before the column existed. `0` for a liquidity-only JIT order.
pub gas_cost: Option<BigDecimal>,
}

pub fn trades<'a>(
Expand All @@ -46,6 +50,7 @@ SELECT
o.owner,
o.buy_token,
o.sell_token,
t.gas_cost,
settlement.tx_hash,
settlement.auction_id"#;

Expand Down Expand Up @@ -183,6 +188,9 @@ pub async fn get_trades_for_settlement(
/// settlement. As with `settlements.gas_used`, a transaction with two
/// settlements attributes its full cost twice, once per settlement. We do not
/// expect this to happen.
///
/// Must run after the settled orders' rows exist: an on-chain order whose row
/// is not indexed yet is permanently classed as liquidity-only.
#[instrument(skip_all)]
pub async fn attribute_gas_cost(
ex: &mut PgTransaction<'_>,
Expand Down Expand Up @@ -995,6 +1003,54 @@ mod tests {
);
}

/// A trade whose settlement was never attributed reports no cost at all.
#[tokio::test]
#[ignore]
async fn postgres_trades_report_attributed_gas_cost() {
let mut db = PgConnection::connect("postgresql://").await.unwrap();
let mut db = db.begin().await.unwrap();
crate::clear_DANGER_(&mut db).await.unwrap();

let users_and_orders = generate_owners_and_order_ids(&[2]).await;
let (owner, orders) = &users_and_orders[0];
let event = |log_index| EventIndex {
block_number: 0,
log_index,
};

// Two settlements in one block: the first settles both orders, the
// second only order[0]'s next fill.
add_order_and_trade(&mut db, *owner, orders[0], event(0), None, None).await;
add_order_and_trade(&mut db, *owner, orders[1], event(1), None, None).await;
let first = event(2);
add_settlement(&mut db, first, Default::default(), ByteArray([1; 32]), 1).await;
add_trade(&mut db, *owner, orders[0], event(3), None, None).await;
let second = event(4);
add_settlement(&mut db, second, Default::default(), ByteArray([2; 32]), 2).await;

// Only the first settlement is attributed.
attribute_gas_cost(&mut db, first, 100.into(), 10.into(), &[])
.await
.unwrap();

let mut rows = trades(&mut db, Some(owner), None, 0, 1000)
.into_inner()
.await
.unwrap();
rows.sort_by_key(|row| row.log_index);
assert_eq!(
rows.iter()
.map(|row| (row.order_uid, row.gas_cost.clone(), row.tx_hash))
.collect::<Vec<_>>(),
vec![
// 1000 wei split between the first settlement's 2 trades.
(orders[0], Some(500.into()), Some(ByteArray([1; 32]))),
(orders[1], Some(500.into()), Some(ByteArray([1; 32]))),
(orders[0], None, Some(ByteArray([2; 32]))),
]
);
}

#[tokio::test]
#[ignore]
async fn postgres_token_first_trade_block() {
Expand Down
5 changes: 5 additions & 0 deletions crates/model/src/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,11 @@ pub struct OrderMetadata {
#[serde_as(as = "HexOrDecimalU256")]
pub executed_fee: U256,
pub executed_fee_token: Address,
/// Share of its settlements' gas costs in native token wei, summed across
/// fills. `None` unless it has fills and every fill's cost is known.
#[serde_as(as = "Option<HexOrDecimalU256>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gas_cost: Option<U256>,
pub invalidated: bool,
pub status: OrderStatus,
#[serde(flatten)]
Expand Down
11 changes: 10 additions & 1 deletion crates/model/src/trade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

use {
crate::{fee_policy::ExecutedProtocolFee, order::OrderUid},
alloy_primitives::{Address, B256},
alloy_primitives::{Address, B256, U256},
num::BigUint,
number::serialization::HexOrDecimalU256,
serde::Serialize,
serde_with::{DisplayFromStr, serde_as},
};
Expand All @@ -30,6 +31,12 @@ pub struct Trade {
// Settlement Data
pub tx_hash: Option<B256>,
pub executed_protocol_fees: Vec<ExecutedProtocolFee>,
/// Share of the settlement's gas cost in native token wei. `None` until
/// the settlement is attributed, which never happens if it predates this
/// being recorded; `0` for a JIT order that only provided liquidity.
#[serde_as(as = "Option<HexOrDecimalU256>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gas_cost: Option<U256>,
}

#[cfg(test)]
Expand All @@ -56,6 +63,7 @@ mod tests {
"sellToken": "0x000000000000000000000000000000000000000a",
"buyToken": "0x0000000000000000000000000000000000000009",
"txHash": "0x0000000000000000000000000000000000000000000000000000000000000040",
"gasCost": "3000000",
"executedProtocolFees": [
{
"amount": "5",
Expand Down Expand Up @@ -104,6 +112,7 @@ mod tests {
buy_token: Address::with_last_byte(9),
sell_token: Address::with_last_byte(10),
tx_hash: Some(B256::with_last_byte(64)),
gas_cost: Some(U256::from(3_000_000u64)),
executed_protocol_fees: vec![
ExecutedProtocolFee {
amount: U256::from(5u64),
Expand Down
Loading
Loading