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},