Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
51 changes: 49 additions & 2 deletions crates/database/src/auction.rs
Original file line number Diff line number Diff line change
@@ -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,
};

Expand Down Expand Up @@ -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<HashMap<(AuctionId, OrderUid), BigDecimal>, 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<BigDecimal>,
}
let rows: Vec<Row> = 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};
Expand Down
20 changes: 20 additions & 0 deletions crates/e2e/tests/e2e/penalty_cap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
7 changes: 7 additions & 0 deletions crates/model/src/trade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ pub struct Trade {
#[serde_as(as = "Option<HexOrDecimalU256>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gas_cost: Option<U256>,
/// 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<HexOrDecimalU256>")]
pub penalty_cap_native: Option<U256>,
}

#[cfg(test)]
Expand Down Expand Up @@ -64,6 +69,7 @@ mod tests {
"buyToken": "0x0000000000000000000000000000000000000009",
"txHash": "0x0000000000000000000000000000000000000000000000000000000000000040",
"gasCost": "3000000",
"penaltyCapNative": "1000000000000000",
"executedProtocolFees": [
{
"amount": "5",
Expand Down Expand Up @@ -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),
Expand Down
9 changes: 9 additions & 0 deletions crates/orderbook/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1810,6 +1810,15 @@ 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 penalties disabled or the
Comment thread
fleupold marked this conversation as resolved.
Outdated
trade can't be attributed to an auction.
allOf:
- $ref: "#/components/schemas/BigUint"
nullable: true
required:
- blockNumber
- logIndex
Expand Down
54 changes: 36 additions & 18 deletions crates/orderbook/src/database/trades.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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::<Result<Vec<_>>>()
}
Expand Down Expand Up @@ -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::<Result<Vec<_>>>()
}
Expand All @@ -155,6 +166,7 @@ impl TradeRetrievingPaginated for Postgres {
fn trade_from(
row: TradesQueryRow,
executed_protocol_fees: Vec<ExecutedProtocolFee>,
penalty_cap_native: Option<BigDecimal>,
) -> Result<Trade> {
let block_number = row
.block_number
Expand All @@ -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,
Expand All @@ -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]
Expand All @@ -211,6 +228,7 @@ mod tests {
..Default::default()
},
vec![],
None,
)
};
assert_eq!(
Expand Down
Loading