Skip to content
Open
9 changes: 9 additions & 0 deletions crates/autopilot/src/infra/persistence/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,15 @@ impl Persistence {
)
.await?;

database::settlements::update_settlement_gas(
&mut ex,
block_number,
log_index,
u256_to_big_decimal(&gas.0),
u256_to_big_decimal(&gas_price.0.0),
)
.await?;
Comment thread
jmg-duarte marked this conversation as resolved.

store_order_events(
&mut ex,
fee_breakdown.keys().cloned().collect(),
Expand Down
26 changes: 26 additions & 0 deletions crates/database/src/settlements.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use {
crate::{Address, PgTransaction, TransactionHash},
bigdecimal::BigDecimal,
sqlx::{Executor, PgConnection},
tracing::instrument,
};
Expand Down Expand Up @@ -89,6 +90,31 @@ WHERE block_number = $3 AND log_index = $4
.map(|_| ())
}

/// Stores the actual gas cost (read from the transaction receipt) of a settled
/// transaction. See migration `V111`.
#[instrument(skip_all)]
pub async fn update_settlement_gas(
ex: &mut PgConnection,
block_number: i64,
log_index: i64,
gas_used: BigDecimal,
effective_gas_price: BigDecimal,
) -> Result<(), sqlx::Error> {
const QUERY: &str = r#"
UPDATE settlements
SET gas_used = $1, effective_gas_price = $2
WHERE block_number = $3 AND log_index = $4
;"#;
sqlx::query(QUERY)
.bind(gas_used)
.bind(effective_gas_price)
.bind(block_number)
.bind(log_index)
.execute(ex)
.await?;
Ok(())
}

/// Deletes all database data that referenced the deleted settlement events.
#[instrument(skip_all)]
pub async fn delete(
Expand Down
114 changes: 102 additions & 12 deletions crates/database/src/trades.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,34 @@ pub struct TradesQueryRow {
pub sell_token: Address,
pub tx_hash: Option<TransactionHash>,
pub auction_id: Option<AuctionId>,
/// This trade's share of the settlement transaction's gas cost in native
/// token wei (`gas_used * effective_gas_price / trades_in_settlement`).
/// `NULL` for settlements observed before gas was persisted (see V111).
pub gas_cost: Option<BigDecimal>,
}

/// SQL expression computing a single trade's share of its settlement's on-chain
/// gas cost: the settlement's total cost (`gas_used * effective_gas_price`)
/// divided equally across all trades settled in the same transaction. Expects
Comment thread
jmg-duarte marked this conversation as resolved.
/// the settlement row to be aliased `s`; selected as the column `gas_cost`,
/// which is `NULL` for settlements whose gas was not persisted (see migration
/// V111). Shared by [`trades`] and [`order_gas_costs`] so the two cannot drift.
const GAS_COST_EXPR: &str = r#"FLOOR(
(s.gas_used * s.effective_gas_price)
/ NULLIF((
SELECT COUNT(*)
FROM trades tc
WHERE tc.block_number = s.block_number
AND tc.log_index < s.log_index
AND tc.log_index > COALESCE((
SELECT MAX(sp.log_index)
FROM settlements sp
WHERE sp.block_number = s.block_number
AND sp.log_index < s.log_index
), -1)
), 0)
) AS gas_cost"#;

Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
pub fn trades<'a>(
ex: &'a mut PgConnection,
owner_filter: Option<&'a Address>,
Expand All @@ -37,24 +63,37 @@ SELECT
t.sell_amount - t.fee_amount as sell_amount_before_fees,
o.owner,
o.buy_token,
o.sell_token,
settlement.tx_hash,
settlement.auction_id"#;

const SETTLEMENT_JOIN: &str = r#"
o.sell_token"#;

// Resolves the settlement that included each returned trade (the first
// settlement event in the same block after the trade) and computes that
// trade's share of the settlement's on-chain gas cost. Joined onto the
// already-paginated `page` CTE rather than inside the UNION branches, so the
// settlement lookup and the (relatively expensive) gas computation run only
// for the rows actually returned instead of for every candidate row across
// all three branches.
Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
const GAS_JOIN: &str = const_format::concatcp!(
r#"
LEFT OUTER JOIN LATERAL (
SELECT tx_hash, auction_id FROM settlements s
WHERE s.block_number = t.block_number
AND s.log_index > t.log_index
SELECT
s.tx_hash,
s.auction_id,
"#,
GAS_COST_EXPR,
r#"
FROM settlements s
WHERE s.block_number = page.block_number
AND s.log_index > page.log_index

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think deciding to not store the tx_hash in the trades table was a blunder back in the day which leads to this ugly code again and again. Also unless I'm missing something this code does not handle trades from multiple settlements in the same block correctly.
WDYT about introducing the tx_hash column on the trades table to simply join on that? Since we can already get the association using the log index this data can even be fully backfilled using a DB migration alone.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For what it's worth. Same-block different-tx case actually works fine imo. the LATERAL JOIN picks the right settlement per trade and the inner subquery counts the right divisor, I did a dry run for multiple settles within a block case.

The case that does mis-attribute is one tx calling settle() multiple times.
Not sure if this is applicable for now. But in theory, each Settlement event row ends up with the full receipt's gas_used (find_settlement_trace_and_callers only returns the first settle frame in the trace), so per-event splitting sums to 2x the real cost across the tx's trades.

No solver does this afaik, but tx_hash on trades sidesteps the question entirely. So +1 on the direction 🚀

ORDER BY s.log_index ASC
LIMIT 1
) AS settlement ON true"#;
) AS settlement ON true"#,
);

