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
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?;

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

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.

This can be wildly inaccurate because a super complicated order can be batched together with a simple order. I don't think there is a way to make this more accurate with reasonable effort - just wanted to flag this in case you weren't aware.

@jmg-duarte jmg-duarte Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I knew all this could be innacurate, the idea is to give an estimate (slightly better than "solver reported" as they can simply write a different value)

/// 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"#;

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.
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
Comment on lines +85 to +86

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,
" 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 on lines +182 to +185

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.

Should it be covered with a small DB test to pin it down: one block, two settlements in different txs, a few trades each, then assert each trade's share and the per-order sum.

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 on lines +715 to +717

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.

This doc comment is incorrect and contradicts both the implementation and the OpenAPI spec. The value populated into gas_cost comes from database::trades::order_gas_costs, i.e. the actual on-chain settlement gas cost (gas_used * effective_gas_price from the receipt, split across trades) — not the quote (gas_amount * gas_price). The None condition is also wrong: it's None for orders settled before V111 or not yet settled, not "the order has no stored quote".

Suggested change
/// 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.
/// Actual on-chain gas cost (in native token wei) attributed to this order:
/// its share of the settlement transaction(s)' gas cost
/// (`gas_used * effective_gas_price`), split equally across all trades in
/// each settlement and summed across the order's fills. `None` for orders
/// settled before this data was recorded (see V111), or not yet settled.

#[serde_as(as = "Option<HexOrDecimalU256>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gas_cost: Option<U256>,
Comment on lines +715 to +720

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.

high

The docstring for gas_cost is incorrect and misleading. It states that the gas cost is an "Estimated network gas cost... derived from its quote", but the PR actually implements and exposes the actual on-chain gas cost attributed to the order (as documented in the OpenAPI spec and implemented in the database queries). Please update the docstring to accurately reflect that this is the actual on-chain gas cost.

Suggested change
/// 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.
#[serde_as(as = "Option<HexOrDecimalU256>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gas_cost: Option<U256>,
/// Actual on-chain gas cost (in native token wei) attributed to this
/// order. Computed as the order's share of the gas cost
/// (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. None for orders
/// settled before this data started being recorded, or not yet settled.
#[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>,
/// 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 on lines +34 to +36

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.

Same issue as the OrderMetadata::gas_cost comment: this is the actual on-chain gas cost (this trade's share of the settlement's gas_used * effective_gas_price), not a quote-derived estimate. The "None ... (e.g. JIT orders)" claim is also misleading — JIT orders have trades and settlements, so they do get a gas cost here; the value is None only for trades settled before V111.

Suggested change
/// 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).
/// Actual on-chain gas cost (in native token wei) attributed to this trade:
/// its share of the settlement transaction's gas cost
/// (`gas_used * effective_gas_price`), split equally across all trades
/// settled in the same transaction. `None` for trades settled before this
/// data was recorded (see V111).

#[serde_as(as = "Option<HexOrDecimalU256>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gas_cost: Option<U256>,
Comment on lines +34 to +39

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.

high

The docstring for gas_cost is incorrect and misleading. It states that the gas cost is an "Estimated network gas cost... derived from the order's quote", but the PR actually implements and exposes the actual on-chain gas cost attributed to the trade. Please update the docstring to accurately reflect that this is the actual on-chain gas cost.

    /// Actual on-chain gas cost (in native token wei) attributed to this
    /// trade. 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. None for trades settled
    /// before this data started being recorded.
    #[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
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 on lines +1341 to +1342

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.

This exposes a ton of implementation details the user doesn't care about, no?
Key pieces of info appear to be:

  • it's an estimate (no need to explain how we estimate IMO)
  • it's an ETH value (instead of gas units)
  • may not be populated for old orders (unsettled orders could just get a 0 here, no?)

Same applies to the other openapi field

(`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