diff --git a/crates/database/src/auction.rs b/crates/database/src/auction.rs index 8b66ce3781..28e40925df 100644 --- a/crates/database/src/auction.rs +++ b/crates/database/src/auction.rs @@ -1,8 +1,8 @@ use { crate::{Address, OrderUid}, bigdecimal::BigDecimal, - sqlx::{Connection, PgConnection, types::JsonValue}, - std::ops::DerefMut, + sqlx::{Connection, PgConnection, QueryBuilder, types::JsonValue}, + std::{collections::HashMap, ops::DerefMut}, tracing::instrument, }; @@ -191,6 +191,53 @@ pub async fn fetch_latest_token_price( Ok(price) } +/// Fetches the penalty caps recorded in `competition_auctions` for the given +/// `(auction_id, order_uid)` keys, in native token wei. A key is absent from +/// the result when its auction has no competition data, the order wasn't part +/// of that auction, or penalties were disabled for it. +#[instrument(skip_all)] +pub async fn penalty_caps( + ex: &mut PgConnection, + keys: &[(AuctionId, OrderUid)], +) -> Result, sqlx::Error> { + if keys.is_empty() { + return Ok(HashMap::new()); + } + + let mut query_builder = QueryBuilder::new( + "SELECT ca.id AS auction_id, vals.order_uid, \ + ca.penalty_caps_native[array_position(ca.order_uids, vals.order_uid)] AS penalty_cap \ + FROM competition_auctions ca INNER JOIN (VALUES ", + ); + for (i, (auction_id, order_uid)) in keys.iter().enumerate() { + if i > 0 { + query_builder.push(", "); + } + query_builder + .push("(") + .push_bind(auction_id) + .push(", ") + .push_bind(order_uid) + .push(")"); + } + query_builder.push(") AS vals(auction_id, order_uid) ON ca.id = vals.auction_id"); + + #[derive(sqlx::FromRow)] + struct Row { + auction_id: AuctionId, + order_uid: OrderUid, + penalty_cap: Option, + } + let rows: Vec = query_builder.build_query_as().fetch_all(ex).await?; + Ok(rows + .into_iter() + .filter_map(|row| { + row.penalty_cap + .map(|cap| ((row.auction_id, row.order_uid), cap)) + }) + .collect()) +} + #[cfg(test)] mod tests { use {super::*, crate::byte_array::ByteArray}; diff --git a/crates/e2e/tests/e2e/penalty_cap.rs b/crates/e2e/tests/e2e/penalty_cap.rs index 3ae60c5d70..80cb0ea09d 100644 --- a/crates/e2e/tests/e2e/penalty_cap.rs +++ b/crates/e2e/tests/e2e/penalty_cap.rs @@ -94,4 +94,24 @@ async fn penalty_cap(web3: Web3) { }) .await .unwrap(); + + // Once the order gets settled, the trade exposes the cap of the auction + // that settled it. + wait_for_condition(TIMEOUT, || async { + onchain.mint_block().await; + !services.get_trades(&uid).await.unwrap().is_empty() + }) + .await + .unwrap(); + let trade = services.get_trades(&uid).await.unwrap().remove(0); + let cap = trade + .penalty_cap_native + .expect("settled trade carries the auction's penalty cap"); + assert!(!cap.is_zero()); + let caps = crate::database::penalty_caps_of_order(services.db(), &uid).await; + assert!( + caps.iter() + .any(|db_cap| number::conversions::big_decimal_to_u256(db_cap) == Some(cap)), + "trade cap {cap} not among the caps persisted for the order" + ); } diff --git a/crates/model/src/trade.rs b/crates/model/src/trade.rs index abd8dabb6b..63aeeaa374 100644 --- a/crates/model/src/trade.rs +++ b/crates/model/src/trade.rs @@ -37,6 +37,11 @@ pub struct Trade { #[serde_as(as = "Option")] #[serde(default, skip_serializing_if = "Option::is_none")] pub gas_cost: Option, + /// Cap on the penalty the winning solver could have incurred. Absent when + /// the auction had penalties disabled or the trade can't be attributed + /// to an auction. + #[serde_as(as = "Option")] + pub penalty_cap_native: Option, } #[cfg(test)] @@ -64,6 +69,7 @@ mod tests { "buyToken": "0x0000000000000000000000000000000000000009", "txHash": "0x0000000000000000000000000000000000000000000000000000000000000040", "gasCost": "3000000", + "penaltyCapNative": "1000000000000000", "executedProtocolFees": [ { "amount": "5", @@ -113,6 +119,7 @@ mod tests { sell_token: Address::with_last_byte(10), tx_hash: Some(B256::with_last_byte(64)), gas_cost: Some(U256::from(3_000_000u64)), + penalty_cap_native: Some(U256::from(1_000_000_000_000_000u64)), executed_protocol_fees: vec![ ExecutedProtocolFee { amount: U256::from(5u64), diff --git a/crates/orderbook/openapi.yml b/crates/orderbook/openapi.yml index c85a085d3f..67e0e5345a 100644 --- a/crates/orderbook/openapi.yml +++ b/crates/orderbook/openapi.yml @@ -1810,6 +1810,14 @@ components: order that only provided liquidity. allOf: - $ref: "#/components/schemas/BigUint" + penaltyCapNative: + description: >- + Cap on the penalty the winning solver could incur for not executing + the order in the auction this trade settled, denominated in native + token wei. Absent when the auction had a pre CIP-87 global penalty cap. + allOf: + - $ref: "#/components/schemas/BigUint" + nullable: true required: - blockNumber - logIndex diff --git a/crates/orderbook/src/database/trades.rs b/crates/orderbook/src/database/trades.rs index 0fb7fdff25..d17dee4ce8 100644 --- a/crates/orderbook/src/database/trades.rs +++ b/crates/orderbook/src/database/trades.rs @@ -5,6 +5,7 @@ use { database::{byte_array::ByteArray, trades::TradesQueryRow}, model::{fee_policy::ExecutedProtocolFee, order::OrderUid, trade::Trade}, number::conversions::{big_decimal_to_big_uint, big_decimal_to_u256}, + sqlx::types::BigDecimal, std::convert::TryInto, }; @@ -72,20 +73,25 @@ impl TradeRetrieving for Postgres { let executed_protocol_fees = self .executed_protocol_fees(auction_order_uids.as_slice()) .await?; + let penalty_caps = { + let _timer = super::Metrics::get() + .database_queries + .with_label_values(&["penalty_caps"]) + .start_timer(); + database::auction::penalty_caps(&mut ex, auction_order_uids.as_slice()).await? + }; trades .into_iter() .map(|trade| { - let executed_protocol_fees = trade + let key = trade .auction_id - .map(|auction_id| { - executed_protocol_fees - .get(&(auction_id, trade.order_uid)) - .cloned() - .unwrap_or_default() - }) + .map(|auction_id| (auction_id, trade.order_uid)); + let executed_protocol_fees = key + .and_then(|key| executed_protocol_fees.get(&key).cloned()) .unwrap_or_default(); - trade_from(trade, executed_protocol_fees) + let penalty_cap_native = key.and_then(|key| penalty_caps.get(&key).cloned()); + trade_from(trade, executed_protocol_fees, penalty_cap_native) }) .collect::>>() } @@ -133,20 +139,25 @@ impl TradeRetrievingPaginated for Postgres { let executed_protocol_fees = self .executed_protocol_fees(auction_order_uids.as_slice()) .await?; + let penalty_caps = { + let _timer = super::Metrics::get() + .database_queries + .with_label_values(&["penalty_caps"]) + .start_timer(); + database::auction::penalty_caps(&mut ex, auction_order_uids.as_slice()).await? + }; trades .into_iter() .map(|trade| { - let executed_protocol_fees = trade + let key = trade .auction_id - .map(|auction_id| { - executed_protocol_fees - .get(&(auction_id, trade.order_uid)) - .cloned() - .unwrap_or_default() - }) + .map(|auction_id| (auction_id, trade.order_uid)); + let executed_protocol_fees = key + .and_then(|key| executed_protocol_fees.get(&key).cloned()) .unwrap_or_default(); - trade_from(trade, executed_protocol_fees) + let penalty_cap_native = key.and_then(|key| penalty_caps.get(&key).cloned()); + trade_from(trade, executed_protocol_fees, penalty_cap_native) }) .collect::>>() } @@ -155,6 +166,7 @@ impl TradeRetrievingPaginated for Postgres { fn trade_from( row: TradesQueryRow, executed_protocol_fees: Vec, + penalty_cap_native: Option, ) -> Result { let block_number = row .block_number @@ -177,6 +189,10 @@ fn trade_from( .as_ref() .map(|cost| big_decimal_to_u256(cost).context("gas cost is not a valid u256")) .transpose()?; + let penalty_cap_native = penalty_cap_native + .as_ref() + .map(|cap| big_decimal_to_u256(cap).context("penalty_cap_native is not a U256")) + .transpose()?; Ok(Trade { block_number, log_index, @@ -190,16 +206,17 @@ fn trade_from( tx_hash, executed_protocol_fees, gas_cost, + penalty_cap_native, }) } #[cfg(test)] mod tests { - use {super::*, alloy::primitives::U256, sqlx::types::BigDecimal}; + use {super::*, alloy::primitives::U256}; #[test] fn convert_trade() { - trade_from(TradesQueryRow::default(), vec![]).unwrap(); + trade_from(TradesQueryRow::default(), vec![], None).unwrap(); } #[test] @@ -211,6 +228,7 @@ mod tests { ..Default::default() }, vec![], + None, ) }; assert_eq!(