diff --git a/crates/autopilot/src/domain/auction/order.rs b/crates/autopilot/src/domain/auction/order.rs index 3f1678767b..4b1a3a8198 100644 --- a/crates/autopilot/src/domain/auction/order.rs +++ b/crates/autopilot/src/domain/auction/order.rs @@ -155,13 +155,51 @@ pub enum Signature { PreSign, } +impl Signature { + pub fn scheme(&self) -> SigningScheme { + match self { + Signature::Eip712(_) => SigningScheme::Eip712, + Signature::EthSign(_) => SigningScheme::EthSign, + Signature::Eip1271(_) => SigningScheme::Eip1271, + Signature::PreSign => SigningScheme::PreSign, + } + } + + pub fn to_bytes(&self) -> Vec { + match self { + Self::Eip712(signature) | Self::EthSign(signature) => signature.to_bytes().to_vec(), + Self::Eip1271(signature) => signature.clone(), + Self::PreSign => Vec::new(), + } + } +} + #[derive(Clone, Debug, PartialEq)] +pub enum SigningScheme { + Eip712, + EthSign, + Eip1271, + PreSign, +} + +#[derive(Copy, Clone, Debug, PartialEq)] pub struct EcdsaSignature { pub r: H256, pub s: H256, pub v: u8, } +impl EcdsaSignature { + /// r + s + v + pub fn to_bytes(self) -> [u8; 65] { + let mut bytes = [0u8; 65]; + bytes[..32].copy_from_slice(self.r.as_bytes()); + bytes[32..64].copy_from_slice(self.s.as_bytes()); + bytes[64] = self.v; + bytes + } +} + /// An amount denominated in the sell token for [`Side::Sell`] [`Order`]s, or in /// the buy token for [`Side::Buy`] [`Order`]s. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] diff --git a/crates/autopilot/src/domain/eth/mod.rs b/crates/autopilot/src/domain/eth/mod.rs index 49f19c3246..af97ab2541 100644 --- a/crates/autopilot/src/domain/eth/mod.rs +++ b/crates/autopilot/src/domain/eth/mod.rs @@ -8,11 +8,11 @@ pub use primitive_types::{H160, H256, U256}; pub struct Address(pub H160); /// Block number. -#[derive(Debug, Copy, Clone, From, PartialEq, PartialOrd)] +#[derive(Debug, Copy, Clone, From, PartialEq, PartialOrd, Default)] pub struct BlockNo(pub u64); /// A transaction ID, AKA transaction hash. -#[derive(Debug, Copy, Clone, From)] +#[derive(Debug, Copy, Clone, From, Default)] pub struct TxId(pub H256); /// An ERC20 token address. @@ -123,7 +123,7 @@ pub struct Gas(pub U256); /// The `effective_gas_price` as defined by EIP-1559. /// /// https://eips.ethereum.org/EIPS/eip-1559#specification -#[derive(Debug, Clone, Copy, Display)] +#[derive(Debug, Clone, Copy, Display, Default)] pub struct EffectiveGasPrice(pub Ether); impl From for EffectiveGasPrice { @@ -218,7 +218,7 @@ pub struct Asset { } /// An amount of native Ether tokens denominated in wei. -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, From, Into, Display)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, From, Into, Display, Default)] pub struct Ether(pub U256); impl std::ops::Add for Ether { @@ -263,7 +263,7 @@ pub struct Event { } /// Any type of on-chain transaction. -#[derive(Debug)] +#[derive(Debug, Clone, Default)] pub struct Transaction { /// The hash of the transaction. pub hash: TxId, @@ -273,8 +273,10 @@ pub struct Transaction { pub input: Calldata, /// The block number of the block that contains the transaction. pub block: BlockNo, + /// The timestamp of the block that contains the transaction. + pub timestamp: u32, /// The gas used by the transaction. pub gas: Gas, /// The effective gas price of the transaction. - pub effective_gas_price: EffectiveGasPrice, + pub gas_price: EffectiveGasPrice, } diff --git a/crates/autopilot/src/domain/settlement/auction.rs b/crates/autopilot/src/domain/settlement/auction.rs index a06646353f..4994ba2a7f 100644 --- a/crates/autopilot/src/domain/settlement/auction.rs +++ b/crates/autopilot/src/domain/settlement/auction.rs @@ -17,22 +17,3 @@ pub struct Auction { /// surplus if settled. pub surplus_capturing_jit_order_owners: HashSet, } - -impl Auction { - /// Protocol defines rules whether an order is eligible to contribute to the - /// surplus of a settlement. - pub fn is_surplus_capturing(&self, order: &domain::OrderUid) -> bool { - // All orders in the auction contribute to surplus - if self.orders.contains_key(order) { - return true; - } - // Some JIT orders contribute to surplus, for example COW AMM orders - if self - .surplus_capturing_jit_order_owners - .contains(&order.owner()) - { - return true; - } - false - } -} diff --git a/crates/autopilot/src/domain/settlement/mod.rs b/crates/autopilot/src/domain/settlement/mod.rs index 847c1e6671..749cb51482 100644 --- a/crates/autopilot/src/domain/settlement/mod.rs +++ b/crates/autopilot/src/domain/settlement/mod.rs @@ -4,15 +4,15 @@ //! a form of settlement transaction. use { - self::solution::ExecutedFee, crate::{domain, domain::eth, infra}, + num::Saturating, std::collections::HashMap, }; mod auction; -mod solution; +mod trade; mod transaction; -pub use {auction::Auction, solution::Solution, transaction::Transaction}; +pub use {auction::Auction, trade::Trade, transaction::Transaction}; /// A settled transaction together with the `Auction`, for which it was executed /// on-chain. @@ -21,11 +21,97 @@ pub use {auction::Auction, solution::Solution, transaction::Transaction}; #[allow(dead_code)] #[derive(Debug)] pub struct Settlement { - settled: Transaction, + /// The gas used by the settlement transaction. + gas: eth::Gas, + /// The effective gas price of the settlement transaction. + gas_price: eth::EffectiveGasPrice, + /// The address of the solver that submitted the settlement transaction. + solver: eth::Address, + /// The block number of the block that contains the settlement transaction. + block: eth::BlockNo, + /// The associated auction. auction: Auction, + /// Trades that were settled by the transaction. + trades: Vec, } impl Settlement { + /// The gas used by the settlement. + pub fn gas(&self) -> eth::Gas { + self.gas + } + + /// The effective gas price at the time of settlement. + pub fn gas_price(&self) -> eth::EffectiveGasPrice { + self.gas_price + } + + /// Total surplus for all trades in the settlement. + pub fn surplus_in_ether(&self) -> eth::Ether { + self.trades + .iter() + .map(|trade| { + trade + .surplus_in_ether(&self.auction.prices) + .unwrap_or_else(|err| { + tracing::warn!( + ?err, + trade = %trade.uid(), + "possible incomplete surplus calculation", + ); + num::zero() + }) + }) + .sum() + } + + /// Total fee taken for all the trades in the settlement. + pub fn fee_in_ether(&self) -> eth::Ether { + self.trades + .iter() + .map(|trade| { + trade + .fee_in_ether(&self.auction.prices) + .unwrap_or_else(|err| { + tracing::warn!( + ?err, + trade = %trade.uid(), + "possible incomplete fee calculation", + ); + num::zero() + }) + }) + .sum() + } + + /// Per order fees breakdown. Contains all orders from the settlement + pub fn order_fees(&self) -> HashMap> { + self.trades + .iter() + .map(|trade| { + let total = trade.fee_in_sell_token(); + let protocol = trade.protocol_fees_in_sell_token(&self.auction); + let fee = match (total, protocol) { + (Ok(total), Ok(protocol)) => { + let network = + total.saturating_sub(protocol.iter().map(|(fee, _)| *fee).sum()); + Some(trade::ExecutedFee { protocol, network }) + } + _ => None, + }; + (*trade.uid(), fee) + }) + .collect() + } + + /// Return all trades that are classified as Just-In-Time (JIT) orders. + pub fn jit_orders(&self) -> Vec<&trade::Jit> { + self.trades + .iter() + .filter_map(|trade| trade.as_jit()) + .collect() + } + pub async fn new( settled: Transaction, persistence: &infra::Persistence, @@ -42,54 +128,20 @@ impl Settlement { let auction = persistence.get_auction(settled.auction_id).await?; - // winning solution - solution promised during solver competition - let promised = persistence.get_winning_solution(settled.auction_id).await?; - - if settled.solver != promised.solver() { - return Err(Error::SolverMismatch { - expected: promised.solver(), - got: settled.solver, - }); - } - - let settled_score = settled.solution.score(&auction)?; - - // temp log - if settled_score != promised.score() { - tracing::debug!( - ?settled.auction_id, - "score mismatch: expected promised score {}, settled score {}", - promised.score(), - settled_score, - ); - } - - Ok(Self { settled, auction }) - } - - /// The gas used by the settlement. - pub fn gas(&self) -> eth::Gas { - self.settled.gas - } - - /// The effective gas price at the time of settlement. - pub fn gas_price(&self) -> eth::EffectiveGasPrice { - self.settled.effective_gas_price - } - - /// Total surplus expressed in native token. - pub fn native_surplus(&self) -> eth::Ether { - self.settled.solution.native_surplus(&self.auction) - } - - /// Total fee expressed in native token. - pub fn native_fee(&self) -> eth::Ether { - self.settled.solution.native_fee(&self.auction.prices) - } + let trades = settled + .trades + .into_iter() + .map(|trade| Trade::new(trade, &auction, settled.timestamp)) + .collect(); - /// Per order fees breakdown. Contains all orders from the settlement - pub fn order_fees(&self) -> HashMap> { - self.settled.solution.fees(&self.auction) + Ok(Self { + solver: settled.solver, + block: settled.block, + gas: settled.gas, + gas_price: settled.gas_price, + trades, + auction, + }) } } @@ -101,15 +153,6 @@ pub enum Error { InconsistentData(InconsistentData), #[error("settlement refers to an auction from a different environment")] WrongEnvironment, - #[error(transparent)] - BuildingSolution(#[from] solution::Error), - #[error(transparent)] - BuildingScore(#[from] solution::error::Score), - #[error("solver mismatch: expected competition solver {expected}, settlement solver {got}")] - SolverMismatch { - expected: eth::Address, - got: eth::Address, - }, } /// Errors that can occur when fetching data from the persistence layer. @@ -176,3 +219,435 @@ impl From for Error { Self::Infra(err.0) } } + +#[cfg(test)] +mod tests { + use { + crate::{ + domain, + domain::{auction, eth}, + }, + hex_literal::hex, + std::collections::{HashMap, HashSet}, + }; + + // https://etherscan.io/tx/0xc48dc0d43ffb43891d8c3ad7bcf05f11465518a2610869b20b0b4ccb61497634 + #[test] + fn settlement() { + let calldata = hex!( + " + 13d79a0b + 0000000000000000000000000000000000000000000000000000000000000080 + 0000000000000000000000000000000000000000000000000000000000000120 + 00000000000000000000000000000000000000000000000000000000000001c0 + 00000000000000000000000000000000000000000000000000000000000003c0 + 0000000000000000000000000000000000000000000000000000000000000004 + 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 + 000000000000000000000000c52fafdc900cb92ae01e6e4f8979af7f436e2eb2 + 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 + 000000000000000000000000c52fafdc900cb92ae01e6e4f8979af7f436e2eb2 + 0000000000000000000000000000000000000000000000000000000000000004 + 0000000000000000000000000000000000000000000000010000000000000000 + 0000000000000000000000000000000000000000000000000023f003f04b5a92 + 0000000000000000000000000000000000000000000000f676b2510588839eb6 + 00000000000000000000000000000000000000000000000022b1c8c1227a0000 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000020 + 0000000000000000000000000000000000000000000000000000000000000002 + 0000000000000000000000000000000000000000000000000000000000000003 + 0000000000000000000000009398a8948e1ac88432a509b218f9ac8cf9cecdee + 00000000000000000000000000000000000000000000000022b1c8c1227a0000 + 0000000000000000000000000000000000000000000000f11f89f17728c24a5c + 00000000000000000000000000000000000000000000000000000000ffffffff + ae848d463143d030dd3875930a875de6417f58adc5dde0e94d485706d34b4797 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000040 + 00000000000000000000000000000000000000000000000022b1c8c1227a0000 + 0000000000000000000000000000000000000000000000000000000000000160 + 0000000000000000000000000000000000000000000000000000000000000028 + 40a50cf069e992aa4536211b23f286ef8875218740a50cf069e992aa4536211b + 23f286ef88752187000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000140 + 00000000000000000000000000000000000000000000000000000000000004c0 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000020 + 00000000000000000000000040a50cf069e992aa4536211b23f286ef88752187 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000004 + 4c84c1c800000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000003 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000140 + 0000000000000000000000000000000000000000000000000000000000000220 + 00000000000000000000000000000000be48a3000b818e9615d85aacfed4ca97 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 000000000000000000000000000000000000000000000000000000000000004f + 0000000101010000000000000000063a508037887d5d5aca4b69771e56f3c92c + 20840dd09188a65771d8000000000000002c400000000000000001c02aaa39b2 + 23fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000 + 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000044 + a9059cbb000000000000000000000000c88deb1ce0bc4a4306b7f20be2abd28a + d3a5c8d10000000000000000000000000000000000000000000000001c5efcf2 + c41873fd00000000000000000000000000000000000000000000000000000000 + 000000000000000000000000c88deb1ce0bc4a4306b7f20be2abd28ad3a5c8d1 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 00000000000000000000000000000000000000000000000000000000000000a4 + 022c0d9f00000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000000000ca2b0dae6c + b90dbc4b0000000000000000000000009008d19f58aabd9ed0d60971565aa851 + 0560ab4100000000000000000000000000000000000000000000000000000000 + 0000008000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000084120c" + ) + .to_vec(); + + let domain_separator = eth::DomainSeparator(hex!( + "c078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943" + )); + let transaction = super::transaction::Transaction::new( + &domain::eth::Transaction { + input: calldata.into(), + ..Default::default() + }, + &domain_separator, + ) + .unwrap(); + + let order_uid = transaction.trades[0].uid; + + let auction = super::Auction { + // prices read from https://solver-instances.s3.eu-central-1.amazonaws.com/prod/mainnet/legacy/8655372.json + prices: auction::Prices::from([ + ( + eth::TokenAddress(eth::H160::from_slice(&hex!( + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + ))), + auction::Price::new(eth::U256::from(1000000000000000000u128).into()).unwrap(), + ), + ( + eth::TokenAddress(eth::H160::from_slice(&hex!( + "c52fafdc900cb92ae01e6e4f8979af7f436e2eb2" + ))), + auction::Price::new(eth::U256::from(537359915436704u128).into()).unwrap(), + ), + ]), + surplus_capturing_jit_order_owners: Default::default(), + id: 0, + orders: HashMap::from([(order_uid, vec![])]), + }; + + let trade = super::trade::Trade::new(transaction.trades[0].clone(), &auction, 0); + + // surplus (score) read from https://api.cow.fi/mainnet/api/v1/solver_competition/by_tx_hash/0xc48dc0d43ffb43891d8c3ad7bcf05f11465518a2610869b20b0b4ccb61497634 + assert_eq!( + trade.surplus_in_ether(&auction.prices).unwrap().0, + eth::U256::from(52937525819789126u128) + ); + // fee read from "executedSurplusFee" https://api.cow.fi/mainnet/api/v1/orders/0x10dab31217bb6cc2ace0fe601c15d342f7626a1ee5ef0495449800e73156998740a50cf069e992aa4536211b23f286ef88752187ffffffff + assert_eq!( + trade.fee_in_ether(&auction.prices).unwrap().0, + eth::U256::from(6890975030480504u128) + ); + } + + // https://etherscan.io/tx/0x688508eb59bd20dc8c0d7c0c0b01200865822c889f0fcef10113e28202783243 + #[test] + fn settlement_with_protocol_fee() { + let calldata = hex!( + " + 13d79a0b + 0000000000000000000000000000000000000000000000000000000000000080 + 0000000000000000000000000000000000000000000000000000000000000120 + 00000000000000000000000000000000000000000000000000000000000001c0 + 00000000000000000000000000000000000000000000000000000000000003e0 + 0000000000000000000000000000000000000000000000000000000000000004 + 000000000000000000000000056fd409e1d7a124bd7017459dfea2f387b6d5cd + 000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7 + 000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7 + 000000000000000000000000056fd409e1d7a124bd7017459dfea2f387b6d5cd + 0000000000000000000000000000000000000000000000000000000000000004 + 00000000000000000000000000000000000000000000000000000019b743b945 + 0000000000000000000000000000000000000000000000000000000000a87cf3 + 0000000000000000000000000000000000000000000000000000000000a87c7c + 00000000000000000000000000000000000000000000000000000019b8b69873 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000020 + 0000000000000000000000000000000000000000000000000000000000000002 + 0000000000000000000000000000000000000000000000000000000000000003 + 000000000000000000000000f87da2093abee9b13a6f89671e4c3a3f80b42767 + 0000000000000000000000000000000000000000000000000000006d6e2edc00 + 0000000000000000000000000000000000000000000000000000000002cccdff + 000000000000000000000000000000000000000000000000000000006799c219 + 2d365e5affcfa62cf1067b845add9c01bedcb2fc5d7a37442d2177262af26a0c + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000002 + 00000000000000000000000000000000000000000000000000000019b8b69873 + 0000000000000000000000000000000000000000000000000000000000000160 + 0000000000000000000000000000000000000000000000000000000000000041 + e2ef661343676f9f4371ce809f728bb39a406f47835ee2b0104a8a1f340409ae + 742dfe47fe469c024dc2fb7f80b99878b35985d66312856a8b5dcf5de4b069ee + 1c00000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000080 + 0000000000000000000000000000000000000000000000000000000000000520 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000003 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000140 + 00000000000000000000000000000000000000000000000000000000000002e0 + 000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000044 + 095ea7b3000000000000000000000000e592427a0aece92de3edee1f18e0157c + 05861564ffffffffffffffffffffffffffffffffffffffffffffffffffffffff + ffffffff00000000000000000000000000000000000000000000000000000000 + 000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000104 + db3e2198000000000000000000000000dac17f958d2ee523a2206206994597c1 + 3d831ec7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce + 3606eb4800000000000000000000000000000000000000000000000000000000 + 000001f40000000000000000000000009008d19f58aabd9ed0d60971565aa851 + 0560ab4100000000000000000000000000000000000000000000000000000000 + 66abb94e00000000000000000000000000000000000000000000000000000019 + b4b64b9b00000000000000000000000000000000000000000000000000000019 + bdd90a1800000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000104 + db3e2198000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce + 3606eb48000000000000000000000000056fd409e1d7a124bd7017459dfea2f3 + 87b6d5cd00000000000000000000000000000000000000000000000000000000 + 000001f40000000000000000000000009008d19f58aabd9ed0d60971565aa851 + 0560ab4100000000000000000000000000000000000000000000000000000000 + 66abb94e00000000000000000000000000000000000000000000000000000000 + 00a87cf300000000000000000000000000000000000000000000000000000019 + bb4af52700000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000008c912c" + ) + .to_vec(); + + let domain_separator = eth::DomainSeparator(hex!( + "c078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943" + )); + let transaction = super::transaction::Transaction::new( + &domain::eth::Transaction { + input: calldata.into(), + ..Default::default() + }, + &domain_separator, + ) + .unwrap(); + + let prices: auction::Prices = From::from([ + ( + eth::TokenAddress(eth::H160::from_slice(&hex!( + "dac17f958d2ee523a2206206994597c13d831ec7" + ))), + auction::Price::new(eth::U256::from(321341140475275961528483840u128).into()) + .unwrap(), + ), + ( + eth::TokenAddress(eth::H160::from_slice(&hex!( + "056fd409e1d7a124bd7017459dfea2f387b6d5cd" + ))), + auction::Price::new(eth::U256::from(3177764302250520038326415654912u128).into()) + .unwrap(), + ), + ]); + + let order_uid = transaction.trades[0].uid; + let auction = super::Auction { + prices, + surplus_capturing_jit_order_owners: Default::default(), + id: 0, + orders: HashMap::from([( + order_uid, + vec![domain::fee::Policy::Surplus { + factor: 0.5f64.try_into().unwrap(), + max_volume_factor: 0.01.try_into().unwrap(), + }], + )]), + }; + let trade = super::trade::Trade::new(transaction.trades[0].clone(), &auction, 0); + + assert_eq!( + trade.surplus_in_ether(&auction.prices).unwrap().0, + eth::U256::from(384509480572312u128) + ); + + assert_eq!( + trade.score(&auction).unwrap().0, + eth::U256::from(769018961144624u128) // 2 x surplus + ); + } + + // https://etherscan.io/tx/0x24ea2ea3d70db3e864935008d14170389bda124c786ca90dfb745278db9d24ee + #[test] + fn settlement_with_cow_amm() { + let calldata = hex!( + " + 13d79a0b + 0000000000000000000000000000000000000000000000000000000000000080 + 0000000000000000000000000000000000000000000000000000000000000120 + 00000000000000000000000000000000000000000000000000000000000001c0 + 0000000000000000000000000000000000000000000000000000000000000520 + 0000000000000000000000000000000000000000000000000000000000000004 + 000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 + 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 + 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 + 000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 + 0000000000000000000000000000000000000000000000000000000000000004 + 000000000000000000000000000000000000000000000000019a3146915f155e + 000000000000000000000000000000000000000000000000000000001270a05f + 000000000000000000000000000000000000000000000000000000001270a05f + 000000000000000000000000000000000000000000000000019b4a78844e21f2 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000020 + 0000000000000000000000000000000000000000000000000000000000000002 + 0000000000000000000000000000000000000000000000000000000000000003 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000000000000000000000000000019b4a78844e21f2 + 00000000000000000000000000000000000000000000000000000000126f1d1f + 0000000000000000000000000000000000000000000000000000000066c84917 + 362e5182440b52aa8fffe70a251550fbbcbca424740fe5a14f59bf0c1b06fe1d + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000042 + 000000000000000000000000000000000000000000000000019b4a78844e21f2 + 0000000000000000000000000000000000000000000000000000000000000160 + 0000000000000000000000000000000000000000000000000000000000000194 + f08d4dea369c456d26a3168ff0024b904f2d8b91000000000000000000000000 + c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000 + a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 000000000000000000000000019b4a78844e21f2000000000000000000000000 + 00000000000000000000000000000000126f1d1f000000000000000000000000 + 0000000000000000000000000000000066c84917362e5182440b52aa8fffe70a + 251550fbbcbca424740fe5a14f59bf0c1b06fe1d000000000000000000000000 + 0000000000000000000000000000000000000000f3b277728b3fee749481eb3e + 0b3b48980dbbab78658fc419025cb16eee346775000000000000000000000000 + 00000000000000000000000000000000000000015a28e9363bb942b639270062 + aa6bb295f434bcdfc42c97267bf003f272060dc95a28e9363bb942b639270062 + aa6bb295f434bcdfc42c97267bf003f272060dc9000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000160 + 0000000000000000000000000000000000000000000000000000000000000740 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000020 + 000000000000000000000000f08d4dea369c456d26a3168ff0024b904f2d8b91 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000024 + f14fcbc8bfb27bd6d0a9e23c8bbc1cc85596e1c0639265a3c0b46a72f850529d + 17bc1b5b00000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000020 + 0000000000000000000000009c05bdcc909c2b190837e8fe71619cf389598c2c + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000060 + 0000000000000000000000000000000000000000000000000000000000000504 + 3732900900000000000000000000000000000000000000000000000000000000 + 0000002000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000000000000000000000000000000000000000000000000000019b4a78 + 844e21f200000000000000000000000000000000000000000000000000000000 + 1270a05f00000000000000000000000000000000000000000000000000000000 + 66c848250000000000000000000000009008d19f58aabd9ed0d60971565aa851 + 0560ab41000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce + 3606eb4800000000000000000000000000000000000000000000000000000000 + 0000010000000000000000000000000000000000000000000000000000000000 + 0000008000000000000000000000000000000000000000000000000000000000 + 0000036000000000000000000000000000000000000000000000000000000000 + 0000038000000000000000000000000000000000000000000000000000000000 + 000003a000000000000000000000000000000000000000000000000000000000 + 000002c0000000000000000000000000000000000000000000000000019b4a78 + 844e21f20000000000000000000000000000000000004b905f8f54ec051e2802 + 2bde09620000000000000000000000000000000000004b905f8f54ec051e2802 + 2bde09620000000000000000000000000000000000004b8f0f47c8c28d464eaa + a8087e530000000000000000000000009008d19f58aabd9ed0d60971565aa851 + 0560ab410000000000000000000000009008d19f58aabd9ed0d60971565aa851 + 0560ab4100000000000000000000000000000000000000000000000000000000 + 66c847e500000000000000000000000000000000000000000000000000000000 + 0000004000000000000000000000000000000000000000000000000000000000 + 0000000a00000000000000000000000000000000000000000000000000000000 + 0000006400000000000000000000000000000000000000000000000000000000 + 000001f400000000000000000000000000000000000000000000000000000000 + 0000000a00000000000000000000000000000000000000000000000000000000 + 0000006400000000000000000000000000000000000000000000000000000000 + 000001f400000000000000000000000000000000000000000000000000000000 + 0000001900000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000024000000000000000000000000000000000000000000000000000000000 + 00000041abcdb567a3168a149e106f95473e0605b44540005c64336deb7a17e8 + 3d7275616e37d90b0431a7fe719c619db5103bab6054a120bd6b9e20d78a3b70 + 6a76d2841b000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000131000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000000008dd870" + ) + .to_vec(); + + let domain_separator = eth::DomainSeparator(hex!( + "c078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943" + )); + let transaction = super::transaction::Transaction::new( + &domain::eth::Transaction { + input: calldata.into(), + ..Default::default() + }, + &domain_separator, + ) + .unwrap(); + + let prices: auction::Prices = From::from([ + ( + eth::TokenAddress(eth::H160::from_slice(&hex!( + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + ))), + auction::Price::new(eth::U256::from(374263465721452989998170112u128).into()) + .unwrap(), + ), + ( + eth::TokenAddress(eth::H160::from_slice(&hex!( + "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + ))), + auction::Price::new(eth::U256::from(1000000000000000000u128).into()).unwrap(), + ), + ]); + + let auction = super::Auction { + prices, + surplus_capturing_jit_order_owners: HashSet::from([eth::Address( + eth::H160::from_slice(&hex!("f08d4dea369c456d26a3168ff0024b904f2d8b91")), + )]), + id: 0, + orders: Default::default(), + }; + let trade = super::trade::Trade::new(transaction.trades[0].clone(), &auction, 0); + println!("{}", trade.uid().owner()); + assert_eq!( + trade.surplus_in_ether(&auction.prices).unwrap().0, + eth::U256::from(37102982937761u128) + ); + } +} diff --git a/crates/autopilot/src/domain/settlement/solution/mod.rs b/crates/autopilot/src/domain/settlement/solution/mod.rs deleted file mode 100644 index 8b4d346a61..0000000000 --- a/crates/autopilot/src/domain/settlement/solution/mod.rs +++ /dev/null @@ -1,468 +0,0 @@ -//! This module defines Solution as originated from a mined transaction -//! calldata. - -use { - self::trade::Trade, - crate::domain::{ - auction::{self}, - competition, - eth, - }, -}; - -mod tokenized; -mod trade; -pub use error::Error; -use { - crate::{domain, domain::fee}, - num::Saturating, - std::collections::HashMap, -}; - -/// A solution that was executed on-chain. -/// -/// Contains only data observable on-chain. No off-chain data is used to create -/// this struct. -/// -/// Referenced as [`settlement::Solution`] in the codebase. -#[derive(Debug, Clone)] -pub struct Solution { - trades: Vec, -} - -impl Solution { - /// CIP38 score calculation - pub fn score(&self, auction: &super::Auction) -> Result { - Ok(competition::Score::new( - self.trades - .iter() - .map(|trade| trade.score(auction)) - .sum::>()?, - )?) - } - - /// Total surplus for all trades in the solution. - /// - /// Always returns a value, even if some trades have incomplete surplus - /// calculation. - pub fn native_surplus(&self, auction: &super::Auction) -> eth::Ether { - self.trades - .iter() - .map(|trade| { - trade.native_surplus(auction).unwrap_or_else(|err| { - tracing::warn!( - ?err, - "possible incomplete surplus calculation for trade {}", - trade.order_uid() - ); - num::zero() - }) - }) - .sum() - } - - /// Total fee for all trades in the solution. - /// - /// Always returns a value, even if some trades have incomplete fee - /// calculation. - pub fn native_fee(&self, prices: &auction::Prices) -> eth::Ether { - self.trades - .iter() - .map(|trade| { - trade.native_fee(prices).unwrap_or_else(|err| { - tracing::warn!( - ?err, - "possible incomplete fee calculation for trade {}", - trade.order_uid() - ); - num::zero() - }) - }) - .sum() - } - - /// Returns fees breakdown for each order in the solution. - pub fn fees(&self, auction: &super::Auction) -> HashMap> { - self.trades - .iter() - .map(|trade| { - (*trade.order_uid(), { - let total = trade.total_fee_in_sell_token(); - let protocol = trade.protocol_fees_in_sell_token(auction); - match (total, protocol) { - (Ok(total), Ok(protocol)) => { - let network = - total.saturating_sub(protocol.iter().map(|(fee, _)| *fee).sum()); - Some(ExecutedFee { protocol, network }) - } - _ => None, - } - }) - }) - .collect() - } - - pub fn new( - calldata: ð::Calldata, - domain_separator: ð::DomainSeparator, - ) -> Result { - let tokenized::Tokenized { - tokens, - clearing_prices, - trades: decoded_trades, - interactions: _interactions, - } = tokenized::Tokenized::new(calldata)?; - - let mut trades = Vec::with_capacity(decoded_trades.len()); - for trade in decoded_trades { - let flags = tokenized::TradeFlags(trade.8); - let sell_token_index = trade.0.as_usize(); - let buy_token_index = trade.1.as_usize(); - let sell_token = tokens[sell_token_index]; - let buy_token = tokens[buy_token_index]; - let uniform_sell_token_index = tokens - .iter() - .position(|token| token == &sell_token) - .unwrap(); - let uniform_buy_token_index = - tokens.iter().position(|token| token == &buy_token).unwrap(); - trades.push(trade::Trade::new( - tokenized::order_uid(&trade, &tokens, domain_separator) - .map_err(Error::OrderUidRecover)?, - eth::Asset { - token: sell_token.into(), - amount: trade.3.into(), - }, - eth::Asset { - token: buy_token.into(), - amount: trade.4.into(), - }, - flags.side(), - trade.9.into(), - trade::Prices { - uniform: trade::ClearingPrices { - sell: clearing_prices[uniform_sell_token_index].into(), - buy: clearing_prices[uniform_buy_token_index].into(), - }, - custom: trade::ClearingPrices { - sell: clearing_prices[sell_token_index].into(), - buy: clearing_prices[buy_token_index].into(), - }, - }, - )); - } - - Ok(Self { trades }) - } -} - -pub mod error { - use super::*; - - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - Decoding(#[from] tokenized::error::Decoding), - #[error("failed to recover order uid {0}")] - OrderUidRecover(tokenized::error::Uid), - } - - #[derive(Debug, thiserror::Error)] - pub enum Score { - /// Per CIP38, zero score solutions are rejected. - #[error(transparent)] - Zero(#[from] competition::ZeroScore), - /// Score calculation requires native prices for all tokens in the - /// solution, so that the surplus can be normalized to native currency. - #[error("missing native price for token {0:?}")] - MissingPrice(eth::TokenAddress), - #[error(transparent)] - Math(trade::error::Math), - } - - impl From for Score { - fn from(err: trade::Error) -> Self { - match err { - trade::Error::MissingPrice(token) => Self::MissingPrice(token), - trade::Error::Math(err) => Self::Math(err), - } - } - } -} - -/// Fee per trade in a solution. These fees are taken for the execution of the -/// trade. -#[derive(Debug, Clone)] -pub struct ExecutedFee { - /// Gas fee spent to bring the order onchain - pub network: eth::SellTokenAmount, - /// Breakdown of protocol fees. Executed protocol fees are in the same order - /// as policies are defined for an order. - pub protocol: Vec<(eth::SellTokenAmount, fee::Policy)>, -} - -impl ExecutedFee { - /// Total fee paid for the trade. - pub fn total(&self) -> eth::SellTokenAmount { - self.network + self.protocol.iter().map(|(fee, _)| *fee).sum() - } -} - -#[cfg(test)] -mod tests { - use { - crate::{ - domain, - domain::{auction, eth}, - }, - hex_literal::hex, - std::collections::HashMap, - }; - - // https://etherscan.io/tx/0xc48dc0d43ffb43891d8c3ad7bcf05f11465518a2610869b20b0b4ccb61497634 - #[test] - fn settlement() { - let calldata = hex!( - " - 13d79a0b - 0000000000000000000000000000000000000000000000000000000000000080 - 0000000000000000000000000000000000000000000000000000000000000120 - 00000000000000000000000000000000000000000000000000000000000001c0 - 00000000000000000000000000000000000000000000000000000000000003c0 - 0000000000000000000000000000000000000000000000000000000000000004 - 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - 000000000000000000000000c52fafdc900cb92ae01e6e4f8979af7f436e2eb2 - 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - 000000000000000000000000c52fafdc900cb92ae01e6e4f8979af7f436e2eb2 - 0000000000000000000000000000000000000000000000000000000000000004 - 0000000000000000000000000000000000000000000000010000000000000000 - 0000000000000000000000000000000000000000000000000023f003f04b5a92 - 0000000000000000000000000000000000000000000000f676b2510588839eb6 - 00000000000000000000000000000000000000000000000022b1c8c1227a0000 - 0000000000000000000000000000000000000000000000000000000000000001 - 0000000000000000000000000000000000000000000000000000000000000020 - 0000000000000000000000000000000000000000000000000000000000000002 - 0000000000000000000000000000000000000000000000000000000000000003 - 0000000000000000000000009398a8948e1ac88432a509b218f9ac8cf9cecdee - 00000000000000000000000000000000000000000000000022b1c8c1227a0000 - 0000000000000000000000000000000000000000000000f11f89f17728c24a5c - 00000000000000000000000000000000000000000000000000000000ffffffff - ae848d463143d030dd3875930a875de6417f58adc5dde0e94d485706d34b4797 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000040 - 00000000000000000000000000000000000000000000000022b1c8c1227a0000 - 0000000000000000000000000000000000000000000000000000000000000160 - 0000000000000000000000000000000000000000000000000000000000000028 - 40a50cf069e992aa4536211b23f286ef8875218740a50cf069e992aa4536211b - 23f286ef88752187000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000140 - 00000000000000000000000000000000000000000000000000000000000004c0 - 0000000000000000000000000000000000000000000000000000000000000001 - 0000000000000000000000000000000000000000000000000000000000000020 - 00000000000000000000000040a50cf069e992aa4536211b23f286ef88752187 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000004 - 4c84c1c800000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000003 - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000140 - 0000000000000000000000000000000000000000000000000000000000000220 - 00000000000000000000000000000000be48a3000b818e9615d85aacfed4ca97 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000060 - 000000000000000000000000000000000000000000000000000000000000004f - 0000000101010000000000000000063a508037887d5d5aca4b69771e56f3c92c - 20840dd09188a65771d8000000000000002c400000000000000001c02aaa39b2 - 23fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000 - 000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000044 - a9059cbb000000000000000000000000c88deb1ce0bc4a4306b7f20be2abd28a - d3a5c8d10000000000000000000000000000000000000000000000001c5efcf2 - c41873fd00000000000000000000000000000000000000000000000000000000 - 000000000000000000000000c88deb1ce0bc4a4306b7f20be2abd28ad3a5c8d1 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000060 - 00000000000000000000000000000000000000000000000000000000000000a4 - 022c0d9f00000000000000000000000000000000000000000000000000000000 - 000000000000000000000000000000000000000000000000000000ca2b0dae6c - b90dbc4b0000000000000000000000009008d19f58aabd9ed0d60971565aa851 - 0560ab4100000000000000000000000000000000000000000000000000000000 - 0000008000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 000000000084120c" - ) - .to_vec(); - - let domain_separator = eth::DomainSeparator(hex!( - "c078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943" - )); - let solution = super::Solution::new(&calldata.into(), &domain_separator).unwrap(); - assert_eq!(solution.trades.len(), 1); - - // prices read from https://solver-instances.s3.eu-central-1.amazonaws.com/prod/mainnet/legacy/8655372.json - let prices: auction::Prices = From::from([ - ( - eth::TokenAddress(eth::H160::from_slice(&hex!( - "c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" - ))), - auction::Price::new(eth::U256::from(1000000000000000000u128).into()).unwrap(), - ), - ( - eth::TokenAddress(eth::H160::from_slice(&hex!( - "c52fafdc900cb92ae01e6e4f8979af7f436e2eb2" - ))), - auction::Price::new(eth::U256::from(537359915436704u128).into()).unwrap(), - ), - ]); - - let auction = super::super::Auction { - prices, - surplus_capturing_jit_order_owners: Default::default(), - id: 0, - orders: HashMap::from([(domain::OrderUid(hex!("10dab31217bb6cc2ace0fe601c15d342f7626a1ee5ef0495449800e73156998740a50cf069e992aa4536211b23f286ef88752187ffffffff")), vec![])]), - }; - - // surplus (score) read from https://api.cow.fi/mainnet/api/v1/solver_competition/by_tx_hash/0xc48dc0d43ffb43891d8c3ad7bcf05f11465518a2610869b20b0b4ccb61497634 - assert_eq!( - solution.native_surplus(&auction).0, - eth::U256::from(52937525819789126u128) - ); - // fee read from "executedSurplusFee" https://api.cow.fi/mainnet/api/v1/orders/0x10dab31217bb6cc2ace0fe601c15d342f7626a1ee5ef0495449800e73156998740a50cf069e992aa4536211b23f286ef88752187ffffffff - assert_eq!( - solution.native_fee(&auction.prices).0, - eth::U256::from(6890975030480504u128) - ); - } - - // https://etherscan.io/tx/0x688508eb59bd20dc8c0d7c0c0b01200865822c889f0fcef10113e28202783243 - #[test] - fn settlement_with_protocol_fee() { - let calldata = hex!( - " - 13d79a0b - 0000000000000000000000000000000000000000000000000000000000000080 - 0000000000000000000000000000000000000000000000000000000000000120 - 00000000000000000000000000000000000000000000000000000000000001c0 - 00000000000000000000000000000000000000000000000000000000000003e0 - 0000000000000000000000000000000000000000000000000000000000000004 - 000000000000000000000000056fd409e1d7a124bd7017459dfea2f387b6d5cd - 000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7 - 000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7 - 000000000000000000000000056fd409e1d7a124bd7017459dfea2f387b6d5cd - 0000000000000000000000000000000000000000000000000000000000000004 - 00000000000000000000000000000000000000000000000000000019b743b945 - 0000000000000000000000000000000000000000000000000000000000a87cf3 - 0000000000000000000000000000000000000000000000000000000000a87c7c - 00000000000000000000000000000000000000000000000000000019b8b69873 - 0000000000000000000000000000000000000000000000000000000000000001 - 0000000000000000000000000000000000000000000000000000000000000020 - 0000000000000000000000000000000000000000000000000000000000000002 - 0000000000000000000000000000000000000000000000000000000000000003 - 000000000000000000000000f87da2093abee9b13a6f89671e4c3a3f80b42767 - 0000000000000000000000000000000000000000000000000000006d6e2edc00 - 0000000000000000000000000000000000000000000000000000000002cccdff - 000000000000000000000000000000000000000000000000000000006799c219 - 2d365e5affcfa62cf1067b845add9c01bedcb2fc5d7a37442d2177262af26a0c - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000002 - 00000000000000000000000000000000000000000000000000000019b8b69873 - 0000000000000000000000000000000000000000000000000000000000000160 - 0000000000000000000000000000000000000000000000000000000000000041 - e2ef661343676f9f4371ce809f728bb39a406f47835ee2b0104a8a1f340409ae - 742dfe47fe469c024dc2fb7f80b99878b35985d66312856a8b5dcf5de4b069ee - 1c00000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000080 - 0000000000000000000000000000000000000000000000000000000000000520 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000003 - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000140 - 00000000000000000000000000000000000000000000000000000000000002e0 - 000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000044 - 095ea7b3000000000000000000000000e592427a0aece92de3edee1f18e0157c - 05861564ffffffffffffffffffffffffffffffffffffffffffffffffffffffff - ffffffff00000000000000000000000000000000000000000000000000000000 - 000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000104 - db3e2198000000000000000000000000dac17f958d2ee523a2206206994597c1 - 3d831ec7000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce - 3606eb4800000000000000000000000000000000000000000000000000000000 - 000001f40000000000000000000000009008d19f58aabd9ed0d60971565aa851 - 0560ab4100000000000000000000000000000000000000000000000000000000 - 66abb94e00000000000000000000000000000000000000000000000000000019 - b4b64b9b00000000000000000000000000000000000000000000000000000019 - bdd90a1800000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000060 - 0000000000000000000000000000000000000000000000000000000000000104 - db3e2198000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce - 3606eb48000000000000000000000000056fd409e1d7a124bd7017459dfea2f3 - 87b6d5cd00000000000000000000000000000000000000000000000000000000 - 000001f40000000000000000000000009008d19f58aabd9ed0d60971565aa851 - 0560ab4100000000000000000000000000000000000000000000000000000000 - 66abb94e00000000000000000000000000000000000000000000000000000000 - 00a87cf300000000000000000000000000000000000000000000000000000019 - bb4af52700000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 00000000008c912c" - ) - .to_vec(); - - let domain_separator = eth::DomainSeparator(hex!( - "c078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943" - )); - let solution = super::Solution::new(&calldata.into(), &domain_separator).unwrap(); - assert_eq!(solution.trades.len(), 1); - - let prices: auction::Prices = From::from([ - ( - eth::TokenAddress(eth::H160::from_slice(&hex!( - "dac17f958d2ee523a2206206994597c13d831ec7" - ))), - auction::Price::new(eth::U256::from(321341140475275961528483840u128).into()) - .unwrap(), - ), - ( - eth::TokenAddress(eth::H160::from_slice(&hex!( - "056fd409e1d7a124bd7017459dfea2f387b6d5cd" - ))), - auction::Price::new(eth::U256::from(3177764302250520038326415654912u128).into()) - .unwrap(), - ), - ]); - - let auction = super::super::Auction { - prices, - surplus_capturing_jit_order_owners: Default::default(), - id: 0, - orders: HashMap::from([(domain::OrderUid(hex!("c6a81144bc822569a0752c7a537fa9cbbf6344cb187ce0ff15a534b571e277eaf87da2093abee9b13a6f89671e4c3a3f80b427676799c219")), vec![domain::fee::Policy::Surplus { - factor: 0.5f64.try_into().unwrap(), - max_volume_factor: 0.01.try_into().unwrap(), - }])]), - }; - - assert_eq!( - solution.native_surplus(&auction).0, - eth::U256::from(384509480572312u128) - ); - - assert_eq!( - solution.score(&auction).unwrap().get().0, - eth::U256::from(769018961144624u128) // 2 x surplus - ); - } -} diff --git a/crates/autopilot/src/domain/settlement/solution/trade.rs b/crates/autopilot/src/domain/settlement/trade/math.rs similarity index 91% rename from crates/autopilot/src/domain/settlement/solution/trade.rs rename to crates/autopilot/src/domain/settlement/trade/math.rs index 2a8ed7752c..e90ce2b1df 100644 --- a/crates/autopilot/src/domain/settlement/solution/trade.rs +++ b/crates/autopilot/src/domain/settlement/trade/math.rs @@ -6,7 +6,10 @@ use { auction::{self, order}, eth, fee, - settlement, + settlement::{ + transaction::{ClearingPrices, Prices}, + {self}, + }, }, util::conv::U256Ext, }, @@ -14,12 +17,11 @@ use { num::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub}, }; -/// A single trade executed on-chain, as part of the [`settlement::Solution`]. -/// -/// Referenced as [`settlement::solution::Trade`] in the codebase. +/// A trade containing bare minimum of onchain information required to calculate +/// the surplus, fees and score. #[derive(Debug, Clone)] -pub struct Trade { - order_uid: domain::OrderUid, +pub(super) struct Trade { + uid: domain::OrderUid, sell: eth::Asset, buy: eth::Asset, side: order::Side, @@ -28,33 +30,11 @@ pub struct Trade { } impl Trade { - pub fn new( - order_uid: domain::OrderUid, - sell: eth::Asset, - buy: eth::Asset, - side: order::Side, - executed: order::TargetAmount, - prices: Prices, - ) -> Self { - Self { - order_uid, - sell, - buy, - side, - executed, - prices, - } - } - - pub fn order_uid(&self) -> &domain::OrderUid { - &self.order_uid - } - /// CIP38 score defined as surplus + protocol fee /// /// Denominated in NATIVE token pub fn score(&self, auction: &settlement::Auction) -> Result { - Ok(self.native_surplus(auction)? + self.native_protocol_fee(auction)?) + Ok(self.surplus_in_ether(&auction.prices)? + self.protocol_fee_in_ether(auction)?) } /// A general surplus function. @@ -118,16 +98,9 @@ impl Trade { /// Surplus based on custom clearing prices returns the surplus after all /// fees have been applied. - /// - /// Denominated in NATIVE token - pub fn native_surplus(&self, auction: &settlement::Auction) -> Result { - if !auction.is_surplus_capturing(&self.order_uid) { - return Ok(Zero::zero()); - } - + pub fn surplus_in_ether(&self, prices: &auction::Prices) -> Result { let surplus = self.surplus_over_limit_price()?; - let price = auction - .prices + let price = prices .get(&surplus.token) .ok_or(Error::MissingPrice(surplus.token))?; @@ -136,10 +109,8 @@ impl Trade { /// Total fee (protocol fee + network fee). Equal to a surplus difference /// before and after applying the fees. - /// - /// Denominated in NATIVE token - pub fn native_fee(&self, prices: &auction::Prices) -> Result { - let total_fee = self.total_fee_in_sell_token()?; + pub fn fee_in_ether(&self, prices: &auction::Prices) -> Result { + let total_fee = self.fee_in_sell_token()?; let price = prices .get(&self.sell.token) .ok_or(Error::MissingPrice(self.sell.token))?; @@ -162,9 +133,7 @@ impl Trade { /// Total fee (protocol fee + network fee). Equal to a surplus difference /// before and after applying the fees. - /// - /// Denominated in SELL token - pub fn total_fee_in_sell_token(&self) -> Result { + pub fn fee_in_sell_token(&self) -> Result { let fee = self.fee()?; self.fee_into_sell_token(fee.amount) } @@ -186,8 +155,6 @@ impl Trade { } /// Protocol fees are defined by fee policies attached to the order. - /// - /// Denominated in SELL token pub fn protocol_fees_in_sell_token( &self, auction: &settlement::Auction, @@ -207,7 +174,7 @@ impl Trade { ) -> Result, Error> { let policies = auction .orders - .get(&self.order_uid) + .get(&self.uid) .map(|value| value.as_slice()) .unwrap_or_default(); let mut current_trade = self.clone(); @@ -456,9 +423,7 @@ impl Trade { } /// Protocol fee is defined by fee policies attached to the order. - /// - /// Denominated in NATIVE token - fn native_protocol_fee(&self, auction: &settlement::Auction) -> Result { + fn protocol_fee_in_ether(&self, auction: &settlement::Auction) -> Result { self.protocol_fees(auction)? .into_iter() .map(|(fee, _)| { @@ -479,26 +444,12 @@ impl Trade { } } -#[derive(Debug, Clone)] -pub struct Prices { - pub uniform: ClearingPrices, - /// Adjusted uniform prices to account for fees (gas cost and protocol fees) - pub custom: ClearingPrices, -} - #[derive(Clone, Debug)] pub struct PriceLimits { pub sell: eth::TokenAmount, pub buy: eth::TokenAmount, } -/// Uniform clearing prices at which the trade was executed. -#[derive(Debug, Clone, Copy)] -pub struct ClearingPrices { - pub sell: eth::U256, - pub buy: eth::U256, -} - /// This function adjusts quote amounts to directly compare them with the /// order's limits, ensuring a meaningful comparison for potential price /// improvements. It scales quote amounts when necessary, accounting for quote @@ -576,6 +527,40 @@ struct Quote { pub fee: eth::TokenAmount, } +impl From<&super::Fulfillment> for Trade { + fn from(fulfillment: &super::Fulfillment) -> Self { + Self { + uid: fulfillment.uid, + sell: fulfillment.sell, + buy: fulfillment.buy, + side: fulfillment.side, + executed: fulfillment.executed, + prices: fulfillment.prices, + } + } +} + +impl From<&super::Jit> for Trade { + fn from(jit: &super::Jit) -> Self { + Self { + uid: jit.uid, + sell: jit.sell, + buy: jit.buy, + side: jit.side, + executed: jit.executed, + prices: jit.prices, + } + } +} + +impl From<&super::Trade> for Trade { + fn from(trade: &super::Trade) -> Self { + match trade { + super::Trade::Fulfillment(fulfillment) => fulfillment.into(), + super::Trade::Jit(jit) => jit.into(), + } + } +} pub mod error { use crate::domain::eth; diff --git a/crates/autopilot/src/domain/settlement/trade/mod.rs b/crates/autopilot/src/domain/settlement/trade/mod.rs new file mode 100644 index 0000000000..57ce8d6d84 --- /dev/null +++ b/crates/autopilot/src/domain/settlement/trade/mod.rs @@ -0,0 +1,170 @@ +use { + super::{transaction, transaction::Prices}, + crate::domain::{ + self, + auction::{self, order}, + eth, + fee, + }, + bigdecimal::Zero, +}; + +mod math; + +/// Trade type evaluated in a context of an Auction. +#[derive(Clone, Debug)] +pub enum Trade { + /// A regular user order that exist in the associated Auction. + Fulfillment(Fulfillment), + /// JIT trades are not part of the orderbook and are created by solvers at + /// the time of settlement. + /// Note that user orders can also be classified as JIT orders if they are + /// settled outside of the Auction. + Jit(Jit), +} + +impl Trade { + /// UID of the order that was settled in this trade. + pub fn uid(&self) -> &domain::OrderUid { + match self { + Self::Fulfillment(trade) => &trade.uid, + Self::Jit(trade) => &trade.uid, + } + } + + /// Return JIT order if it's a JIT order. + pub fn as_jit(&self) -> Option<&Jit> { + match self { + Self::Fulfillment(_) => None, + Self::Jit(trade) => Some(trade), + } + } + + /// CIP38 score defined as surplus + protocol fee + pub fn score(&self, auction: &super::Auction) -> Result { + math::Trade::from(self).score(auction) + } + + /// Surplus of a trade. + pub fn surplus_in_ether(&self, prices: &auction::Prices) -> Result { + match self { + Self::Fulfillment(trade) => math::Trade::from(trade).surplus_in_ether(prices), + Self::Jit(trade) => { + if trade.surplus_capturing { + math::Trade::from(trade).surplus_in_ether(prices) + } else { + // JIT orders that are not surplus capturing have zero + // surplus, even if they settled at a better price than + // limit price. + Ok(eth::Ether::zero()) + } + } + } + } + + /// Total fee taken for the trade. + pub fn fee_in_ether(&self, prices: &auction::Prices) -> Result { + math::Trade::from(self).fee_in_ether(prices) + } + + /// Total fee (protocol fee + network fee). Equal to a surplus difference + /// before and after applying the fees. + pub fn fee_in_sell_token(&self) -> Result { + math::Trade::from(self).fee_in_sell_token() + } + + /// Protocol fees are defined by fee policies attached to the order. + pub fn protocol_fees_in_sell_token( + &self, + auction: &super::Auction, + ) -> Result, math::Error> { + math::Trade::from(self).protocol_fees_in_sell_token(auction) + } + + pub fn new(trade: transaction::EncodedTrade, auction: &super::Auction, created: u32) -> Self { + if auction.orders.contains_key(&trade.uid) { + Trade::Fulfillment(Fulfillment { + uid: trade.uid, + sell: trade.sell, + buy: trade.buy, + side: trade.side, + executed: trade.executed, + prices: trade.prices, + }) + } else { + // All orders that were settled outside of the auction are JIT orders. This + // includes regular JIT orders that the protocol is not aware of upfront, as + // well as user orders that were not listed in the auction during competition. + Trade::Jit(Jit { + uid: trade.uid, + sell: trade.sell, + buy: trade.buy, + side: trade.side, + receiver: trade.receiver, + valid_to: trade.valid_to, + app_data: trade.app_data, + fee_amount: trade.fee_amount, + sell_token_balance: trade.sell_token_balance, + buy_token_balance: trade.buy_token_balance, + partially_fillable: trade.partially_fillable, + signature: trade.signature, + executed: trade.executed, + prices: trade.prices, + created, + surplus_capturing: auction + .surplus_capturing_jit_order_owners + .contains(&trade.uid.owner()), + }) + } + } +} + +/// A trade filling an order that was part of the auction. +#[derive(Debug, Clone)] +pub struct Fulfillment { + uid: domain::OrderUid, + sell: eth::Asset, + buy: eth::Asset, + side: order::Side, + executed: order::TargetAmount, + prices: Prices, +} + +/// A trade filling an order that was not part of the auction. +#[derive(Debug, Clone)] +pub struct Jit { + pub uid: domain::OrderUid, + pub sell: eth::Asset, + pub buy: eth::Asset, + pub side: order::Side, + pub receiver: eth::Address, + pub valid_to: u32, + pub app_data: order::AppDataHash, + pub fee_amount: eth::TokenAmount, + pub sell_token_balance: order::SellTokenSource, + pub buy_token_balance: order::BuyTokenDestination, + pub partially_fillable: bool, + pub signature: order::Signature, + pub executed: order::TargetAmount, + pub prices: super::transaction::Prices, + pub created: u32, + pub surplus_capturing: bool, +} + +/// Fee per trade in a solution. These fees are taken for the execution of the +/// trade. +#[derive(Debug, Clone)] +pub struct ExecutedFee { + /// Gas fee spent to bring the order onchain + pub network: eth::SellTokenAmount, + /// Breakdown of protocol fees. Executed protocol fees are in the same order + /// as policies are defined for an order. + pub protocol: Vec<(eth::SellTokenAmount, fee::Policy)>, +} + +impl ExecutedFee { + /// Total fee paid for the trade. + pub fn total(&self) -> eth::SellTokenAmount { + self.network + self.protocol.iter().map(|(fee, _)| *fee).sum() + } +} diff --git a/crates/autopilot/src/domain/settlement/transaction.rs b/crates/autopilot/src/domain/settlement/transaction.rs deleted file mode 100644 index 1dff563491..0000000000 --- a/crates/autopilot/src/domain/settlement/transaction.rs +++ /dev/null @@ -1,56 +0,0 @@ -use { - crate::domain::{self, eth}, - anyhow::{anyhow, Context}, -}; - -/// An on-chain transaction that settled a solution. -#[derive(Debug, Clone)] -pub struct Transaction { - /// The hash of the transaction. - pub hash: eth::TxId, - /// The associated auction id. - pub auction_id: domain::auction::Id, - /// The address of the solver that submitted the transaction. - pub solver: eth::Address, - /// The block number of the block that contains the transaction. - pub block: eth::BlockNo, - /// The gas used by the transaction. - pub gas: eth::Gas, - /// The effective gas price of the transaction. - pub effective_gas_price: eth::EffectiveGasPrice, - /// The solution that was settled. - pub solution: domain::settlement::Solution, -} - -impl Transaction { - pub fn new( - transaction: ð::Transaction, - domain_separator: ð::DomainSeparator, - ) -> anyhow::Result { - /// Number of bytes that may be appended to the calldata to store an - /// auction id. - const META_DATA_LEN: usize = 8; - - let (data, metadata) = transaction - .input - .0 - .split_at(transaction.input.0.len() - META_DATA_LEN); - let metadata: Option<[u8; META_DATA_LEN]> = metadata.try_into().ok(); - let auction_id = metadata - .map(crate::domain::auction::Id::from_be_bytes) - .context("invalid metadata")?; - Ok(Self { - hash: transaction.hash, - auction_id, - solver: transaction.from, - block: transaction.block, - gas: transaction.gas, - effective_gas_price: transaction.effective_gas_price, - solution: domain::settlement::Solution::new( - &crate::util::Bytes(data.to_vec()), - domain_separator, - ) - .map_err(|err| anyhow!("solution build {}", err))?, - }) - } -} diff --git a/crates/autopilot/src/domain/settlement/transaction/mod.rs b/crates/autopilot/src/domain/settlement/transaction/mod.rs new file mode 100644 index 0000000000..4f7ca2e62e --- /dev/null +++ b/crates/autopilot/src/domain/settlement/transaction/mod.rs @@ -0,0 +1,163 @@ +use crate::{ + boundary, + domain::{self, auction::order, eth}, +}; + +mod tokenized; + +/// An on-chain transaction that settled a solution. +#[derive(Debug, Clone)] +pub struct Transaction { + /// The hash of the transaction. + pub hash: eth::TxId, + /// The associated auction id. + pub auction_id: domain::auction::Id, + /// The address of the solver that submitted the transaction. + pub solver: eth::Address, + /// The block number of the block that contains the transaction. + pub block: eth::BlockNo, + /// The timestamp of the block that contains the transaction. + pub timestamp: u32, + /// The gas used by the transaction. + pub gas: eth::Gas, + /// The effective gas price of the transaction. + pub gas_price: eth::EffectiveGasPrice, + /// Encoded trades that were settled by the transaction. + pub trades: Vec, +} + +impl Transaction { + pub fn new( + transaction: ð::Transaction, + domain_separator: ð::DomainSeparator, + ) -> Result { + /// Number of bytes that may be appended to the calldata to store an + /// auction id. + const META_DATA_LEN: usize = 8; + + let (data, metadata) = transaction + .input + .0 + .split_at(transaction.input.0.len() - META_DATA_LEN); + let metadata: Option<[u8; META_DATA_LEN]> = metadata.try_into().ok(); + let auction_id = metadata + .map(crate::domain::auction::Id::from_be_bytes) + .ok_or(Error::MissingAuctionId)?; + Ok(Self { + hash: transaction.hash, + auction_id, + solver: transaction.from, + block: transaction.block, + timestamp: transaction.timestamp, + gas: transaction.gas, + gas_price: transaction.gas_price, + trades: { + let tokenized::Tokenized { + tokens, + clearing_prices, + trades: decoded_trades, + interactions: _interactions, + } = tokenized::Tokenized::new(&crate::util::Bytes(data.to_vec()))?; + + let mut trades = Vec::with_capacity(decoded_trades.len()); + for trade in decoded_trades { + let flags = tokenized::TradeFlags(trade.8); + let sell_token_index = trade.0.as_usize(); + let buy_token_index = trade.1.as_usize(); + let sell_token = tokens[sell_token_index]; + let buy_token = tokens[buy_token_index]; + let uniform_sell_token_index = tokens + .iter() + .position(|token| token == &sell_token) + .unwrap(); + let uniform_buy_token_index = + tokens.iter().position(|token| token == &buy_token).unwrap(); + trades.push(EncodedTrade { + uid: tokenized::order_uid(&trade, &tokens, domain_separator) + .map_err(Error::OrderUidRecover)?, + sell: eth::Asset { + token: sell_token.into(), + amount: trade.3.into(), + }, + buy: eth::Asset { + token: buy_token.into(), + amount: trade.4.into(), + }, + side: flags.side(), + receiver: trade.2.into(), + valid_to: trade.5, + app_data: domain::auction::order::AppDataHash(trade.6 .0), + fee_amount: trade.7.into(), + sell_token_balance: flags.sell_token_balance().into(), + buy_token_balance: flags.buy_token_balance().into(), + partially_fillable: flags.partially_fillable(), + signature: (boundary::Signature::from_bytes( + flags.signing_scheme(), + &trade.10 .0, + ) + .map_err(Error::SignatureRecover)?) + .into(), + executed: trade.9.into(), + prices: Prices { + uniform: ClearingPrices { + sell: clearing_prices[uniform_sell_token_index].into(), + buy: clearing_prices[uniform_buy_token_index].into(), + }, + custom: ClearingPrices { + sell: clearing_prices[sell_token_index].into(), + buy: clearing_prices[buy_token_index].into(), + }, + }, + }) + } + trades + }, + }) + } +} + +/// Trade containing onchain observable data specific to a settlement +/// transaction. +#[derive(Debug, Clone)] +pub struct EncodedTrade { + pub uid: domain::OrderUid, + pub sell: eth::Asset, + pub buy: eth::Asset, + pub side: order::Side, + pub receiver: eth::Address, + pub valid_to: u32, + pub app_data: order::AppDataHash, + pub fee_amount: eth::TokenAmount, + pub sell_token_balance: order::SellTokenSource, + pub buy_token_balance: order::BuyTokenDestination, + pub partially_fillable: bool, + pub signature: order::Signature, + pub executed: order::TargetAmount, + pub prices: Prices, +} + +#[derive(Debug, Copy, Clone)] +pub struct Prices { + pub uniform: ClearingPrices, + /// Adjusted uniform prices to account for fees (gas cost and protocol fees) + pub custom: ClearingPrices, +} + +/// Uniform clearing prices at which the trade was executed. +#[derive(Debug, Clone, Copy)] +pub struct ClearingPrices { + pub sell: eth::U256, + pub buy: eth::U256, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("missing auction id")] + MissingAuctionId, + #[error(transparent)] + Decoding(#[from] tokenized::error::Decoding), + #[error("failed to recover order uid {0}")] + OrderUidRecover(tokenized::error::Uid), + #[error("failed to recover signature {0}")] + SignatureRecover(#[source] anyhow::Error), +} diff --git a/crates/autopilot/src/domain/settlement/solution/tokenized.rs b/crates/autopilot/src/domain/settlement/transaction/tokenized.rs similarity index 100% rename from crates/autopilot/src/domain/settlement/solution/tokenized.rs rename to crates/autopilot/src/domain/settlement/transaction/tokenized.rs diff --git a/crates/autopilot/src/infra/blockchain/mod.rs b/crates/autopilot/src/infra/blockchain/mod.rs index f0fccee7f1..90d0b9b3e2 100644 --- a/crates/autopilot/src/infra/blockchain/mod.rs +++ b/crates/autopilot/src/infra/blockchain/mod.rs @@ -126,13 +126,26 @@ impl Ethereum { )?; let transaction = transaction.ok_or(Error::TransactionNotFound)?; let receipt = receipt.ok_or(Error::TransactionNotFound)?; - into_domain(transaction, receipt).map_err(Error::IncompleteTransactionData) + let block_hash = + receipt + .block_hash + .ok_or(Error::IncompleteTransactionData(anyhow::anyhow!( + "missing block_hash" + )))?; + let block = self + .web3 + .eth() + .block(block_hash.into()) + .await? + .ok_or(Error::TransactionNotFound)?; + into_domain(transaction, receipt, block.timestamp).map_err(Error::IncompleteTransactionData) } } fn into_domain( transaction: web3::types::Transaction, receipt: web3::types::TransactionReceipt, + timestamp: U256, ) -> anyhow::Result { Ok(eth::Transaction { hash: transaction.hash.into(), @@ -150,10 +163,11 @@ fn into_domain( .gas_used .ok_or(anyhow::anyhow!("missing gas_used"))? .into(), - effective_gas_price: receipt + gas_price: receipt .effective_gas_price .ok_or(anyhow::anyhow!("missing effective_gas_price"))? .into(), + timestamp: timestamp.as_u32(), }) } diff --git a/crates/autopilot/src/infra/persistence/mod.rs b/crates/autopilot/src/infra/persistence/mod.rs index 0e2d826f17..9e93f1f8d9 100644 --- a/crates/autopilot/src/infra/persistence/mod.rs +++ b/crates/autopilot/src/infra/persistence/mod.rs @@ -8,7 +8,20 @@ use { anyhow::Context, boundary::database::byte_array::ByteArray, chrono::Utc, - database::{order_events::OrderEventLabel, settlement_observations::Observation}, + database::{ + order_events::OrderEventLabel, + orders::{ + BuyTokenDestination as DbBuyTokenDestination, + SellTokenSource as DbSellTokenSource, + SigningScheme as DbSigningScheme, + }, + settlement_observations::Observation, + }, + domain::auction::order::{ + BuyTokenDestination as DomainBuyTokenDestination, + SellTokenSource as DomainSellTokenSource, + SigningScheme as DomainSigningScheme, + }, number::conversions::{big_decimal_to_u256, u256_to_big_decimal}, primitive_types::{H160, H256}, std::{ @@ -436,9 +449,10 @@ impl Persistence { if let Some(settlement) = settlement { let gas = settlement.gas(); let gas_price = settlement.gas_price(); - let surplus = settlement.native_surplus(); - let fee = settlement.native_fee(); + let surplus = settlement.surplus_in_ether(); + let fee = settlement.fee_in_ether(); let order_fees = settlement.order_fees(); + let jit_orders = settlement.jit_orders(); tracing::debug!( ?auction_id, @@ -448,6 +462,7 @@ impl Persistence { ?surplus, ?fee, ?order_fees, + ?jit_orders, "settlement update", ); @@ -484,6 +499,54 @@ impl Persistence { ) .await?; } + + database::jit_orders::insert( + &mut ex, + &jit_orders + .into_iter() + .map(|jit_order| database::jit_orders::JitOrder { + block_number, + log_index, + uid: ByteArray(jit_order.uid.0), + owner: ByteArray(jit_order.uid.owner().0 .0), + creation_timestamp: chrono::DateTime::from_timestamp( + i64::from(jit_order.created), + 0, + ) + .unwrap_or_default(), + sell_token: ByteArray(jit_order.sell.token.0 .0), + buy_token: ByteArray(jit_order.buy.token.0 .0), + sell_amount: u256_to_big_decimal(&jit_order.sell.amount.0), + buy_amount: u256_to_big_decimal(&jit_order.buy.amount.0), + valid_to: i64::from(jit_order.valid_to), + app_data: ByteArray(jit_order.app_data.0), + fee_amount: u256_to_big_decimal(&jit_order.fee_amount.0), + kind: match jit_order.side { + domain::auction::order::Side::Buy => database::orders::OrderKind::Buy, + domain::auction::order::Side::Sell => database::orders::OrderKind::Sell, + }, + partially_fillable: jit_order.partially_fillable, + signature: jit_order.signature.to_bytes(), + receiver: ByteArray(jit_order.receiver.0 .0), + signing_scheme: match jit_order.signature.scheme() { + DomainSigningScheme::Eip712 => DbSigningScheme::Eip712, + DomainSigningScheme::EthSign => DbSigningScheme::EthSign, + DomainSigningScheme::Eip1271 => DbSigningScheme::Eip1271, + DomainSigningScheme::PreSign => DbSigningScheme::PreSign, + }, + sell_token_balance: match jit_order.sell_token_balance { + DomainSellTokenSource::Erc20 => DbSellTokenSource::Erc20, + DomainSellTokenSource::External => DbSellTokenSource::External, + DomainSellTokenSource::Internal => DbSellTokenSource::Internal, + }, + buy_token_balance: match jit_order.buy_token_balance { + DomainBuyTokenDestination::Erc20 => DbBuyTokenDestination::Erc20, + DomainBuyTokenDestination::Internal => DbBuyTokenDestination::Internal, + }, + }) + .collect::>(), + ) + .await?; } ex.commit().await?; diff --git a/crates/autopilot/src/on_settlement_event_updater.rs b/crates/autopilot/src/on_settlement_event_updater.rs index 6bedda65bf..a5dd659f87 100644 --- a/crates/autopilot/src/on_settlement_event_updater.rs +++ b/crates/autopilot/src/on_settlement_event_updater.rs @@ -134,11 +134,5 @@ fn retryable(err: &settlement::Error) -> bool { settlement::Error::Infra(_) => true, settlement::Error::InconsistentData(_) => false, settlement::Error::WrongEnvironment => false, - settlement::Error::BuildingSolution(_) => false, - settlement::Error::BuildingScore(_) => false, - settlement::Error::SolverMismatch { - expected: _, - got: _, - } => false, } } diff --git a/crates/database/src/jit_orders.rs b/crates/database/src/jit_orders.rs new file mode 100644 index 0000000000..1482950511 --- /dev/null +++ b/crates/database/src/jit_orders.rs @@ -0,0 +1,247 @@ +use { + crate::{ + orders, + orders::{BuyTokenDestination, OrderKind, SellTokenSource, SigningScheme}, + Address, + AppId, + OrderUid, + }, + sqlx::{ + types::{ + chrono::{DateTime, Utc}, + BigDecimal, + }, + PgConnection, + QueryBuilder, + }, +}; + +pub async fn get_by_id( + ex: &mut PgConnection, + uid: &OrderUid, +) -> Result, sqlx::Error> { + #[rustfmt::skip] + const QUERY: &str = const_format::concatcp!( +"SELECT o.uid, o.owner, o.creation_timestamp, o.sell_token, o.buy_token, o.sell_amount, o.buy_amount, +o.valid_to, o.app_data, o.fee_amount, o.kind, o.partially_fillable, o.signature, +o.receiver, o.signing_scheme, o.sell_token_balance, o.buy_token_balance, +(SELECT COALESCE(SUM(t.buy_amount), 0) FROM trades t WHERE t.order_uid = o.uid) AS sum_buy, +(SELECT COALESCE(SUM(t.sell_amount), 0) FROM trades t WHERE t.order_uid = o.uid) AS sum_sell, +(SELECT COALESCE(SUM(t.fee_amount), 0) FROM trades t WHERE t.order_uid = o.uid) AS sum_fee, +COALESCE((SELECT SUM(surplus_fee) FROM order_execution oe WHERE oe.order_uid = o.uid), 0) as executed_surplus_fee", +" FROM jit_orders o", +" WHERE o.uid = $1 ", + ); + sqlx::query_as::<_, JitOrderWithExecutions>(QUERY) + .bind(uid) + .fetch_optional(ex) + .await + .map(|r| r.map(Into::into)) +} + +/// 1:1 mapping to the `jit_orders` table, used to store orders. +#[derive(Debug, Clone, Default, PartialEq, sqlx::FromRow)] +pub struct JitOrder { + pub block_number: i64, + pub log_index: i64, + pub uid: OrderUid, + pub owner: Address, + pub creation_timestamp: DateTime, + pub sell_token: Address, + pub buy_token: Address, + pub sell_amount: BigDecimal, + pub buy_amount: BigDecimal, + pub valid_to: i64, + pub app_data: AppId, + pub fee_amount: BigDecimal, + pub kind: OrderKind, + pub partially_fillable: bool, + pub signature: Vec, + pub receiver: Address, + pub signing_scheme: SigningScheme, + pub sell_token_balance: SellTokenSource, + pub buy_token_balance: BuyTokenDestination, +} + +pub async fn insert(ex: &mut PgConnection, jit_orders: &[JitOrder]) -> Result<(), sqlx::Error> { + if jit_orders.is_empty() { + return Ok(()); + } + + let mut query_builder = QueryBuilder::new( + r#" + INSERT INTO jit_orders ( + block_number, + log_index, + uid, + owner, + creation_timestamp, + sell_token, + buy_token, + sell_amount, + buy_amount, + valid_to, + app_data, + fee_amount, + kind, + partially_fillable, + signature, + receiver, + signing_scheme, + sell_token_balance, + buy_token_balance + ) + "#, + ); + + query_builder.push_values(jit_orders.iter(), |mut builder, jit_order| { + builder + .push_bind(jit_order.block_number) + .push_bind(jit_order.log_index) + .push_bind(jit_order.uid) + .push_bind(jit_order.owner) + .push_bind(jit_order.creation_timestamp) + .push_bind(jit_order.sell_token) + .push_bind(jit_order.buy_token) + .push_bind(jit_order.sell_amount.clone()) + .push_bind(jit_order.buy_amount.clone()) + .push_bind(jit_order.valid_to) + .push_bind(jit_order.app_data) + .push_bind(jit_order.fee_amount.clone()) + .push_bind(jit_order.kind) + .push_bind(jit_order.partially_fillable) + .push_bind(jit_order.signature.clone()) + .push_bind(jit_order.receiver) + .push_bind(jit_order.signing_scheme) + .push_bind(jit_order.sell_token_balance) + .push_bind(jit_order.buy_token_balance); + }); + + query_builder.push( + r#" + ON CONFLICT DO NOTHING"#, + ); + + let query = query_builder.build(); + query.execute(ex).await?; + + Ok(()) +} + +/// Jit order combined with trades table and order_execution table, suitable for +/// API responses. +#[derive(Debug, Clone, Default, PartialEq, sqlx::FromRow)] +struct JitOrderWithExecutions { + pub uid: OrderUid, + pub owner: Address, + pub creation_timestamp: DateTime, + pub sell_token: Address, + pub buy_token: Address, + pub sell_amount: BigDecimal, + pub buy_amount: BigDecimal, + pub valid_to: i64, + pub app_data: AppId, + pub fee_amount: BigDecimal, + pub kind: OrderKind, + pub partially_fillable: bool, + pub signature: Vec, + pub sum_sell: BigDecimal, + pub sum_buy: BigDecimal, + pub sum_fee: BigDecimal, + pub receiver: Address, + pub signing_scheme: SigningScheme, + pub sell_token_balance: SellTokenSource, + pub buy_token_balance: BuyTokenDestination, + pub executed_surplus_fee: BigDecimal, +} + +impl From for orders::FullOrder { + fn from(jit_order: JitOrderWithExecutions) -> Self { + orders::FullOrder { + uid: jit_order.uid, + owner: jit_order.owner, + creation_timestamp: jit_order.creation_timestamp, + sell_token: jit_order.sell_token, + buy_token: jit_order.buy_token, + sell_amount: jit_order.sell_amount, + buy_amount: jit_order.buy_amount, + valid_to: jit_order.valid_to, + app_data: jit_order.app_data, + fee_amount: jit_order.fee_amount.clone(), + full_fee_amount: jit_order.fee_amount, + kind: jit_order.kind, + class: orders::OrderClass::Limit, // irrelevant + partially_fillable: jit_order.partially_fillable, + signature: jit_order.signature, + sum_sell: jit_order.sum_sell, + sum_buy: jit_order.sum_buy, + sum_fee: jit_order.sum_fee, + invalidated: false, + receiver: Some(jit_order.receiver), + signing_scheme: jit_order.signing_scheme, + settlement_contract: Address::default(), + sell_token_balance: jit_order.sell_token_balance, + buy_token_balance: jit_order.buy_token_balance, + presignature_pending: false, + pre_interactions: Vec::new(), + post_interactions: Vec::new(), + ethflow_data: None, + onchain_user: None, + onchain_placement_error: None, + executed_surplus_fee: jit_order.executed_surplus_fee, + full_app_data: None, + } + } +} + +#[cfg(test)] +mod tests { + pub async fn read_order( + ex: &mut PgConnection, + uid: &OrderUid, + ) -> Result, sqlx::Error> { + const QUERY: &str = r#" + SELECT * + FROM jit_orders + WHERE uid = $1 + ;"#; + sqlx::query_as(QUERY).bind(uid).fetch_optional(ex).await + } + + use { + super::*, + crate::byte_array::ByteArray, + sqlx::{Connection, PgConnection}, + }; + + #[tokio::test] + #[ignore] + async fn postgres_roundtrip() { + let mut db = PgConnection::connect("postgresql://").await.unwrap(); + let mut db = db.begin().await.unwrap(); + crate::clear_DANGER_(&mut db).await.unwrap(); + + let jit_order = JitOrder::default(); + + // insert a jit order and read it back + insert(&mut db, &[jit_order.clone()]).await.unwrap(); + let read_jit_order = read_order(&mut db, &jit_order.uid).await.unwrap().unwrap(); + assert_eq!(jit_order, read_jit_order); + + // try to insert updated order, but no update was done on conflict + let jit_order_updated = JitOrder { + creation_timestamp: DateTime::::default() + chrono::Duration::days(1), + ..jit_order.clone() + }; + insert(&mut db, &[jit_order_updated.clone()]).await.unwrap(); + let read_jit_order = read_order(&mut db, &jit_order_updated.uid) + .await + .unwrap() + .unwrap(); + assert_eq!(jit_order, read_jit_order); + + // read non existent order + let read_jit_order = read_order(&mut db, &ByteArray([1u8; 56])).await.unwrap(); + assert!(read_jit_order.is_none()); + } +} diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs index de15a2547f..0c8da4b21f 100644 --- a/crates/database/src/lib.rs +++ b/crates/database/src/lib.rs @@ -7,6 +7,7 @@ pub mod byte_array; pub mod ethflow_orders; pub mod events; pub mod fee_policies; +pub mod jit_orders; pub mod onchain_broadcasted_orders; pub mod onchain_invalidations; pub mod order_events; @@ -67,6 +68,7 @@ pub const TABLES: &[&str] = &[ "auction_prices", "auction_participants", "app_data", + "jit_orders", ]; /// The names of potentially big volume tables we use in the db. diff --git a/crates/database/src/settlements.rs b/crates/database/src/settlements.rs index 4d4a89d43b..9545c430cc 100644 --- a/crates/database/src/settlements.rs +++ b/crates/database/src/settlements.rs @@ -93,20 +93,19 @@ pub async fn delete( ex: &mut PgTransaction<'_>, delete_from_block_number: u64, ) -> Result<(), sqlx::Error> { + let delete_from_block_number = i64::try_from(delete_from_block_number).unwrap_or(i64::MAX); const QUERY_OBSERVATIONS: &str = "DELETE FROM settlement_observations WHERE block_number >= $1;"; - ex.execute( - sqlx::query(QUERY_OBSERVATIONS) - .bind(i64::try_from(delete_from_block_number).unwrap_or(i64::MAX)), - ) - .await?; + ex.execute(sqlx::query(QUERY_OBSERVATIONS).bind(delete_from_block_number)) + .await?; const QUERY_ORDER_EXECUTIONS: &str = "DELETE FROM order_execution WHERE block_number >= $1;"; - ex.execute( - sqlx::query(QUERY_ORDER_EXECUTIONS) - .bind(i64::try_from(delete_from_block_number).unwrap_or(i64::MAX)), - ) - .await?; + ex.execute(sqlx::query(QUERY_ORDER_EXECUTIONS).bind(delete_from_block_number)) + .await?; + + const QUERY_JIT_ORDERS: &str = "DELETE FROM jit_orders WHERE block_number >= $1;"; + ex.execute(sqlx::query(QUERY_JIT_ORDERS).bind(delete_from_block_number)) + .await?; Ok(()) } diff --git a/crates/database/src/trades.rs b/crates/database/src/trades.rs index 35423ee4b6..bb6fc6903f 100644 --- a/crates/database/src/trades.rs +++ b/crates/database/src/trades.rs @@ -45,19 +45,25 @@ LEFT OUTER JOIN LATERAL ( AND s.log_index > t.log_index ORDER BY s.log_index ASC LIMIT 1 -) AS settlement ON true -JOIN orders o -ON o.uid = t.order_uid"#; +) AS settlement ON true"#; + const QUERY: &str = const_format::concatcp!( COMMON_QUERY, + " JOIN orders o ON o.uid = t.order_uid", " WHERE ($1 IS NULL OR o.owner = $1)", " AND ($2 IS NULL OR o.uid = $2)", - "UNION", + " UNION ", COMMON_QUERY, + " JOIN orders o ON o.uid = t.order_uid", " LEFT OUTER JOIN onchain_placed_orders onchain_o", " ON onchain_o.uid = t.order_uid", " WHERE onchain_o.sender = $1", " AND ($2 IS NULL OR o.uid = $2)", + " UNION ", + COMMON_QUERY, + " JOIN jit_orders o ON o.uid = t.order_uid", + " WHERE ($1 IS NULL OR o.owner = $1)", + " AND ($2 IS NULL OR o.uid = $2)", ); sqlx::query_as(QUERY) diff --git a/crates/e2e/src/setup/solver/solution.rs b/crates/e2e/src/setup/solver/solution.rs index 9e2d790f79..60dd7bc188 100644 --- a/crates/e2e/src/setup/solver/solution.rs +++ b/crates/e2e/src/setup/solver/solution.rs @@ -2,7 +2,7 @@ use { app_data::AppDataHash, ethcontract::common::abi::ethereum_types::Address, model::{ - order::{BuyTokenDestination, OrderData, OrderKind, SellTokenSource}, + order::{BuyTokenDestination, OrderData, OrderKind, OrderUid, SellTokenSource}, signature::EcdsaSigningScheme, DomainSeparator, }, @@ -45,22 +45,28 @@ impl JitOrder { signing_scheme: EcdsaSigningScheme, domain: &DomainSeparator, key: SecretKeyRef, - ) -> solvers_dto::solution::JitOrder { + ) -> (solvers_dto::solution::JitOrder, OrderUid) { let data = self.data(); - let signature = match model::signature::EcdsaSignature::sign( + let signature = model::signature::EcdsaSignature::sign( signing_scheme, domain, &data.hash_struct(), key, ) - .to_signature(signing_scheme) - { + .to_signature(signing_scheme); + let order_uid = data.uid( + domain, + &signature + .recover_owner(&signature.to_bytes(), domain, &data.hash_struct()) + .unwrap(), + ); + let signature = match signature { model::signature::Signature::Eip712(signature) => signature.to_bytes().to_vec(), model::signature::Signature::EthSign(signature) => signature.to_bytes().to_vec(), model::signature::Signature::Eip1271(signature) => signature, model::signature::Signature::PreSign => panic!("Not supported PreSigned JIT orders"), }; - solvers_dto::solution::JitOrder { + let order = solvers_dto::solution::JitOrder { sell_token: data.sell_token, buy_token: data.buy_token, receiver: data.receiver.unwrap_or_default(), @@ -86,6 +92,7 @@ impl JitOrder { EcdsaSigningScheme::EthSign => solvers_dto::solution::SigningScheme::EthSign, }, signature, - } + }; + (order, order_uid) } } diff --git a/crates/e2e/tests/e2e/jit_orders.rs b/crates/e2e/tests/e2e/jit_orders.rs index 41e3c088e6..86eb20c85e 100644 --- a/crates/e2e/tests/e2e/jit_orders.rs +++ b/crates/e2e/tests/e2e/jit_orders.rs @@ -112,6 +112,28 @@ async fn single_limit_order_test(web3: Web3) { let limit_order = services.get_order(&order_id).await.unwrap(); assert_eq!(limit_order.metadata.class, OrderClass::Limit); + let (jit_order, jit_order_uid) = JitOrder { + owner: trader.address(), + sell: Asset { + amount: to_wei(10), + token: token.address(), + }, + buy: Asset { + amount: to_wei(1), + token: onchain.contracts().weth.address(), + }, + kind: OrderKind::Sell, + partially_fillable: false, + valid_to: model::time::now_in_epoch_seconds() + 300, + app_data: Default::default(), + receiver: solver.address(), + } + .sign( + EcdsaSigningScheme::Eip712, + &onchain.contracts().domain_separator, + SecretKeyRef::from(&SecretKey::from_slice(solver.private_key()).unwrap()), + ); + mock_solver.configure_solution(Some(Solution { id: 0, prices: HashMap::from([ @@ -120,27 +142,7 @@ async fn single_limit_order_test(web3: Web3) { ]), trades: vec![ solvers_dto::solution::Trade::Jit(solvers_dto::solution::JitTrade { - order: JitOrder { - owner: trader.address(), - sell: Asset { - amount: to_wei(10), - token: token.address(), - }, - buy: Asset { - amount: to_wei(1), - token: onchain.contracts().weth.address(), - }, - kind: OrderKind::Sell, - partially_fillable: false, - valid_to: model::time::now_in_epoch_seconds() + 300, - app_data: Default::default(), - receiver: solver.address(), - } - .sign( - EcdsaSigningScheme::Eip712, - &onchain.contracts().domain_separator, - SecretKeyRef::from(&SecretKey::from_slice(solver.private_key()).unwrap()), - ), + order: jit_order, executed_amount: to_wei(10), fee: Some(0.into()), }), @@ -190,4 +192,10 @@ async fn single_limit_order_test(web3: Web3) { }) .await .unwrap(); + + // jit order can be found on /get_order + services.get_order(&jit_order_uid).await.unwrap(); + // jit order can be found on /get_trades + let orders = services.get_trades(&jit_order_uid).await.unwrap(); + assert_eq!(orders.len(), 1); } diff --git a/crates/orderbook/src/database/orders.rs b/crates/orderbook/src/database/orders.rs index 0acafe2f18..e1ca3c8492 100644 --- a/crates/orderbook/src/database/orders.rs +++ b/crates/orderbook/src/database/orders.rs @@ -312,7 +312,13 @@ impl OrderStoring for Postgres { .start_timer(); let mut ex = self.pool.acquire().await?; - let order = database::orders::single_full_order(&mut ex, &ByteArray(uid.0)).await?; + let order = match database::orders::single_full_order(&mut ex, &ByteArray(uid.0)).await? { + Some(order) => Some(order), + None => { + // try to find the order in the JIT orders table + database::jit_orders::get_by_id(&mut ex, &ByteArray(uid.0)).await? + } + }; order.map(full_order_into_model_order).transpose() } diff --git a/database/README.md b/database/README.md index 6a908a1a67..be60567f14 100644 --- a/database/README.md +++ b/database/README.md @@ -424,6 +424,40 @@ owners | bytea[] | not null | surplus capturing jit order owner included in Indexes: - PRIMARY KEY: btree(`auction_id`) +### jit\_orders + +JIT orders stored here are orders that were settled outside of the competitition Auction. This means both regular JIT orders that protocol is not aware of, as well as regular user orders that were not listed in the Auction can appear in this table. + +Column | Type | Nullable | Details +--------------------------|------------------------------|----------|-------- + block\_number | bigint | not null | block in which the event happened + log\_index | bigint | not null | index in which the event was emitted + uid | bytea | not null | 56 bytes identifier composed of a 32 bytes `hash` over the order data signed by the user, 20 bytes containing the `owner` and 4 bytes containing `valid_to`. + owner | bytea | not null | address who created this order and where the sell\_token will be taken from, note that for ethflow orders this is the [CoWSwapEthFlow](https://github.com/cowprotocol/ethflowcontract/blob/main/src/CoWSwapEthFlow.sol) smart contract and not the user that actually initiated the trade + creation\_timestamp | timestamptz | not null | when the order was created + sell\_token | bytea | not null | address of the token that will be sold + buy\_token | bytea | not null | address of the token that will be bought + sell\_amount | numeric | not null | amount in sell\_token that should be sold at most + buy\_amount | numeric | not null | amount of buy\_token that should be bought at least + valid\_to | timestamptz | not null | point in time when the order can no longer be settled + fee\_amount | numeric | not null | amount in sell\_token the owner agreed upfront as a fee to be taken for the trade + kind | [enum](#orderkind) | not null | trade semantics of the order + signature | bytea | not null | signature provided by the owner stored as raw bytes. What these bytes mean is determined by signing\_scheme + receiver | bytea | nullable | address that should receive the buy\_tokens. If this is null the owner will receive the buy tokens + app\_data | bytea | not null | arbitrary data associated with this order but per [design](https://docs.cow.fi/cow-sdk/order-meta-data-appdata) this is an IPFS hash which may contain additional meta data for this order signed by the user + signing\_scheme | [enum](#signingscheme) | not null | what kind of signature was used to proof that the `owner` actually created the order + sell\_token\_balance | [enum](#selltokensource) | not null | defines how sell\_tokens need to be transferred into the settlement contract + buy\_token\_balance | [enum](#buytokendestination) | not null | defined how buy\_tokens need to be transferred back to the user + class | [enum](#orderclass) | not null | determines which special trade semantics will apply to the execution of this order + +Indexes: +- PRIMARY KEY: btree(`block_number`, `log_index`) +- jit\_order\_creation\_timestamp: btree(`creation_timestamp`) +- jit\_order\_owner: hash(`owner`) +- jit\_order\_uid: hash(`uid`) +- jit\_user\_order\_creation\_timestamp: btree(`owner`, `creation_timestamp` DESC) +- jit\_event\_id: btree(`block_number`, `log_index`) + ### Enums #### executiontime diff --git a/database/sql/V068__create_jit_orders.sql b/database/sql/V068__create_jit_orders.sql new file mode 100644 index 0000000000..468c259e10 --- /dev/null +++ b/database/sql/V068__create_jit_orders.sql @@ -0,0 +1,40 @@ +-- JIT orders are not stored in `orders` table but in a separate table. Two reasons for this: +-- 1. Some fields from `orders` table are not needed for JIT orders, such as `partially_fillable`, `settlement_contract` etc. +-- 2. JIT orders are observed from blockchain which means this table needs to be reorg safe, so it contains block_number and log_index. +CREATE TABLE jit_orders ( + block_number bigint NOT NULL, + log_index bigint NOT NULL, + uid bytea NOT NULL, + owner bytea NOT NULL, + creation_timestamp timestamptz NOT NULL, + sell_token bytea NOT NULL, + buy_token bytea NOT NULL, + sell_amount numeric(78,0) NOT NULL, + buy_amount numeric(78,0) NOT NULL, + valid_to bigint NOT NULL, + app_data bytea NOT NULL, + fee_amount numeric(78,0) NOT NULL, + kind OrderKind NOT NULL, + partially_fillable boolean NOT NULL, + signature bytea NOT NULL, -- r + s + v + receiver bytea, + signing_scheme SigningScheme NOT NULL, + sell_token_balance SellTokenSource NOT NULL, + buy_token_balance BuyTokenDestination NOT NULL, + + PRIMARY KEY (block_number, log_index) +); + +-- Get a specific user's orders. +CREATE INDEX jit_order_owner ON jit_orders USING HASH (owner); + +CREATE INDEX jit_order_uid ON jit_orders USING HASH (uid); + +CREATE INDEX jit_order_creation_timestamp ON jit_orders USING BTREE (creation_timestamp); + +-- To optimize the performance of the user_orders query, we introduce a new index that allows +-- us to quickly get the latest orders from a owner +CREATE INDEX jit_user_order_creation_timestamp ON jit_orders USING BTREE (owner, creation_timestamp DESC); + +-- To optimize deletion of reorged orders +CREATE INDEX jit_event_id ON jit_orders USING BTREE (block_number, log_index);