From 6fef56250312f5acf5fe8f24e13edb907d9b5775 Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Wed, 27 Sep 2023 12:34:15 +0200 Subject: [PATCH] Simulate Balances with PreInteractions In Driver (#1894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds balance simulation to the driver. This enables the `driver` to continue to work with orders that require pre-interactions for setting up balances (such as EthFlow). In addition - the pesky EthFlow exception was removed from the `driver` crate entirely 🎉! In fact, the `driver` no longer cares at all about the EthFlow contract address. ### Test Plan Added E2E test to verify that EthFlow orders are still executed. They use pre-interactions to ensure the balance is available, so we check that our new simulation logic actually works! Existing hooks E2E test continues to pass.
You can also apply this patch to make the test fail: ```diff diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index 144b0562..4377d46e 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -119,10 +119,10 @@ impl Auction { .collect::>(); let mut balances = join_all(traders.into_iter().map( - |(trader, token, source, interactions)| async move { + |(trader, token, source, _interactions)| async move { let balance = eth .erc20(token) - .tradable_balance(trader.into(), source, interactions) + .balance(trader.into()) .await; ((trader, token, source), balance) }, ```
--- crates/contracts/src/storage_accessible.rs | 43 ++- .../driver/src/domain/competition/auction.rs | 65 ++-- .../src/domain/competition/order/mod.rs | 16 +- crates/driver/src/domain/eth/mod.rs | 2 +- .../driver/src/infra/blockchain/contracts.rs | 92 ++++-- crates/driver/src/infra/blockchain/mod.rs | 12 +- crates/driver/src/infra/blockchain/token.rs | 75 ++++- crates/driver/src/infra/config/file/load.rs | 1 - crates/driver/src/infra/config/file/mod.rs | 5 - crates/driver/src/infra/observe/mod.rs | 2 +- .../driver/src/tests/cases/example_config.rs | 6 +- crates/driver/src/tests/setup/blockchain.rs | 38 ++- crates/driver/src/tests/setup/mod.rs | 10 + crates/driver/src/tests/setup/solver.rs | 1 - crates/e2e/tests/e2e/colocation_ethflow.rs | 310 ++++++++++++++++++ .../e2e/tests/e2e/{eth_flow.rs => ethflow.rs} | 10 +- crates/e2e/tests/e2e/main.rs | 3 +- crates/e2e/tests/e2e/refunder.rs | 2 +- 18 files changed, 598 insertions(+), 95 deletions(-) create mode 100644 crates/e2e/tests/e2e/colocation_ethflow.rs rename crates/e2e/tests/e2e/{eth_flow.rs => ethflow.rs} (98%) diff --git a/crates/contracts/src/storage_accessible.rs b/crates/contracts/src/storage_accessible.rs index e0c38b7cd4..d18b82bb94 100644 --- a/crates/contracts/src/storage_accessible.rs +++ b/crates/contracts/src/storage_accessible.rs @@ -4,8 +4,14 @@ use { crate::support::SimulateCode, ethcontract::{ common::abi, + contract::MethodBuilder, + errors::MethodError, tokens::Tokenize, - web3::types::{Bytes, CallRequest}, + web3::{ + types::{Bytes, CallRequest}, + Transport, + Web3, + }, H160, }, }; @@ -39,3 +45,38 @@ pub fn call(target: H160, code: Bytes, call: Bytes) -> CallRequest { ..Default::default() } } + +/// Simulates the specified `ethcontract::MethodBuilder` encoded as a +/// `StorageAccessible` call. +/// +/// # Panics +/// +/// Panics if: +/// - The method doesn't specify a target address or calldata +/// - The function name doesn't exist or match the method signature +/// - The contract does not have deployment code +pub async fn simulate( + web3: &Web3, + contract: ðcontract::Contract, + function_name: &str, + method: MethodBuilder, +) -> Result +where + T: Transport, + R: Tokenize, +{ + let function = contract.abi.function(function_name).unwrap(); + let code = contract.bytecode.to_bytes().unwrap(); + + let call = call(method.tx.to.unwrap(), code, method.tx.data.clone().unwrap()); + let output = web3 + .eth() + .call(call, None) + .await + .map_err(|err| MethodError::new(function, err))?; + + let tokens = function + .decode_output(&output.0) + .map_err(|err| MethodError::new(function, err))?; + R::from_token(abi::Token::Tuple(tokens)).map_err(|err| MethodError::new(function, err)) +} diff --git a/crates/driver/src/domain/competition/auction.rs b/crates/driver/src/domain/competition/auction.rs index 2d205a32e7..6295aeb021 100644 --- a/crates/driver/src/domain/competition/auction.rs +++ b/crates/driver/src/domain/competition/auction.rs @@ -3,13 +3,9 @@ use { crate::{ domain::{ competition::{self, solution}, - eth::{self}, - }, - infra::{ - blockchain, - observe::{self}, - Ethereum, + eth, }, + infra::{blockchain, observe, Ethereum}, }, futures::future::join_all, itertools::Itertools, @@ -97,34 +93,45 @@ impl Auction { )) }); - // Fetch balances of each token for each trader. - // Has to be separate closure due to compiler bug. - let f = |order: &competition::Order| -> (order::Trader, eth::TokenAddress) { - (order.trader(), order.sell.token) - }; - let tokens_by_trader = self.orders.iter().map(f).unique(); - let mut balances: HashMap< - (order::Trader, eth::TokenAddress), - Result, - > = join_all(tokens_by_trader.map(|(trader, token)| async move { - let contract = eth.erc20(token); - let balance = contract.balance(trader.into()).await; - ((trader, token), balance) - })) + // Collect trader/token/source/interaction tuples for fetching available + // balances. Note that we are pessimistic here, if a trader is selling + // the same token with the same source in two different orders using a + // different set of pre-interactions, then we fetch the balance as if no + // pre-interactions were specified. This is done to avoid creating + // dependencies between orders (i.e. order 1 is required for executing + // order 2) which we currently cannot express with the solver interface. + let traders = self + .orders() + .iter() + .group_by(|order| (order.trader(), order.sell.token, order.sell_token_balance)) + .into_iter() + .map(|((trader, token, source), mut orders)| { + let first = orders.next().expect("group contains at least 1 order"); + let mut others = orders; + if others.all(|order| order.pre_interactions == first.pre_interactions) { + (trader, token, source, &first.pre_interactions[..]) + } else { + (trader, token, source, Default::default()) + } + }) + .collect::>(); + + let mut balances = join_all(traders.into_iter().map( + |(trader, token, source, interactions)| async move { + let balance = eth + .erc20(token) + .tradable_balance(trader.into(), source, interactions) + .await; + ((trader, token, source), balance) + }, + )) .await .into_iter() - .collect(); + .collect::>(); self.orders.retain(|order| { - // TODO: We should use balance fetching that takes interactions into account - // from `crates/shared/src/account_balances/simulation.rs` instead of hardcoding - // an Ethflow exception. https://github.com/cowprotocol/services/issues/1595 - if Some(order.signature.signer.0) == eth.contracts().ethflow_address().map(|a| a.0) { - return true; - } - let remaining_balance = match balances - .get_mut(&(order.trader(), order.sell.token)) + .get_mut(&(order.trader(), order.sell.token, order.sell_token_balance)) .unwrap() { Ok(balance) => &mut balance.0, diff --git a/crates/driver/src/domain/competition/order/mod.rs b/crates/driver/src/domain/competition/order/mod.rs index aabf607f77..1d23341b1b 100644 --- a/crates/driver/src/domain/competition/order/mod.rs +++ b/crates/driver/src/domain/competition/order/mod.rs @@ -273,15 +273,27 @@ pub enum Kind { } /// [Balancer V2](https://docs.balancer.fi/) integration, used for settlement encoding. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] pub enum SellTokenBalance { Erc20, Internal, External, } +impl SellTokenBalance { + /// Returns the hash value for the specified source. + pub fn hash(&self) -> eth::H256 { + let name = match self { + Self::Erc20 => "erc20", + Self::Internal => "internal", + Self::External => "external", + }; + eth::H256(web3::signing::keccak256(name.as_bytes())) + } +} + /// [Balancer V2](https://docs.balancer.fi/) integration, used for settlement encoding. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] pub enum BuyTokenBalance { Erc20, Internal, diff --git a/crates/driver/src/domain/eth/mod.rs b/crates/driver/src/domain/eth/mod.rs index fc3ca4093b..ac72a55d50 100644 --- a/crates/driver/src/domain/eth/mod.rs +++ b/crates/driver/src/domain/eth/mod.rs @@ -295,7 +295,7 @@ impl From for Ether { pub struct BlockNo(pub u64); /// An onchain transaction which interacts with a smart contract. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Interaction { pub target: Address, pub value: Ether, diff --git a/crates/driver/src/infra/blockchain/contracts.rs b/crates/driver/src/infra/blockchain/contracts.rs index 0a9b449e6f..fee873ae2f 100644 --- a/crates/driver/src/infra/blockchain/contracts.rs +++ b/crates/driver/src/infra/blockchain/contracts.rs @@ -1,55 +1,73 @@ use { crate::{domain::eth, infra::blockchain::Ethereum}, ethcontract::dyns::DynWeb3, + thiserror::Error, }; -pub use crate::boundary::contracts::{GPv2Settlement, IUniswapLikeRouter, ERC20, WETH9}; - #[derive(Debug, Clone)] pub struct Contracts { settlement: contracts::GPv2Settlement, + vault_relayer: eth::ContractAddress, + vault: contracts::BalancerV2Vault, weth: contracts::WETH9, - ethflow: Option, } #[derive(Debug, Default, Clone, Copy)] pub struct Addresses { pub settlement: Option, pub weth: Option, - pub ethflow: Option, } impl Contracts { - pub(super) fn new(web3: &DynWeb3, network_id: ð::NetworkId, addresses: Addresses) -> Self { - let address = addresses - .settlement - .or_else(|| deployment_address(contracts::GPv2Settlement::raw_contract(), network_id)) - .unwrap() - .into(); - let settlement = contracts::GPv2Settlement::at(web3, address); - - let address = addresses - .weth - .or_else(|| deployment_address(contracts::WETH9::raw_contract(), network_id)) - .unwrap() - .into(); - let weth = contracts::WETH9::at(web3, address); - - // Not doing deployment information because there are separate Ethflow contracts - // for staging and production. - let ethflow = addresses.ethflow; - - Self { + pub(super) async fn new( + web3: &DynWeb3, + network_id: ð::NetworkId, + addresses: Addresses, + ) -> Result { + let address_for = |contract: ðcontract::Contract, + address: Option| { + address + .or_else(|| deployment_address(contract, network_id)) + .unwrap() + .0 + }; + + let settlement = contracts::GPv2Settlement::at( + web3, + address_for( + contracts::GPv2Settlement::raw_contract(), + addresses.settlement, + ), + ); + let vault_relayer = settlement.methods().vault_relayer().call().await?.into(); + let vault = + contracts::BalancerV2Vault::at(web3, settlement.methods().vault().call().await?); + + let weth = contracts::WETH9::at( + web3, + address_for(contracts::WETH9::raw_contract(), addresses.weth), + ); + + Ok(Self { settlement, + vault_relayer, + vault, weth, - ethflow, - } + }) } pub fn settlement(&self) -> &contracts::GPv2Settlement { &self.settlement } + pub fn vault_relayer(&self) -> eth::ContractAddress { + self.vault_relayer + } + + pub fn vault(&self) -> &contracts::BalancerV2Vault { + &self.vault + } + pub fn weth(&self) -> &contracts::WETH9 { &self.weth } @@ -57,10 +75,6 @@ impl Contracts { pub fn weth_address(&self) -> eth::WethAddress { self.weth.address().into() } - - pub fn ethflow_address(&self) -> Option { - self.ethflow - } } /// Returns the address of a contract for the specified network, or `None` if @@ -77,14 +91,26 @@ pub trait ContractAt { fn at(eth: &Ethereum, address: eth::ContractAddress) -> Self; } -impl ContractAt for IUniswapLikeRouter { +impl ContractAt for contracts::IUniswapLikeRouter { fn at(eth: &Ethereum, address: eth::ContractAddress) -> Self { Self::at(ð.web3, address.0) } } -impl ContractAt for ERC20 { +impl ContractAt for contracts::ERC20 { + fn at(eth: &Ethereum, address: eth::ContractAddress) -> Self { + Self::at(ð.web3, address.into()) + } +} + +impl ContractAt for contracts::support::Balances { fn at(eth: &Ethereum, address: eth::ContractAddress) -> Self { - ERC20::at(ð.web3, address.into()) + Self::at(ð.web3, address.into()) } } + +#[derive(Debug, Error)] +pub enum Error { + #[error("method error: {0:?}")] + Method(#[from] ethcontract::errors::MethodError), +} diff --git a/crates/driver/src/infra/blockchain/mod.rs b/crates/driver/src/infra/blockchain/mod.rs index e26777ef9e..0d2742e245 100644 --- a/crates/driver/src/infra/blockchain/mod.rs +++ b/crates/driver/src/infra/blockchain/mod.rs @@ -59,7 +59,7 @@ impl Ethereum { /// Access the Ethereum blockchain through an RPC API. pub async fn new(rpc: Rpc, addresses: contracts::Addresses) -> Result { let Rpc { web3, network } = rpc; - let contracts = Contracts::new(&web3, &network.id, addresses); + let contracts = Contracts::new(&web3, &network.id, addresses).await?; let gas = Arc::new( NativeGasEstimator::new(web3.transport().clone(), None) .await @@ -166,7 +166,7 @@ impl Ethereum { /// Returns a [`token::Erc20`] for the specified address. pub fn erc20(&self, address: eth::TokenAddress) -> token::Erc20 { - token::Erc20::new(self.contract_at(address.into())) + token::Erc20::new(self, address) } } @@ -192,3 +192,11 @@ pub enum Error { #[error("web3 error returned in response: {0:?}")] Response(serde_json::Value), } + +impl From for Error { + fn from(err: contracts::Error) -> Self { + match err { + contracts::Error::Method(err) => Self::Method(err), + } + } +} diff --git a/crates/driver/src/infra/blockchain/token.rs b/crates/driver/src/infra/blockchain/token.rs index ac6b7f9ed1..b6d8414f3c 100644 --- a/crates/driver/src/infra/blockchain/token.rs +++ b/crates/driver/src/infra/blockchain/token.rs @@ -1,20 +1,32 @@ -use {super::Error, crate::domain::eth}; +use { + super::{Error, Ethereum}, + crate::domain::{competition::order, eth}, +}; /// An ERC-20 token. /// /// https://eips.ethereum.org/EIPS/eip-20 pub struct Erc20 { - contract: contracts::ERC20, + token: contracts::ERC20, + balances: contracts::support::Balances, + vault_relayer: eth::ContractAddress, + vault: eth::ContractAddress, } impl Erc20 { - pub(super) fn new(contract: contracts::ERC20) -> Self { - Self { contract } + pub(super) fn new(eth: &Ethereum, address: eth::TokenAddress) -> Self { + let settlement = eth.contracts().settlement().address().into(); + Self { + token: eth.contract_at(address.into()), + balances: eth.contract_at(settlement), + vault_relayer: eth.contracts().vault_relayer(), + vault: eth.contracts().vault().address().into(), + } } /// Returns the [`eth::TokenAddress`] of the ERC20. pub fn address(&self) -> eth::TokenAddress { - self.contract.address().into() + self.token.address().into() } /// Fetch the ERC20 allowance for the spender. See the allowance method in @@ -26,9 +38,9 @@ impl Erc20 { owner: eth::Address, spender: eth::Address, ) -> Result { - let amount = self.contract.allowance(owner.0, spender.0).call().await?; + let amount = self.token.allowance(owner.0, spender.0).call().await?; Ok(eth::Allowance { - token: self.contract.address().into(), + token: self.token.address().into(), spender, amount, } @@ -40,7 +52,7 @@ impl Erc20 { /// /// https://eips.ethereum.org/EIPS/eip-20#decimals pub async fn decimals(&self) -> Result, Error> { - match self.contract.decimals().call().await { + match self.token.decimals().call().await { Ok(decimals) => Ok(Some(decimals)), Err(err) if is_contract_error(&err) => Ok(None), Err(err) => Err(err.into()), @@ -52,7 +64,7 @@ impl Erc20 { /// /// https://eips.ethereum.org/EIPS/eip-20#symbol pub async fn symbol(&self) -> Result, Error> { - match self.contract.symbol().call().await { + match self.token.symbol().call().await { Ok(symbol) => Ok(Some(symbol)), Err(err) if is_contract_error(&err) => Ok(None), Err(err) => Err(err.into()), @@ -64,13 +76,56 @@ impl Erc20 { /// /// https://eips.ethereum.org/EIPS/eip-20#balanceof pub async fn balance(&self, holder: eth::Address) -> Result { - self.contract + self.token .balance_of(holder.0) .call() .await .map(Into::into) .map_err(Into::into) } + + /// Fetches the tradable balance for the specified user given an order's + /// pre-interactions. + pub async fn tradable_balance( + &self, + trader: eth::Address, + source: order::SellTokenBalance, + interactions: &[eth::Interaction], + ) -> Result { + let (_, _, effective_balance, can_transfer) = contracts::storage_accessible::simulate( + &self.balances.raw_instance().web3(), + contracts::support::Balances::raw_contract(), + "balance", + self.balances.balance( + ( + self.balances.address(), + self.vault_relayer.into(), + self.vault.into(), + ), + trader.into(), + self.token.address(), + 0.into(), + ethcontract::Bytes(source.hash().0), + interactions + .iter() + .map(|i| { + ( + i.target.into(), + i.value.into(), + ethcontract::Bytes(i.call_data.0.clone()), + ) + }) + .collect(), + ), + ) + .await?; + + if can_transfer { + Ok(effective_balance.into()) + } else { + Ok(eth::TokenAmount(0.into())) + } + } } /// Returns `true` if a [`ethcontract::errors::MethodError`] is the result of diff --git a/crates/driver/src/infra/config/file/load.rs b/crates/driver/src/infra/config/file/load.rs index 9bf5971f2e..953a39c3ac 100644 --- a/crates/driver/src/infra/config/file/load.rs +++ b/crates/driver/src/infra/config/file/load.rs @@ -244,7 +244,6 @@ pub async fn load(network: &blockchain::Network, path: &Path) -> infra::Config { contracts: blockchain::contracts::Addresses { settlement: config.contracts.gp_v2_settlement.map(Into::into), weth: config.contracts.weth.map(Into::into), - ethflow: config.contracts.ethflow.map(Into::into), }, disable_access_list_simulation: config.disable_access_list_simulation, disable_gas_simulation: config.disable_gas_simulation.map(Into::into), diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index edcde8420b..86b873f7df 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -188,11 +188,6 @@ struct ContractsConfig { /// Override the default address of the WETH contract. weth: Option, - - /// Sets the Ethflow contract address. Without this we cannot detect Ethflow - /// orders, which leads to such orders not being solved because it appears - /// that the user doesn't have enough sell token balance. - ethflow: Option, } #[derive(Debug, Deserialize)] diff --git a/crates/driver/src/infra/observe/mod.rs b/crates/driver/src/infra/observe/mod.rs index 6abea639aa..1f666bf20b 100644 --- a/crates/driver/src/infra/observe/mod.rs +++ b/crates/driver/src/infra/observe/mod.rs @@ -324,5 +324,5 @@ pub fn order_excluded_from_auction( order: &competition::Order, reason: OrderExcludedFromAuctionReason, ) { - tracing::trace!(uid=?order.uid, ?reason,"order excluded from auction"); + tracing::trace!(uid=?order.uid, ?reason, "order excluded from auction"); } diff --git a/crates/driver/src/tests/cases/example_config.rs b/crates/driver/src/tests/cases/example_config.rs index bf96dac431..6e33e56487 100644 --- a/crates/driver/src/tests/cases/example_config.rs +++ b/crates/driver/src/tests/cases/example_config.rs @@ -6,5 +6,9 @@ use crate::tests; #[ignore] async fn test() { let example_config_file = std::env::current_dir().unwrap().join("example.toml"); - tests::setup().config(example_config_file).done().await; + tests::setup() + .config(example_config_file) + .settlement_address("0x9008D19f58AAbD9eD0D60971565AA8510560ab41") + .done() + .await; } diff --git a/crates/driver/src/tests/setup/blockchain.rs b/crates/driver/src/tests/setup/blockchain.rs index 72850c7aa5..dd041aca84 100644 --- a/crates/driver/src/tests/setup/blockchain.rs +++ b/crates/driver/src/tests/setup/blockchain.rs @@ -11,6 +11,7 @@ use { ethcontract::{dyns::DynWeb3, transport::DynTransport, Web3}, futures::Future, secp256k1::SecretKey, + serde_json::json, std::collections::HashMap, }; @@ -144,6 +145,7 @@ pub struct Config { pub solver_address: eth::H160, pub solver_secret_key: SecretKey, pub fund_solver: bool, + pub settlement_address: Option, } impl Blockchain { @@ -249,7 +251,7 @@ impl Blockchain { ) .await .unwrap(); - let settlement = wait_for( + let mut settlement = wait_for( &web3, contracts::GPv2Settlement::builder(&web3, authenticator.address(), vault.address()) .from(trader_account.clone()) @@ -257,6 +259,27 @@ impl Blockchain { ) .await .unwrap(); + if let Some(settlement_address) = config.settlement_address { + let vault_relayer = settlement.vault_relayer().call().await.unwrap(); + let vault_relayer_code = { + // replace the vault relayer code to allow the settlement + // contract at a specific address. + let mut code = web3.eth().code(vault_relayer, None).await.unwrap().0; + for i in 0..code.len() - 20 { + let window = &mut code[i..][..20]; + if window == settlement.address().0 { + window.copy_from_slice(&settlement_address.0); + } + } + code + }; + let settlement_code = web3.eth().code(settlement.address(), None).await.unwrap().0; + + set_code(&web3, vault_relayer, &vault_relayer_code).await; + set_code(&web3, settlement_address, &settlement_code).await; + + settlement = contracts::GPv2Settlement::at(&web3, settlement_address); + } wait_for( &web3, authenticator @@ -781,3 +804,16 @@ pub async fn wait_for(web3: &DynWeb3, fut: impl Future) -> T { .expect("timeout while waiting for next block to be mined"); result } + +/// Sets code at a specific address for testing. +pub async fn set_code(web3: &DynWeb3, address: eth::H160, code: &[u8]) { + use web3::Transport; + + web3.transport() + .execute( + "anvil_setCode", + vec![json!(address), json!(format!("0x{}", hex::encode(code)))], + ) + .await + .unwrap(); +} diff --git a/crates/driver/src/tests/setup/mod.rs b/crates/driver/src/tests/setup/mod.rs index 76e0a9f7b3..351439aa15 100644 --- a/crates/driver/src/tests/setup/mod.rs +++ b/crates/driver/src/tests/setup/mod.rs @@ -263,6 +263,7 @@ pub fn setup() -> Setup { quote: Default::default(), fund_solver: true, enable_simulation: true, + settlement_address: Default::default(), } } @@ -280,6 +281,8 @@ pub struct Setup { fund_solver: bool, /// Should simulation be enabled? True by default. enable_simulation: bool, + /// Ensure the settlement contract is deployed on a specific address? + settlement_address: Option, } /// The validity of a solution. @@ -504,6 +507,12 @@ impl Setup { self } + /// Ensure that the settlement contract is deployed to a specific address. + pub fn settlement_address(mut self, address: &str) -> Self { + self.settlement_address = Some(address.parse().unwrap()); + self + } + /// Create the test: set up onchain contracts and pools, start a mock HTTP /// server for the solver and start the HTTP server for the driver. pub async fn done(self) -> Test { @@ -553,6 +562,7 @@ impl Setup { solver_address, solver_secret_key, fund_solver: self.fund_solver, + settlement_address: self.settlement_address, }) .await; let mut solutions = Vec::new(); diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index b76b29cc58..4dd854111a 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -199,7 +199,6 @@ impl Solver { Addresses { settlement: Some(config.blockchain.settlement.address().into()), weth: Some(config.blockchain.weth.address().into()), - ethflow: config.blockchain.ethflow, }, ) .await diff --git a/crates/e2e/tests/e2e/colocation_ethflow.rs b/crates/e2e/tests/e2e/colocation_ethflow.rs new file mode 100644 index 0000000000..dda9e23d1f --- /dev/null +++ b/crates/e2e/tests/e2e/colocation_ethflow.rs @@ -0,0 +1,310 @@ +use { + crate::ethflow::{EthFlowOrderOnchainStatus, EthFlowTradeIntent, ExtendedEthFlowOrder}, + autopilot::database::onchain_order_events::ethflow_events::WRAP_ALL_SELECTOR, + contracts::ERC20Mintable, + e2e::setup::*, + ethcontract::{Account, H160, U256}, + ethrpc::{current_block::timestamp_of_current_block_in_seconds, Web3}, + model::{ + order::{EthflowData, OnchainOrderData, Order, OrderClass, OrderUid}, + quote::{OrderQuoteRequest, OrderQuoteResponse, OrderQuoteSide}, + trade::Trade, + }, + reqwest::Client, +}; + +const DAI_PER_ETH: u32 = 1_000; + +#[tokio::test] +#[ignore] +async fn local_node_eth_flow() { + run_test(eth_flow_tx).await; +} + +async fn eth_flow_tx(web3: Web3) { + let mut onchain = OnchainComponents::deploy(web3.clone()).await; + + let [solver] = onchain.make_solvers(to_wei(2)).await; + let [trader] = onchain.make_accounts(to_wei(2)).await; + + // Create token with Uniswap pool for price estimation + let [dai] = onchain + .deploy_tokens_with_weth_uni_v2_pools(to_wei(DAI_PER_ETH * 1_000), to_wei(1_000)) + .await; + + // Get a quote from the services + let buy_token = dai.address(); + let receiver = H160([0x42; 20]); + let sell_amount = to_wei(1); + let intent = EthFlowTradeIntent { + sell_amount, + buy_token, + receiver, + }; + + let solver_endpoint = colocation::start_solver(onchain.contracts().weth.address()).await; + colocation::start_driver(onchain.contracts(), &solver_endpoint, &solver); + + let services = Services::new(onchain.contracts()).await; + services.start_autopilot(vec![ + "--enable-colocation=true".to_string(), + "--drivers=http://localhost:11088/test_solver".to_string(), + ]); + services.start_api(vec![]).await; + + let quote: OrderQuoteResponse = test_submit_quote( + &services, + &intent.to_quote_request(&onchain.contracts().ethflow, &onchain.contracts().weth), + ) + .await; + + let valid_to = chrono::offset::Utc::now().timestamp() as u32 + + timestamp_of_current_block_in_seconds(&web3).await.unwrap() + + 3600; + let ethflow_order = + ExtendedEthFlowOrder::from_quote("e, valid_to).include_slippage_bps(300); + + sumbit_order(ðflow_order, trader.account(), onchain.contracts()).await; + + test_order_availability_in_api( + &services, + ðflow_order, + &trader.address(), + onchain.contracts(), + ) + .await; + + tracing::info!("waiting for trade"); + wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 1 }) + .await + .unwrap(); + + test_order_was_settled(&services, ðflow_order, &web3).await; + + test_trade_availability_in_api( + services.client(), + ðflow_order, + &trader.address(), + onchain.contracts(), + ) + .await; +} + +async fn test_submit_quote( + services: &Services<'_>, + quote: &OrderQuoteRequest, +) -> OrderQuoteResponse { + let response = services.submit_quote(quote).await.unwrap(); + + assert!(response.id.is_some()); + // Ideally the fee would be nonzero, but this is not the case in the test + // environment assert_ne!(response.quote.fee_amount, 0.into()); + // Amount is reasonable (±10% from real price) + let approx_output: U256 = response.quote.sell_amount * DAI_PER_ETH; + assert!(response.quote.buy_amount.gt(&(approx_output * 9u64 / 10))); + assert!(response.quote.buy_amount.lt(&(approx_output * 11u64 / 10))); + + let OrderQuoteSide::Sell { + sell_amount: + model::quote::SellAmount::AfterFee { + value: sell_amount_after_fees, + }, + } = quote.side + else { + panic!("untested!"); + }; + + assert_eq!(response.quote.sell_amount, sell_amount_after_fees.get()); + + response +} + +async fn sumbit_order(ethflow_order: &ExtendedEthFlowOrder, user: &Account, contracts: &Contracts) { + assert_eq!( + ethflow_order.status(contracts).await, + EthFlowOrderOnchainStatus::Free + ); + + let result = ethflow_order + .mine_order_creation(user, &contracts.ethflow) + .await; + assert_eq!(result.as_receipt().unwrap().status, Some(1.into())); + assert_eq!( + ethflow_order.status(contracts).await, + EthFlowOrderOnchainStatus::Created(user.address(), ethflow_order.0.valid_to) + ); +} + +async fn test_order_availability_in_api( + services: &Services<'_>, + order: &ExtendedEthFlowOrder, + owner: &H160, + contracts: &Contracts, +) { + tracing::info!("Waiting for order to show up in API."); + let uid = order.uid(contracts).await; + let is_available = || async { services.get_order(&uid).await.is_ok() }; + wait_for_condition(TIMEOUT, is_available).await.unwrap(); + + test_orders_query(services, order, owner, contracts).await; + + // Api returns eth flow orders for both eth-flow contract address and actual + // owner + for address in [owner, &contracts.ethflow.address()] { + test_account_query(address, services.client(), order, owner, contracts).await; + } + + wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 1 }) + .await + .unwrap(); + + test_auction_query(services, order, owner, contracts).await; +} + +async fn test_trade_availability_in_api( + client: &Client, + order: &ExtendedEthFlowOrder, + owner: &H160, + contracts: &Contracts, +) { + test_trade_query( + &TradeQuery::ByUid(order.uid(contracts).await), + client, + contracts, + ) + .await; + + // Api returns eth flow orders for both eth-flow contract address and actual + // owner + for address in [owner, &contracts.ethflow.address()] { + test_trade_query(&TradeQuery::ByOwner(*address), client, contracts).await; + } +} + +async fn test_order_was_settled( + services: &Services<'_>, + ethflow_order: &ExtendedEthFlowOrder, + web3: &Web3, +) { + let auction_is_empty = || async { services.solvable_orders().await == 0 }; + wait_for_condition(TIMEOUT, auction_is_empty).await.unwrap(); + + let buy_token = ERC20Mintable::at(web3, ethflow_order.0.buy_token); + let receiver_buy_token_balance = buy_token + .balance_of(ethflow_order.0.receiver) + .call() + .await + .expect("Unable to get token balance"); + assert!(receiver_buy_token_balance >= ethflow_order.0.buy_amount); +} + +async fn test_orders_query( + services: &Services<'_>, + order: &ExtendedEthFlowOrder, + owner: &H160, + contracts: &Contracts, +) { + let response = services + .get_order(&order.uid(contracts).await) + .await + .unwrap(); + test_order_parameters(&response, order, owner, contracts).await; +} + +async fn test_account_query( + queried_account: &H160, + client: &Client, + order: &ExtendedEthFlowOrder, + owner: &H160, + contracts: &Contracts, +) { + let query = client + .get(&format!( + "{API_HOST}{ACCOUNT_ENDPOINT}/{queried_account:?}/orders", + )) + .send() + .await + .unwrap(); + assert_eq!(query.status(), 200); + let response = query.json::>().await.unwrap(); + assert_eq!(response.len(), 1); + test_order_parameters(&response[0], order, owner, contracts).await; +} + +async fn test_auction_query( + services: &Services<'_>, + order: &ExtendedEthFlowOrder, + owner: &H160, + contracts: &Contracts, +) { + let response = services.get_auction().await; + assert_eq!(response.auction.orders.len(), 1); + test_order_parameters(&response.auction.orders[0], order, owner, contracts).await; +} + +enum TradeQuery { + ByUid(OrderUid), + ByOwner(H160), +} + +async fn test_trade_query(query_type: &TradeQuery, client: &Client, contracts: &Contracts) { + let query = client + .get(&format!("{API_HOST}{TRADES_ENDPOINT}",)) + .query(&[match query_type { + TradeQuery::ByUid(uid) => ("orderUid", format!("{uid:?}")), + TradeQuery::ByOwner(owner) => ("owner", format!("{owner:?}")), + }]) + .send() + .await + .unwrap(); + assert_eq!(query.status(), 200); + let response = query.json::>().await.unwrap(); + assert_eq!(response.len(), 1); + + // Expected values from actual EIP1271 order instead of eth-flow order + assert_eq!(response[0].owner, contracts.ethflow.address()); + assert_eq!(response[0].sell_token, contracts.weth.address()); +} + +async fn test_order_parameters( + response: &Order, + order: &ExtendedEthFlowOrder, + owner: &H160, + contracts: &Contracts, +) { + // Expected values from actual EIP1271 order instead of eth-flow order + assert_eq!(response.data.valid_to, u32::MAX); + assert_eq!(response.metadata.owner, contracts.ethflow.address()); + assert_eq!(response.data.sell_token, contracts.weth.address()); + + // Specific parameters return the missing values + assert_eq!( + response.metadata.ethflow_data, + Some(EthflowData { + user_valid_to: order.0.valid_to as i64, + refund_tx_hash: None, + }) + ); + assert_eq!( + response.metadata.onchain_order_data, + Some(OnchainOrderData { + sender: *owner, + placement_error: None, + }) + ); + + assert_eq!(response.metadata.class, OrderClass::Market); + + assert!(order + .is_valid_cowswap_signature(&response.signature, contracts) + .await + .is_ok()); + + // Requires wrapping first + assert_eq!(response.interactions.pre.len(), 1); + assert_eq!( + response.interactions.pre[0].target, + contracts.ethflow.address() + ); + assert_eq!(response.interactions.pre[0].call_data, WRAP_ALL_SELECTOR); +} diff --git a/crates/e2e/tests/e2e/eth_flow.rs b/crates/e2e/tests/e2e/ethflow.rs similarity index 98% rename from crates/e2e/tests/e2e/eth_flow.rs rename to crates/e2e/tests/e2e/ethflow.rs index 71b0e12e33..3e8fa71300 100644 --- a/crates/e2e/tests/e2e/eth_flow.rs +++ b/crates/e2e/tests/e2e/ethflow.rs @@ -570,15 +570,15 @@ impl From<(H160, u32)> for EthFlowOrderOnchainStatus { } } -struct EthFlowTradeIntent { - sell_amount: U256, - buy_token: H160, - receiver: H160, +pub struct EthFlowTradeIntent { + pub sell_amount: U256, + pub buy_token: H160, + pub receiver: H160, } impl EthFlowTradeIntent { // How a user trade intent is converted into a quote request by the frontend - fn to_quote_request(&self, ethflow: &CoWSwapEthFlow, weth: &WETH9) -> OrderQuoteRequest { + pub fn to_quote_request(&self, ethflow: &CoWSwapEthFlow, weth: &WETH9) -> OrderQuoteRequest { OrderQuoteRequest { from: ethflow.address(), // Even if the user sells ETH, we request a quote for WETH diff --git a/crates/e2e/tests/e2e/main.rs b/crates/e2e/tests/e2e/main.rs index bae6d7eb83..198ba7f6d3 100644 --- a/crates/e2e/tests/e2e/main.rs +++ b/crates/e2e/tests/e2e/main.rs @@ -5,12 +5,13 @@ // Each of the following modules contains tests. mod app_data; +mod colocation_ethflow; mod colocation_hooks; mod colocation_partial_fill; mod colocation_univ2; mod database; -mod eth_flow; mod eth_integration; +mod ethflow; mod hooks; mod limit_orders; mod onchain_settlement; diff --git a/crates/e2e/tests/e2e/refunder.rs b/crates/e2e/tests/e2e/refunder.rs index 8b092abede..a53cc313b0 100644 --- a/crates/e2e/tests/e2e/refunder.rs +++ b/crates/e2e/tests/e2e/refunder.rs @@ -1,5 +1,5 @@ use { - crate::eth_flow::{EthFlowOrderOnchainStatus, ExtendedEthFlowOrder}, + crate::ethflow::{EthFlowOrderOnchainStatus, ExtendedEthFlowOrder}, chrono::{TimeZone, Utc}, e2e::{nodes::local_node::TestNodeApi, setup::*}, ethcontract::{H160, U256},