const QUERY: &str = const_format::concatcp!(
"WITH page AS (",
"(",
SELECT,
" FROM trades t",
SETTLEMENT_JOIN,
" JOIN orders o ON o.uid = t.order_uid",
// the uid already contains the owner address and we have
// an index on this expression so this is very efficient
Expand All @@ -67,7 +106,6 @@ LEFT OUTER JOIN LATERAL (
"(",
SELECT,
" FROM trades t",
SETTLEMENT_JOIN,
" JOIN orders o ON o.uid = t.order_uid",
" JOIN onchain_placed_orders onchain_o",
" ON onchain_o.uid = t.order_uid",
Expand Down Expand Up @@ -101,13 +139,30 @@ LEFT OUTER JOIN LATERAL (
SELECT,
" FROM jit o",
" JOIN trades t ON o.uid = t.order_uid",
SETTLEMENT_JOIN,
" ORDER BY t.block_number DESC, t.log_index DESC",
" LIMIT $3 + $4",
")",
" ORDER BY block_number DESC, log_index DESC",
" LIMIT $3",
" OFFSET $4",
")",
r#"
SELECT
page.block_number,
page.log_index,
page.order_uid,
page.buy_amount,
page.sell_amount,
page.sell_amount_before_fees,
page.owner,
page.buy_token,
page.sell_token,
settlement.tx_hash,
settlement.auction_id,
settlement.gas_cost
FROM page"#,
GAS_JOIN,
Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
" ORDER BY page.block_number DESC, page.log_index DESC",
);

sqlx::query_as(QUERY)
Expand All @@ -119,6 +174,41 @@ LEFT OUTER JOIN LATERAL (
.instrument(info_span!("trades"))
}

/// Returns, per order, the total on-chain gas cost (native token wei)
/// attributed to the order across all of its trades. Each trade contributes its
/// share of its settlement's gas cost (see [`GAS_COST_EXPR`]). Orders whose
/// settlements have no persisted gas (see V111) are absent from the result.
#[instrument(skip_all)]
pub async fn order_gas_costs(
ex: &mut PgConnection,
order_uids: &[OrderUid],
) -> Result<Vec<(OrderUid, BigDecimal)>, sqlx::Error> {
Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
if order_uids.is_empty() {
return Ok(vec![]);
}

const QUERY: &str = const_format::concatcp!(
r#"
SELECT t.order_uid, FLOOR(SUM(settlement.gas_cost)) AS gas_cost
FROM trades t
JOIN LATERAL (
SELECT "#,
GAS_COST_EXPR,
r#"
FROM settlements s
WHERE s.block_number = t.block_number
AND s.log_index > t.log_index
ORDER BY s.log_index ASC
LIMIT 1
) AS settlement ON true
WHERE settlement.gas_cost IS NOT NULL
AND t.order_uid = ANY($1)
GROUP BY t.order_uid"#,
);

sqlx::query_as(QUERY).bind(order_uids).fetch_all(ex).await
}

#[derive(Clone, Debug, Default, Eq, PartialEq, sqlx::FromRow)]
pub struct TradeEvent {
pub block_number: i64,
Expand Down
6 changes: 6 additions & 0 deletions crates/model/src/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,12 @@ pub struct OrderMetadata {
#[serde_as(as = "HexOrDecimalU256")]
pub executed_fee: U256,
pub executed_fee_token: Address,
/// Estimated network gas cost (in native token wei) of executing this
/// order, derived from its quote (`gas_amount * gas_price`). `None` when
/// the order has no stored quote.
Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
#[serde_as(as = "Option<HexOrDecimalU256>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gas_cost: Option<U256>,
Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
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>,
/// Estimated network gas cost (in native token wei) of executing this
/// trade, derived from the order's quote (`gas_amount * gas_price`).
/// `None` when the order has no stored quote (e.g. JIT orders).
Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
#[serde_as(as = "Option<HexOrDecimalU256>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gas_cost: Option<U256>,
Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
}

#[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
21 changes: 21 additions & 0 deletions crates/orderbook/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,17 @@ components:
allOf:
- $ref: "#/components/schemas/Address"
nullable: false
gasCost:
description: >
Actual on-chain gas cost attributed to this order, in native token
wei. Computed as the order's share of the gas cost
Comment thread
jmg-duarte marked this conversation as resolved.
Outdated
(`gas_used * effective_gas_price`) of the settlement transaction(s)
that executed it, split equally across all trades settled in the same
transaction, and summed across the order's fills. `null` for orders
settled before this data started being recorded, or not yet settled.
allOf:
- $ref: "#/components/schemas/BigUint"
nullable: true
fullAppData:
description: >
Full `appData`, which the contract-level `appData` is a hash of. See
Expand Down Expand Up @@ -1706,6 +1717,16 @@ components:
type: array
items:
$ref: "#/components/schemas/ExecutedProtocolFee"
gasCost:
description: >
Actual on-chain gas cost attributed to this trade, in native token
wei. Computed as this trade's share of the settlement transaction's
gas cost (`gas_used * effective_gas_price`), split equally across all
trades settled in the same transaction. `null` for trades settled
before this data started being recorded.
allOf:
- $ref: "#/components/schemas/BigUint"
nullable: true
required:
- blockNumber
- logIndex
Expand Down
Loading
Loading