diff --git a/crates/autopilot/src/database/on_settlement_event_updater.rs b/crates/autopilot/src/database/on_settlement_event_updater.rs index d4d7ee42b5..2fa304be39 100644 --- a/crates/autopilot/src/database/on_settlement_event_updater.rs +++ b/crates/autopilot/src/database/on_settlement_event_updater.rs @@ -1,5 +1,4 @@ use { - crate::on_settlement_event_updater::AuctionKind, anyhow::{Context, Result}, database::{byte_array::ByteArray, settlement_observations::Observation}, ethcontract::U256, @@ -14,7 +13,6 @@ pub type AuctionId = i64; #[derive(Debug, Default, Clone)] pub struct AuctionData { - pub auction_id: AuctionId, pub gas_used: U256, pub effective_gas_price: U256, pub surplus: U256, @@ -26,7 +24,8 @@ pub struct AuctionData { pub struct SettlementUpdate { pub block_number: i64, pub log_index: i64, - pub auction_kind: AuctionKind, + pub auction_id: AuctionId, + /// Only set if the auction is for this environment. pub auction_data: Option, } @@ -40,20 +39,12 @@ impl super::Postgres { .with_label_values(&["update_settlement_details"]) .start_timer(); - let (auction_id, auction_kind) = match settlement_update.auction_kind { - AuctionKind::Valid { auction_id } => { - (Some(auction_id), database::settlements::AuctionKind::Valid) - } - AuctionKind::Invalid => (None, database::settlements::AuctionKind::Invalid), - }; - // update settlements database::settlements::update_settlement_auction( ex, settlement_update.block_number, settlement_update.log_index, - auction_id, - auction_kind, + settlement_update.auction_id, ) .await .context("insert_settlement_tx_info")?; @@ -77,7 +68,7 @@ impl super::Postgres { database::order_execution::save( ex, &ByteArray(order.0), - auction_data.auction_id, + settlement_update.auction_id, &u256_to_big_decimal(&executed_fee), ) .await diff --git a/crates/autopilot/src/on_settlement_event_updater.rs b/crates/autopilot/src/on_settlement_event_updater.rs index 89117a3f74..643ee1b4ef 100644 --- a/crates/autopilot/src/on_settlement_event_updater.rs +++ b/crates/autopilot/src/on_settlement_event_updater.rs @@ -36,54 +36,37 @@ use { on_settlement_event_updater::{AuctionData, SettlementUpdate}, Postgres, }, - decoded_settlement::{DecodedSettlement, DecodingError}, + decoded_settlement::DecodedSettlement, infra, }, anyhow::{Context, Result}, futures::StreamExt, primitive_types::H256, - shared::external_prices::ExternalPrices, + shared::{event_handling::MAX_REORG_BLOCK_COUNT, external_prices::ExternalPrices}, sqlx::PgConnection, web3::types::Transaction, }; -#[derive(Debug, Copy, Clone)] -pub enum AuctionKind { - /// This auction is regular and all the auction dependent data should be - /// updated. - Valid { auction_id: i64 }, - /// Some possible reasons to have invalid auction are: - /// - This auction was created by another environment (e.g. - /// production/staging) - /// - Failed to decode settlement calldata - /// - Failed to recover auction id from calldata - /// - Settlement transaction was submitted by solver other than the winner - /// - /// In this case, settlement event should be marked as invalid and no - /// auction dependent data is updated. - Invalid, -} - -impl AuctionKind { - pub fn auction_id(&self) -> Option { - match self { - AuctionKind::Valid { auction_id } => Some(*auction_id), - AuctionKind::Invalid => None, - } - } -} - pub struct OnSettlementEventUpdater { pub eth: infra::Ethereum, pub db: Postgres, } +enum AuctionIdRecoveryStatus { + /// The auction id was recovered and the auction data should be added. + AddAuctionData(i64, DecodedSettlement), + /// The auction id was recovered but the auction data should not be added. + DoNotAddAuctionData(i64), + /// The auction id was not recovered. + InvalidCalldata, +} + impl OnSettlementEventUpdater { pub async fn run_forever(self) -> ! { let mut current_block = self.eth.current_block().borrow().to_owned(); let mut block_stream = ethrpc::current_block::into_stream(self.eth.current_block().clone()); loop { - match self.update().await { + match self.update(current_block.number).await { Ok(true) => { tracing::debug!( block = current_block.number, @@ -103,31 +86,33 @@ impl OnSettlementEventUpdater { } } current_block = block_stream.next().await.expect("blockchains never end"); - - // Wait a bit more to not race with the event indexer. - // Otherwise we might miss event and have to wait for next block to retrigger - // loop. - tokio::time::sleep(std::time::Duration::from_secs(1)).await; } } /// Update database for settlement events that have not been processed yet. /// /// Returns whether an update was performed. - async fn update(&self) -> Result { + async fn update(&self, current_block: u64) -> Result { + let reorg_safe_block: i64 = current_block + .checked_sub(MAX_REORG_BLOCK_COUNT) + .context("no reorg safe block")? + .try_into() + .context("convert block")?; + let mut ex = self .db .pool .begin() .await .context("acquire DB connection")?; - let event = match database::settlements::get_settlement_without_auction(&mut ex) - .await - .context("get_settlement_without_auction")? - { - Some(event) => event, - None => return Ok(false), - }; + let event = + match database::settlements::get_settlement_without_auction(&mut ex, reorg_safe_block) + .await + .context("get_settlement_event_without_tx_info")? + { + Some(event) => event, + None => return Ok(false), + }; let hash = H256(event.tx_hash.0); tracing::debug!("updating settlement details for tx {hash:?}"); @@ -138,104 +123,28 @@ impl OnSettlementEventUpdater { .await? .with_context(|| format!("no tx {hash:?}"))?; - let auction_kind = Self::get_auction_kind(&mut ex, &transaction).await?; + let (auction_id, auction_data) = + match Self::recover_auction_id_from_calldata(&mut ex, &transaction).await? { + AuctionIdRecoveryStatus::InvalidCalldata => { + return Ok(false); + } + AuctionIdRecoveryStatus::DoNotAddAuctionData(auction_id) => (auction_id, None), + AuctionIdRecoveryStatus::AddAuctionData(auction_id, settlement) => ( + auction_id, + Some( + self.fetch_auction_data(hash, settlement, auction_id, &mut ex) + .await?, + ), + ), + }; - let mut update = SettlementUpdate { + let update = SettlementUpdate { block_number: event.block_number, log_index: event.log_index, - auction_kind, - auction_data: None, + auction_id, + auction_data, }; - // It is possible that auction_id does not exist for a transaction. - // This happens for production auctions queried from the staging environment and - // vice versa (because we have databases for both environments). - // - // If auction_id exists, we expect all other relevant information to exist as - // well. - if let AuctionKind::Valid { auction_id } = auction_kind { - let receipt = self - .eth - .transaction_receipt(hash) - .await? - .with_context(|| format!("no receipt {hash:?}"))?; - let gas_used = receipt - .gas_used - .with_context(|| format!("no gas used {hash:?}"))?; - let effective_gas_price = receipt - .effective_gas_price - .with_context(|| format!("no effective gas price {hash:?}"))?; - let auction_external_prices = Postgres::get_auction_prices(&mut ex, auction_id) - .await - .with_context(|| { - format!("no external prices for auction id {auction_id:?} and tx {hash:?}") - })?; - let external_prices = ExternalPrices::try_from_auction_prices( - self.eth.contracts().weth().address(), - auction_external_prices.clone(), - )?; - - tracing::debug!( - ?auction_id, - ?auction_external_prices, - ?external_prices, - "observations input" - ); - - // surplus and fees calculation - match DecodedSettlement::new(&transaction.input.0) { - Ok(settlement) => { - let domain_separator = self.eth.contracts().settlement_domain_separator(); - let order_uids = settlement.order_uids(domain_separator)?; - let order_fees = order_uids - .clone() - .into_iter() - .zip(Postgres::order_fees(&mut ex, &order_uids).await?) - .collect::>(); - - let surplus = settlement.total_surplus(&external_prices); - let (fee, order_executions) = { - let all_fees = settlement.all_fees(&external_prices, &order_fees); - // total unsubsidized fee used for CIP20 rewards - let fee = all_fees - .iter() - .fold(0.into(), |acc, fees| acc + fees.native); - // executed fees for each order execution - let order_executions = all_fees - .into_iter() - .zip(order_fees.iter()) - .filter_map(|(fee, (_, order_fee))| match order_fee { - // filter out orders with order_fee - // since their fee can already be fetched from the database table - // `orders` so no point in storing the same - // value twice, in another table - Some(_) => None, - None => Some((fee.order, fee.sell)), - }) - .collect(); - (fee, order_executions) - }; - - update.auction_data = Some(AuctionData { - auction_id, - surplus, - fee, - gas_used, - effective_gas_price, - order_executions, - }); - } - Err(DecodingError::InvalidSelector) => { - // we indexed a transaction initiated by solver, that was not a settlement - // for this case we want to have the entry in observations table but with zeros - update.auction_data = Some(Default::default()); - } - Err(err) => { - return Err(err.into()); - } - } - } - tracing::debug!(?hash, ?update, "updating settlement details for tx"); Postgres::update_settlement_details(&mut ex, update.clone()) @@ -245,55 +154,139 @@ impl OnSettlementEventUpdater { Ok(true) } + async fn fetch_auction_data( + &self, + hash: H256, + settlement: DecodedSettlement, + auction_id: i64, + ex: &mut PgConnection, + ) -> Result { + let receipt = self + .eth + .transaction_receipt(hash) + .await? + .with_context(|| format!("no receipt {hash:?}"))?; + let gas_used = receipt + .gas_used + .with_context(|| format!("no gas used {hash:?}"))?; + let effective_gas_price = receipt + .effective_gas_price + .with_context(|| format!("no effective gas price {hash:?}"))?; + let auction_external_prices = Postgres::get_auction_prices(ex, auction_id) + .await + .with_context(|| { + format!("no external prices for auction id {auction_id:?} and tx {hash:?}") + })?; + let external_prices = ExternalPrices::try_from_auction_prices( + self.eth.contracts().weth().address(), + auction_external_prices.clone(), + )?; + + tracing::debug!( + ?auction_id, + ?auction_external_prices, + ?external_prices, + "observations input" + ); + + // surplus and fees calculation + let domain_separator = self.eth.contracts().settlement_domain_separator(); + let order_uids = settlement.order_uids(domain_separator)?; + let order_fees = order_uids + .clone() + .into_iter() + .zip(Postgres::order_fees(ex, &order_uids).await?) + .collect::>(); + + let surplus = settlement.total_surplus(&external_prices); + let (fee, order_executions) = { + let all_fees = settlement.all_fees(&external_prices, &order_fees); + // total unsubsidized fee used for CIP20 rewards + let fee = all_fees + .iter() + .fold(0.into(), |acc, fees| acc + fees.native); + // executed fees for each order execution + let order_executions = all_fees + .into_iter() + .zip(order_fees.iter()) + .map(|(fee, (_, order_fee))| match order_fee { + // market orders have no surplus fee + Some(_) => (fee.order, 0.into()), + None => (fee.order, fee.sell), + }) + .collect(); + (fee, order_executions) + }; + + Ok(AuctionData { + surplus, + fee, + gas_used, + effective_gas_price, + order_executions, + }) + } + /// With solver driver colocation solvers are supposed to append the /// `auction_id` to the settlement calldata. This function tries to - /// recover that `auction_id`. This function only returns an error if - /// retrying the operation makes sense. If all went well and there - /// simply is no sensible `auction_id` `AuctionKind::Invalid` will be - /// returned. - async fn get_auction_kind(ex: &mut PgConnection, tx: &Transaction) -> Result { + /// recover that `auction_id`. It also indicates whether the auction + /// should be indexed with its metadata. (ie. if it comes from this + /// environment and not from a different instance of the autopilot, e.g. + /// running in barn/prod). This function only returns an error + /// if retrying the operation makes sense. + async fn recover_auction_id_from_calldata( + ex: &mut PgConnection, + tx: &Transaction, + ) -> Result { let tx_from = tx.from.context("tx is missing sender")?; - let metadata = match DecodedSettlement::new(&tx.input.0) { - Ok(settlement) => settlement.metadata, + let settlement = match DecodedSettlement::new(&tx.input.0) { + Ok(settlement) => settlement, Err(err) => { tracing::warn!( ?tx, ?err, "could not decode settlement tx, unclear which auction it belongs to" ); - return Ok(AuctionKind::Invalid); + return Ok(AuctionIdRecoveryStatus::InvalidCalldata); } }; - let auction_id = match metadata { + let auction_id = match settlement.metadata { Some(bytes) => i64::from_be_bytes(bytes.0), None => { tracing::warn!(?tx, "could not recover the auction_id from the calldata"); - return Ok(AuctionKind::Invalid); + return Ok(AuctionIdRecoveryStatus::InvalidCalldata); } }; let score = database::settlement_scores::fetch(ex, auction_id).await?; - match score { - None => { + let data_already_recorded = database::settlements::data_exists(ex, auction_id).await?; + match (score, data_already_recorded) { + (None, _) => { tracing::debug!( auction_id, "calldata claims to settle auction that has no competition" ); - Ok(AuctionKind::Invalid) + Ok(AuctionIdRecoveryStatus::DoNotAddAuctionData(auction_id)) } - Some(score) => { - if score.winner.0 != tx_from.0 { - tracing::warn!( - auction_id, - ?tx_from, - winner = ?score.winner, - "solution submitted by solver other than the winner" - ); - Ok(AuctionKind::Invalid) - } else { - Ok(AuctionKind::Valid { auction_id }) - } + (Some(score), _) if score.winner.0 != tx_from.0 => { + tracing::warn!( + auction_id, + ?tx_from, + winner = ?score.winner, + "solution submitted by solver other than the winner" + ); + Ok(AuctionIdRecoveryStatus::DoNotAddAuctionData(auction_id)) + } + (Some(_), true) => { + tracing::warn!( + auction_id, + "settlement data already recorded for this auction" + ); + Ok(AuctionIdRecoveryStatus::DoNotAddAuctionData(auction_id)) } + (Some(_), false) => Ok(AuctionIdRecoveryStatus::AddAuctionData( + auction_id, settlement, + )), } } } diff --git a/crates/database/src/settlements.rs b/crates/database/src/settlements.rs index 1b41b3f3a6..13ecc292ea 100644 --- a/crates/database/src/settlements.rs +++ b/crates/database/src/settlements.rs @@ -47,40 +47,45 @@ pub struct SettlementEvent { pub tx_hash: TransactionHash, } -#[derive(Debug, Clone, PartialEq, sqlx::Type)] -#[sqlx(type_name = "AuctionKind", rename_all = "lowercase")] -pub enum AuctionKind { - Valid, - Invalid, -} - pub async fn get_settlement_without_auction( ex: &mut PgConnection, + max_block_number: i64, ) -> Result, sqlx::Error> { const QUERY: &str = r#" SELECT block_number, log_index, tx_hash FROM settlements -WHERE auction_kind = 'unprocessed' +WHERE auction_id IS NULL +AND block_number <= $1 ORDER BY block_number ASC LIMIT 1 "#; - sqlx::query_as(QUERY).fetch_optional(ex).await + sqlx::query_as(QUERY) + .bind(max_block_number) + .fetch_optional(ex) + .await +} + +pub async fn data_exists(ex: &mut PgConnection, auction_id: i64) -> Result { + const QUERY: &str = r#"SELECT COUNT(*) FROM settlements WHERE auction_id = $1;"#; + let count: i64 = sqlx::query_scalar(QUERY) + .bind(auction_id) + .fetch_one(ex) + .await?; + Ok(count >= 1) } pub async fn update_settlement_auction( ex: &mut PgConnection, block_number: i64, log_index: i64, - auction_id: Option, - auction_kind: AuctionKind, + auction_id: i64, ) -> Result<(), sqlx::Error> { const QUERY: &str = r#" UPDATE settlements -SET auction_kind = $1, auction_id = $2 -WHERE block_number = $3 AND log_index = $4 +SET auction_id = $1 +WHERE block_number = $2 AND log_index = $3 ;"#; sqlx::query(QUERY) - .bind(auction_kind) .bind(auction_id) .bind(block_number) .bind(log_index) @@ -170,7 +175,7 @@ mod tests { .await .unwrap(); - let settlement = get_settlement_without_auction(&mut db) + let settlement = get_settlement_without_auction(&mut db, 0) .await .unwrap() .unwrap(); @@ -178,17 +183,11 @@ mod tests { assert_eq!(settlement.block_number, event.block_number); assert_eq!(settlement.log_index, event.log_index); - update_settlement_auction( - &mut db, - event.block_number, - event.log_index, - Some(1), - AuctionKind::Valid, - ) - .await - .unwrap(); + update_settlement_auction(&mut db, event.block_number, event.log_index, 1) + .await + .unwrap(); - let settlement = get_settlement_without_auction(&mut db).await.unwrap(); + let settlement = get_settlement_without_auction(&mut db, 0).await.unwrap(); assert!(settlement.is_none()); } diff --git a/crates/database/src/solver_competition.rs b/crates/database/src/solver_competition.rs index ce6f80e83f..def53d0bde 100644 --- a/crates/database/src/solver_competition.rs +++ b/crates/database/src/solver_competition.rs @@ -127,8 +127,7 @@ mod tests { &mut db, index.block_number, index.log_index, - Some(id), - crate::settlements::AuctionKind::Valid, + id, ) .await .unwrap(); diff --git a/crates/e2e/src/setup/colocation.rs b/crates/e2e/src/setup/colocation.rs index 3062f770fa..301060f0c2 100644 --- a/crates/e2e/src/setup/colocation.rs +++ b/crates/e2e/src/setup/colocation.rs @@ -1,8 +1,7 @@ use { crate::{nodes::NODE_HOST, setup::*}, - ethcontract::{H160, H256, U256}, + ethcontract::{H160, U256}, reqwest::Url, - shared::sources::uniswap_v2::UNISWAP_INIT, tokio::task::JoinHandle, }; @@ -112,7 +111,7 @@ mempool = "public" contracts.gp_settlement.address(), contracts.weth.address(), contracts.uniswap_v2_router.address(), - H256(UNISWAP_INIT), + contracts.default_pool_code(), )); let args = vec![ "driver".to_string(), diff --git a/crates/e2e/src/setup/deploy.rs b/crates/e2e/src/setup/deploy.rs index 2a0f6c4141..7abcf97a26 100644 --- a/crates/e2e/src/setup/deploy.rs +++ b/crates/e2e/src/setup/deploy.rs @@ -10,12 +10,13 @@ use { UniswapV2Router02, WETH9, }, - ethcontract::{Address, U256}, + ethcontract::{Address, H256, U256}, model::DomainSeparator, shared::ethrpc::Web3, }; pub struct Contracts { + pub chain_id: u64, pub balancer_vault: BalancerV2Vault, pub gp_settlement: GPv2Settlement, pub gp_authenticator: GPv2AllowListAuthentication, @@ -36,6 +37,9 @@ impl Contracts { let gp_settlement = GPv2Settlement::deployed(web3).await.unwrap(); Self { + chain_id: network_id + .parse() + .expect("Couldn't parse network ID to u64"), balancer_vault: BalancerV2Vault::deployed(web3).await.unwrap(), gp_authenticator: GPv2AllowListAuthentication::deployed(web3).await.unwrap(), uniswap_v2_factory: UniswapV2Factory::deployed(web3).await.unwrap(), @@ -138,6 +142,9 @@ impl Contracts { let hooks = deploy!(HooksTrampoline(gp_settlement.address())); Self { + chain_id: network_id + .parse() + .expect("Couldn't parse network ID to u64"), balancer_vault, gp_settlement, gp_authenticator, @@ -150,4 +157,11 @@ impl Contracts { hooks, } } + + pub fn default_pool_code(&self) -> H256 { + match self.chain_id { + 100 => H256(shared::sources::uniswap_v2::HONEYSWAP_INIT), + _ => H256(shared::sources::uniswap_v2::UNISWAP_INIT), + } + } } diff --git a/crates/e2e/src/setup/onchain_components.rs b/crates/e2e/src/setup/onchain_components.rs index d902d91a35..a52a776aee 100644 --- a/crates/e2e/src/setup/onchain_components.rs +++ b/crates/e2e/src/setup/onchain_components.rs @@ -475,6 +475,23 @@ impl OnchainComponents { .unwrap(); } + /// We will only index events when they are 64 blocks old so we don't have + /// to throw out indexed data on reorgs. + /// This function executes enough transactions to ensure that all events + /// before calling this function are old enough to be indexed. + pub async fn mint_blocks_past_reorg_threshold(&self) { + tracing::info!("mining blocks to get past the reorg safety threshold for indexing events"); + + let current_block = || async { self.web3.eth().block_number().await.unwrap() }; + let target = current_block().await + 65; + // Simply issuing n transactions may not result in n blocks being mined. + // Therefore we continually call `evm_mine` until we can confirm the block + // number increased the expected number of times. + while current_block().await <= target { + let _ = self.web3.transport().execute("evm_mine", vec![]).await; + } + } + pub async fn mint_block(&self) { tracing::info!("mining block"); self.web3 diff --git a/crates/e2e/src/setup/services.rs b/crates/e2e/src/setup/services.rs index a8892a4f1c..1c90ae4fe3 100644 --- a/crates/e2e/src/setup/services.rs +++ b/crates/e2e/src/setup/services.rs @@ -69,7 +69,7 @@ impl<'a> Services<'a> { format!( "--custom-univ2-baseline-sources={:?}|{:?}", self.contracts.uniswap_v2_router.address(), - H256(shared::sources::uniswap_v2::UNISWAP_INIT), + self.contracts.default_pool_code(), ), format!( "--settlement-contract-address={:?}", @@ -113,15 +113,10 @@ impl<'a> Services<'a> { pub async fn start_api(&self, extra_args: Vec) { let args = [ "orderbook".to_string(), - "--enable-presign-orders=true".to_string(), - "--enable-eip1271-orders=true".to_string(), - "--enable-custom-interactions=true".to_string(), - "--allow-placing-partially-fillable-limit-orders=true".to_string(), format!( "--hooks-contract-address={:?}", self.contracts.hooks.address() ), - "--enable-eth-smart-contract-payments=true".to_string(), ] .into_iter() .chain(self.api_autopilot_solver_arguments()) diff --git a/crates/e2e/tests/e2e/app_data.rs b/crates/e2e/tests/e2e/app_data.rs index 26bfd0679a..43c6f09151 100644 --- a/crates/e2e/tests/e2e/app_data.rs +++ b/crates/e2e/tests/e2e/app_data.rs @@ -4,6 +4,7 @@ use { model::{ app_data::AppDataHash, order::{OrderCreation, OrderCreationAppData, OrderKind}, + quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount}, signature::EcdsaSigningScheme, }, reqwest::StatusCode, @@ -101,6 +102,23 @@ async fn app_data(web3: Web3) { let order3 = create_order(OrderCreationAppData::Hash { hash: app_data_hash, }); + services + .submit_quote(&OrderQuoteRequest { + sell_token: order3.sell_token, + buy_token: order3.buy_token, + side: OrderQuoteSide::Sell { + sell_amount: SellAmount::AfterFee { + value: order3.sell_amount.try_into().unwrap(), + }, + }, + app_data: OrderCreationAppData::Hash { + hash: app_data_hash, + }, + ..Default::default() + }) + .await + .unwrap(); + let uid = services.create_order(&order3).await.unwrap(); let order3_ = services.get_order(&uid).await.unwrap(); assert_eq!(order3_.data.app_data, app_data_hash); diff --git a/crates/e2e/tests/e2e/database.rs b/crates/e2e/tests/e2e/database.rs index 58a89c00ec..d18364e146 100644 --- a/crates/e2e/tests/e2e/database.rs +++ b/crates/e2e/tests/e2e/database.rs @@ -21,6 +21,8 @@ pub async fn events_of_order(db: &Db, uid: &OrderUid) -> Vec Option { let mut db = db.acquire().await.unwrap(); - const LAST_AUCTION_ID: &str = "SELECT auction_id FROM settlements WHERE auction_id IS NOT \ - NULL ORDER BY auction_id DESC LIMIT 1"; + const LAST_AUCTION_ID: &str = + "SELECT auction_id FROM auction_transaction ORDER BY auction_id DESC LIMIT 1"; let auction_id: i64 = sqlx::query_scalar(LAST_AUCTION_ID) .fetch_optional(db.deref_mut()) .await .unwrap()?; const TX_QUERY: &str = r" -SELECT * FROM settlements WHERE auction_id = $1"; - +SELECT s.* +FROM auction_transaction at +JOIN settlements s ON s.tx_from = at.tx_from AND s.tx_nonce = at.tx_nonce +WHERE at.auction_id = $1 + "; let tx: AuctionTransaction = sqlx::query_as(TX_QUERY) .bind(auction_id) .fetch_optional(db.deref_mut()) diff --git a/crates/e2e/tests/e2e/limit_orders.rs b/crates/e2e/tests/e2e/limit_orders.rs index 8075e84576..b212215354 100644 --- a/crates/e2e/tests/e2e/limit_orders.rs +++ b/crates/e2e/tests/e2e/limit_orders.rs @@ -36,19 +36,37 @@ async fn local_node_mixed_limit_and_market_orders() { } /// The block number from which we will fetch state for the forked tests. -pub const FORK_BLOCK: u64 = 18477910; -/// USDC whale address as per [FORK_BLOCK]. -pub const USDC_WHALE: H160 = H160(hex_literal::hex!( +const FORK_BLOCK_MAINNET: u64 = 18477910; +/// USDC whale address as per [FORK_BLOCK_MAINNET]. +const USDC_WHALE_MAINNET: H160 = H160(hex_literal::hex!( "28c6c06298d514db089934071355e5743bf21d60" )); #[tokio::test] #[ignore] -async fn forked_node_single_limit_order_mainnet() { +async fn forked_node_mainnet_single_limit_order() { run_forked_test_with_block_number( - forked_single_limit_order_test, - std::env::var("FORK_URL").expect("FORK_URL must be set to run forked tests"), - FORK_BLOCK, + forked_mainnet_single_limit_order_test, + std::env::var("FORK_URL_MAINNET") + .expect("FORK_URL_MAINNET must be set to run forked tests"), + FORK_BLOCK_MAINNET, + ) + .await; +} + +const FORK_BLOCK_GNOSIS: u64 = 32070725; +/// USDC whale address as per [FORK_BLOCK_GNOSIS]. +const USDC_WHALE_GNOSIS: H160 = H160(hex_literal::hex!( + "ba12222222228d8ba445958a75a0704d566bf2c8" +)); + +#[tokio::test] +#[ignore] +async fn forked_node_gnosis_single_limit_order() { + run_forked_test_with_block_number( + forked_gnosis_single_limit_order_test, + std::env::var("FORK_URL_GNOSIS").expect("FORK_URL_GNOSIS must be set to run forked tests"), + FORK_BLOCK_GNOSIS, ) .await; } @@ -455,7 +473,7 @@ async fn too_many_limit_orders_test(web3: Web3) { assert!(body.contains("TooManyLimitOrders")); } -async fn forked_single_limit_order_test(web3: Web3) { +async fn forked_mainnet_single_limit_order_test(web3: Web3) { let mut onchain = OnchainComponents::deployed(web3.clone()).await; let forked_node_api = web3.api::>(); @@ -478,7 +496,10 @@ async fn forked_single_limit_order_test(web3: Web3) { ); // Give trader some USDC - let usdc_whale = forked_node_api.impersonate(&USDC_WHALE).await.unwrap(); + let usdc_whale = forked_node_api + .impersonate(&USDC_WHALE_MAINNET) + .await + .unwrap(); tx!( usdc_whale, token_usdc.transfer(trader.address(), to_wei_with_exp(1000, 6)) @@ -547,3 +568,99 @@ async fn forked_single_limit_order_test(web3: Web3) { assert!(sell_token_balance_before > sell_token_balance_after); assert!(buy_token_balance_after >= buy_token_balance_before + to_wei_with_exp(500, 6)); } + +async fn forked_gnosis_single_limit_order_test(web3: Web3) { + let mut onchain = OnchainComponents::deployed(web3.clone()).await; + let forked_node_api = web3.api::>(); + + let [solver] = onchain.make_solvers_forked(to_wei(1)).await; + + let [trader] = onchain.make_accounts(to_wei(1)).await; + + let token_usdc = ERC20::at( + &web3, + "0xddafbb505ad214d7b80b1f830fccc89b60fb7a83" + .parse() + .unwrap(), + ); + + let token_wxdai = ERC20::at( + &web3, + "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d" + .parse() + .unwrap(), + ); + + // Give trader some USDC + let usdc_whale = forked_node_api + .impersonate(&USDC_WHALE_GNOSIS) + .await + .unwrap(); + tx!( + usdc_whale, + token_usdc.transfer(trader.address(), to_wei_with_exp(1000, 6)) + ); + + // Approve GPv2 for trading + tx!( + trader.account(), + token_usdc.approve(onchain.contracts().allowance, to_wei_with_exp(1000, 6)) + ); + + // Place Orders + let services = Services::new(onchain.contracts()).await; + services.start_protocol(solver).await; + + let order = OrderCreation { + sell_token: token_usdc.address(), + sell_amount: to_wei_with_exp(1000, 6), + buy_token: token_wxdai.address(), + buy_amount: to_wei(500), + valid_to: model::time::now_in_epoch_seconds() + 300, + kind: OrderKind::Sell, + ..Default::default() + } + .sign( + EcdsaSigningScheme::Eip712, + &onchain.contracts().domain_separator, + SecretKeyRef::from(&SecretKey::from_slice(trader.private_key()).unwrap()), + ); + let order_id = services.create_order(&order).await.unwrap(); + let limit_order = services.get_order(&order_id).await.unwrap(); + assert_eq!(limit_order.metadata.class, OrderClass::Limit); + + // Drive solution + tracing::info!("Waiting for trade."); + let sell_token_balance_before = token_usdc + .balance_of(trader.address()) + .call() + .await + .unwrap(); + let buy_token_balance_before = token_wxdai + .balance_of(trader.address()) + .call() + .await + .unwrap(); + + wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 1 }) + .await + .unwrap(); + + wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 0 }) + .await + .unwrap(); + + let sell_token_balance_after = token_usdc + .balance_of(trader.address()) + .call() + .await + .unwrap(); + let buy_token_balance_after = token_wxdai + .balance_of(trader.address()) + .call() + .await + .unwrap(); + + assert!(sell_token_balance_before > sell_token_balance_after); + assert!(buy_token_balance_after >= buy_token_balance_before + to_wei(500)); +} diff --git a/crates/e2e/tests/e2e/partial_fill.rs b/crates/e2e/tests/e2e/partial_fill.rs index 8b17b27093..58c396fee2 100644 --- a/crates/e2e/tests/e2e/partial_fill.rs +++ b/crates/e2e/tests/e2e/partial_fill.rs @@ -89,7 +89,10 @@ async fn test(web3: Web3) { .contains(&buy_balance.as_u128()) ); + onchain.mint_blocks_past_reorg_threshold().await; + let settlement_event_processed = || async { + onchain.mint_block().await; let order = services.get_order(&uid).await.unwrap(); order.metadata.executed_surplus_fee > U256::zero() }; diff --git a/crates/e2e/tests/e2e/partially_fillable_pool.rs b/crates/e2e/tests/e2e/partially_fillable_pool.rs index 04c77d75bd..ccc78a87d6 100644 --- a/crates/e2e/tests/e2e/partially_fillable_pool.rs +++ b/crates/e2e/tests/e2e/partially_fillable_pool.rs @@ -117,7 +117,9 @@ async fn test(web3: Web3) { .contains(&buy_balance.as_u128()) ); + onchain.mint_blocks_past_reorg_threshold().await; let metadata_updated = || async { + onchain.mint_block().await; let order = services.get_order(&uid).await.unwrap(); !order.metadata.executed_surplus_fee.is_zero() && order.metadata.executed_buy_amount != Default::default() diff --git a/crates/e2e/tests/e2e/protocol_fee.rs b/crates/e2e/tests/e2e/protocol_fee.rs index 3d2e240bc2..14b8da57be 100644 --- a/crates/e2e/tests/e2e/protocol_fee.rs +++ b/crates/e2e/tests/e2e/protocol_fee.rs @@ -328,7 +328,9 @@ async fn execute_test( .await .unwrap(); + onchain.mint_blocks_past_reorg_threshold().await; let metadata_updated = || async { + onchain.mint_block().await; let order = services.get_order(&uid).await.unwrap(); is_approximately_equal(order.metadata.executed_surplus_fee, expected_surplus_fee) }; diff --git a/crates/e2e/tests/e2e/solver_competition.rs b/crates/e2e/tests/e2e/solver_competition.rs index e735e09050..b205c6ef67 100644 --- a/crates/e2e/tests/e2e/solver_competition.rs +++ b/crates/e2e/tests/e2e/solver_competition.rs @@ -90,6 +90,8 @@ async fn solver_competition(web3: Web3) { || async { token_a.balance_of(trader.address()).call().await.unwrap() == U256::zero() }; wait_for_condition(TIMEOUT, trade_happened).await.unwrap(); + onchain.mint_blocks_past_reorg_threshold().await; + let indexed_trades = || async { onchain.mint_block().await; match services.get_trades(&uid).await.unwrap().first() { diff --git a/crates/e2e/tests/e2e/univ2.rs b/crates/e2e/tests/e2e/univ2.rs index 6fab8e4abf..58335da635 100644 --- a/crates/e2e/tests/e2e/univ2.rs +++ b/crates/e2e/tests/e2e/univ2.rs @@ -89,6 +89,8 @@ async fn test(web3: Web3) { .await .unwrap(); + onchain.mint_blocks_past_reorg_threshold().await; + let cip_20_data_updated = || async { let data = match crate::database::most_recent_cip_20_data(services.db()).await { Some(data) => data, @@ -102,6 +104,8 @@ async fn test(web3: Web3) { && data.participants.iter().any(|p| p.participant.0 == solver.address().0) // and won the auction && data.score.winner.0 == solver.address().0 + // and submitted the solution + && data.tx.tx_from.0 == solver.address().0 // and calldata is present && !data.call_data.call_data.is_empty() }; diff --git a/database/sql/V058__add_auction_id_to_settlements.sql b/database/sql/V058__add_auction_id_to_settlements.sql new file mode 100644 index 0000000000..c1a3a4a23c --- /dev/null +++ b/database/sql/V058__add_auction_id_to_settlements.sql @@ -0,0 +1,12 @@ +-- Step 1: Add the new column to the settlements table +ALTER TABLE settlements + ADD COLUMN auction_id bigint; + +-- Step 2: Populate auction_id columns +-- For all settlements that already have an auction_transaction record, set auction_id to the auction_transaction's auction_id +UPDATE settlements +SET auction_id = auction_transaction.auction_id +FROM auction_transaction +WHERE settlements.tx_from = auction_transaction.tx_from AND settlements.tx_nonce = auction_transaction.tx_nonce; + +-- Step 3: (Once migration has been successful) Drop the auction_transaction table, and the tx_from and tx_nonce columns from the settlements table diff --git a/database/sql/V058__drop_auction_transaction.sql b/database/sql/V058__drop_auction_transaction.sql deleted file mode 100644 index a42d6fcecb..0000000000 --- a/database/sql/V058__drop_auction_transaction.sql +++ /dev/null @@ -1,39 +0,0 @@ --- Step 1: Add the new columns to the settlements table -CREATE TYPE AuctionKind AS ENUM ('valid', 'invalid', 'unprocessed'); - -ALTER TABLE settlements - ADD COLUMN auction_kind AuctionKind NOT NULL DEFAULT 'unprocessed', - ADD COLUMN auction_id bigint; - --- Step 2a: Populate auction_kind and auction_id columns --- For all settlements that already have an auction_transaction record, set auction_kind to 'valid' and auction_id to the auction_transaction's auction_id -UPDATE settlements -SET auction_kind = 'valid', - auction_id = auction_transaction.auction_id -FROM auction_transaction -WHERE settlements.tx_from = auction_transaction.tx_from AND settlements.tx_nonce = auction_transaction.tx_nonce; - --- Step 2b: Populate auction_kind and auction_id columns --- For all settlements that have auction_id = NULL, set auction_kind to 'invalid' -UPDATE settlements -SET auction_kind = 'invalid' -WHERE auction_id IS NULL; - --- Step 2c: Populate auction_kind and auction_id columns --- For all settlements, going from the most recent to the oldest, set auction_kind to 'unprocessed' until the first settlement with auction_kind = 'valid' is found -UPDATE settlements -SET - auction_kind = 'unprocessed' -WHERE - auction_kind = 'invalid' - AND block_number > ( - SELECT MAX(block_number) - FROM settlements - WHERE auction_kind = 'valid' - ); - --- Step 3: Drop the auction_transaction table, and the tx_from and tx_nonce columns from the settlements table -DROP TABLE auction_transaction; -ALTER TABLE settlements - DROP COLUMN tx_from, - DROP COLUMN tx_nonce;