Skip to content

Commit

Permalink
Simulate Balances with PreInteractions In Driver (#1894)
Browse files Browse the repository at this point in the history
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.


<details><summary>You can also apply this patch to make the test
fail:</summary>

```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::<Vec<_>>();
 
         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)
             },

```

</details>
  • Loading branch information
Nicholas Rodrigues Lordello authored Sep 27, 2023
1 parent 7cbd3d0 commit 6fef562
Show file tree
Hide file tree
Showing 18 changed files with 598 additions and 95 deletions.
43 changes: 42 additions & 1 deletion crates/contracts/src/storage_accessible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
Expand Down Expand Up @@ -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<T, R>(
web3: &Web3<T>,
contract: &ethcontract::Contract,
function_name: &str,
method: MethodBuilder<T, R>,
) -> Result<R, MethodError>
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))
}
65 changes: 36 additions & 29 deletions crates/driver/src/domain/competition/auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<eth::TokenAmount, crate::infra::blockchain::Error>,
> = 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::<Vec<_>>();

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::<HashMap<_, _>>();

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,
Expand Down
16 changes: 14 additions & 2 deletions crates/driver/src/domain/competition/order/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/driver/src/domain/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ impl From<i32> 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,
Expand Down
92 changes: 59 additions & 33 deletions crates/driver/src/infra/blockchain/contracts.rs
Original file line number Diff line number Diff line change
@@ -1,66 +1,80 @@
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<eth::ContractAddress>,
}

#[derive(Debug, Default, Clone, Copy)]
pub struct Addresses {
pub settlement: Option<eth::ContractAddress>,
pub weth: Option<eth::ContractAddress>,
pub ethflow: Option<eth::ContractAddress>,
}

impl Contracts {
pub(super) fn new(web3: &DynWeb3, network_id: &eth::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: &eth::NetworkId,
addresses: Addresses,
) -> Result<Self, Error> {
let address_for = |contract: &ethcontract::Contract,
address: Option<eth::ContractAddress>| {
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
}

pub fn weth_address(&self) -> eth::WethAddress {
self.weth.address().into()
}

pub fn ethflow_address(&self) -> Option<eth::ContractAddress> {
self.ethflow
}
}

/// Returns the address of a contract for the specified network, or `None` if
Expand All @@ -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(&eth.web3, address.0)
}
}

impl ContractAt for ERC20 {
impl ContractAt for contracts::ERC20 {
fn at(eth: &Ethereum, address: eth::ContractAddress) -> Self {
Self::at(&eth.web3, address.into())
}
}

impl ContractAt for contracts::support::Balances {
fn at(eth: &Ethereum, address: eth::ContractAddress) -> Self {
ERC20::at(&eth.web3, address.into())
Self::at(&eth.web3, address.into())
}
}

#[derive(Debug, Error)]
pub enum Error {
#[error("method error: {0:?}")]
Method(#[from] ethcontract::errors::MethodError),
}
12 changes: 10 additions & 2 deletions crates/driver/src/infra/blockchain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl Ethereum {
/// Access the Ethereum blockchain through an RPC API.
pub async fn new(rpc: Rpc, addresses: contracts::Addresses) -> Result<Self, Error> {
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
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -192,3 +192,11 @@ pub enum Error {
#[error("web3 error returned in response: {0:?}")]
Response(serde_json::Value),
}

impl From<contracts::Error> for Error {
fn from(err: contracts::Error) -> Self {
match err {
contracts::Error::Method(err) => Self::Method(err),
}
}
}
Loading

0 comments on commit 6fef562

Please sign in to comment.