Skip to content

Commit

Permalink
Add a forked test (#2032)
Browse files Browse the repository at this point in the history
# Description
Add a forked test to the end-to-end tests.

# Changes
- A new GitHub action which runs forked e2e tests, that can fail as
requested here:
#1862 (comment).
For now it's only mainnet but we will add a gnosis forked test after
this PR is merged.
- Where relevant, added functions which will return onchain components
that are already deployed.
 - Added more utility functions to `forked_node.rs`.
 - Added a forked test to `limit_orders.rs`.
- Added a function to create a `TokenOwnerFinder` from a web3 client so
that we can easily top-up trader accounts on a forked network.

## How to test
`test-forked-node` job. **Note:** `FORK_URL` must be added to the
secrets for this repo before this job can run.

## Related Issues

Fixes #1862

---------

Co-authored-by: Ape Dev <[email protected]>
Co-authored-by: Martin Beckmann <[email protected]>
  • Loading branch information
3 people authored Nov 10, 2023
1 parent e00a732 commit 1f0f69b
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 29 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,29 @@ jobs:
up-opts: -d db migrations
- run: cargo nextest run -p e2e local_node --test-threads 1 --failure-output final --run-ignored ignored-only

test-forked-node:
timeout-minutes: 60
runs-on: ubuntu-latest
env:
# Shrink artifact size by not including debug info. Makes build faster and shrinks cache.
CARGO_PROFILE_DEV_DEBUG: 0
CARGO_PROFILE_TEST_DEBUG: 0
CARGO_TERM_COLOR: always
FORK_URL: https://eth.merkle.io
steps:
- uses: actions/checkout@v3
- uses: foundry-rs/foundry-toolchain@v1
- uses: Swatinem/rust-cache@v2
# Start the build process in the background. The following cargo test command will automatically
# wait for the build process to be done before proceeding.
- run: cargo build -p e2e --tests &
- uses: taiki-e/install-action@nextest
- uses: yu-ichiro/spin-up-docker-compose-action@v1
with:
file: docker-compose.yaml
up-opts: -d db migrations
- run: cargo nextest run -p e2e forked_node --test-threads 1 --failure-output final --run-ignored ignored-only

test-driver:
timeout-minutes: 60
runs-on: ubuntu-latest
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,18 @@ The CI runs unit tests, e2e tests, `clippy` and `cargo fmt`

**Note:** Requires postgres database running (see below).

### E2E Tests
### E2E Tests - Local Node:

`cargo test -p e2e -- --ignored`.
`cargo test -p e2e local_node -- --ignored`.

**Note:** Requires postgres database and local test network with smart contracts deployed (see below).

### E2E Tests - Forked Node:

`FORK_URL=<mainnet archive node RPC URL> cargo test -p e2e forked_node -- --ignored`.

**Note:** Requires postgres database (see below).

### Clippy

`cargo clippy --all-features --all-targets -- -D warnings`
Expand Down
2 changes: 2 additions & 0 deletions crates/contracts/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,11 +462,13 @@ fn main() {
builder
.add_network_str(MAINNET, "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f")
.add_network_str(GOERLI, "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f")
.add_network_str(GNOSIS, "0xA818b4F111Ccac7AA31D0BCc0806d64F2E0737D7")
});
generate_contract_with_config("UniswapV2Router02", |builder| {
builder
.add_network_str(MAINNET, "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D")
.add_network_str(GOERLI, "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D")
.add_network_str(GNOSIS, "0x1b02da8cb0d097eb8d57a175b88c7d8b47997506")
});
generate_contract_with_config("UniswapV3SwapRouter", |builder| {
builder
Expand Down
24 changes: 21 additions & 3 deletions crates/e2e/src/nodes/forked_node.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use {
ethcontract::H160,
ethcontract::{Account, H160, U256},
reqwest::Url,
serde_json::json,
std::fmt::Debug,
Expand Down Expand Up @@ -36,11 +36,29 @@ impl<T: Transport> ForkedNodeApi<T> {
))
}

pub fn impersonate(&self, address: &H160) -> CallFuture<(), T::Out> {
pub async fn impersonate(&self, address: &H160) -> Result<Account, web3::Error> {
let json_address = serde_json::json!(address);
self.transport
.execute("anvil_impersonateAccount", vec![json_address])
.await?;

Ok(Account::Local(*address, None))
}

pub fn set_chain_id(&self, chain_id: u64) -> CallFuture<(), T::Out> {
let json_chain_id = serde_json::json!(chain_id);
CallFuture::new(
self.transport
.execute("anvil_setChainId", vec![json_chain_id]),
)
}

pub fn set_balance(&self, address: &H160, balance: U256) -> CallFuture<(), T::Out> {
let json_address = serde_json::json!(address);
let json_balance = serde_json::json!(format!("{:#032x}", balance));
CallFuture::new(
self.transport
.execute("anvil_impersonateAccount", vec![json_address]),
.execute("anvil_setBalance", vec![json_address, json_balance]),
)
}
}
24 changes: 19 additions & 5 deletions crates/e2e/src/nodes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,19 @@ pub struct Node {
}

impl Node {
/// Spawns a new node that is forked from the given URL.
pub async fn forked(fork: impl reqwest::IntoUrl) -> Self {
Self::spawn_process(&["--port", "8545", "--fork-url", fork.as_str()]).await
/// Spawns a new node that is forked from the given URL at `block_number` or
/// if not set, latest.
pub async fn forked(fork: impl reqwest::IntoUrl, block_number: Option<u64>) -> Self {
let mut args = ["--port", "8545", "--fork-url", fork.as_str()]
.into_iter()
.map(String::from)
.collect::<Vec<_>>();

if let Some(block_number) = block_number {
args.extend(["--fork-block-number".to_string(), block_number.to_string()]);
}

Self::spawn_process(args).await
}

/// Spawns a new local test net with some default parameters.
Expand All @@ -38,7 +48,10 @@ impl Node {
}

/// Spawn a new node instance using the list of given arguments.
async fn spawn_process(args: &[&str]) -> Self {
async fn spawn_process<T>(args: impl IntoIterator<Item = T>) -> Self
where
T: AsRef<str> + std::convert::AsRef<std::ffi::OsStr>,
{
use tokio::io::AsyncBufReadExt as _;

// Allow using some custom logic to spawn `anvil` by setting `ANVIL_COMMAND`.
Expand Down Expand Up @@ -69,10 +82,11 @@ impl Node {
}
});

let _url = tokio::time::timeout(tokio::time::Duration::from_secs(1), receiver)
let _url = tokio::time::timeout(tokio::time::Duration::from_secs(5), receiver)
.await
.expect("finding anvil URL timed out")
.unwrap();

Self {
process: Some(process),
}
Expand Down
31 changes: 31 additions & 0 deletions crates/e2e/src/setup/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,37 @@ pub struct Contracts {
}

impl Contracts {
pub async fn deployed(web3: &Web3) -> Self {
let network_id = web3.net().version().await.expect("get network ID failed");
tracing::info!("connected to forked test network {}", network_id);

let gp_settlement = GPv2Settlement::deployed(web3).await.unwrap();

Self {
balancer_vault: BalancerV2Vault::deployed(web3).await.unwrap(),
gp_authenticator: GPv2AllowListAuthentication::deployed(web3).await.unwrap(),
uniswap_v2_factory: UniswapV2Factory::deployed(web3).await.unwrap(),
uniswap_v2_router: UniswapV2Router02::deployed(web3).await.unwrap(),
weth: WETH9::deployed(web3).await.unwrap(),
allowance: gp_settlement
.vault_relayer()
.call()
.await
.expect("Couldn't get vault relayer address"),
domain_separator: DomainSeparator(
gp_settlement
.domain_separator()
.call()
.await
.expect("Couldn't query domain separator")
.0,
),
ethflow: CoWSwapEthFlow::deployed(web3).await.unwrap(),
hooks: HooksTrampoline::deployed(web3).await.unwrap(),
gp_settlement,
}
}

pub async fn deploy(web3: &Web3) -> Self {
let network_id = web3.net().version().await.expect("get network ID failed");
tracing::info!("connected to test network {}", network_id);
Expand Down
47 changes: 32 additions & 15 deletions crates/e2e/src/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ mod services;
use {
crate::nodes::{Node, NODE_HOST},
anyhow::{anyhow, Result},
ethcontract::{futures::FutureExt, H160},
ethcontract::futures::FutureExt,
shared::ethrpc::{create_test_transport, Web3},
std::{
future::Future,
Expand Down Expand Up @@ -107,29 +107,52 @@ pub async fn run_test_with_extra_filters<F, Fut, T>(
run(f, extra_filters, None).await
}

pub async fn run_forked_test<F, Fut>(f: F, solver_address: H160, fork_url: String)
pub async fn run_forked_test<F, Fut>(f: F, fork_url: String)
where
F: FnOnce(Web3) -> Fut,
Fut: Future<Output = ()>,
{
run(f, empty::<&str>(), Some((solver_address, fork_url))).await
run(f, empty::<&str>(), Some((fork_url, None))).await
}

pub async fn run_forked_test_with_block_number<F, Fut>(f: F, fork_url: String, block_number: u64)
where
F: FnOnce(Web3) -> Fut,
Fut: Future<Output = ()>,
{
run(f, empty::<&str>(), Some((fork_url, Some(block_number)))).await
}

pub async fn run_forked_test_with_extra_filters<F, Fut, T>(
f: F,
solver_address: H160,
fork_url: String,
extra_filters: impl IntoIterator<Item = T>,
) where
F: FnOnce(Web3) -> Fut,
Fut: Future<Output = ()>,
T: AsRef<str>,
{
run(f, extra_filters, Some((solver_address, fork_url))).await
run(f, extra_filters, Some((fork_url, None))).await
}

async fn run<F, Fut, T>(f: F, filters: impl IntoIterator<Item = T>, fork: Option<(H160, String)>)
where
pub async fn run_forked_test_with_extra_filters_and_block_number<F, Fut, T>(
f: F,
fork_url: String,
block_number: u64,
extra_filters: impl IntoIterator<Item = T>,
) where
F: FnOnce(Web3) -> Fut,
Fut: Future<Output = ()>,
T: AsRef<str>,
{
run(f, extra_filters, Some((fork_url, Some(block_number)))).await
}

async fn run<F, Fut, T>(
f: F,
filters: impl IntoIterator<Item = T>,
fork: Option<(String, Option<u64>)>,
) where
F: FnOnce(Web3) -> Fut,
Fut: Future<Output = ()>,
T: AsRef<str>,
Expand All @@ -144,8 +167,8 @@ where
// it but rather in the locked state.
let _lock = NODE_MUTEX.lock();

let node = match &fork {
Some((_, fork)) => Node::forked(fork).await,
let node = match fork {
Some((fork, block_number)) => Node::forked(fork, block_number).await,
None => Node::new().await,
};

Expand All @@ -159,12 +182,6 @@ where

let http = create_test_transport(NODE_HOST);
let web3 = Web3::new(http);
if let Some((solver, _)) = &fork {
Web3::api::<crate::nodes::forked_node::ForkedNodeApi<_>>(&web3)
.impersonate(solver)
.await
.unwrap();
}

services::clear_database().await;
// Hack: the closure may actually be unwind unsafe; moreover, `catch_unwind`
Expand Down
54 changes: 52 additions & 2 deletions crates/e2e/src/setup/onchain_components.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use {
crate::setup::deploy::Contracts,
crate::{nodes::forked_node::ForkedNodeApi, setup::deploy::Contracts},
contracts::{CowProtocolToken, ERC20Mintable},
ethcontract::{transaction::TransactionBuilder, Account, Bytes, PrivateKey, H160, U256},
hex_literal::hex,
Expand Down Expand Up @@ -37,8 +37,12 @@ macro_rules! tx {
};
}

pub fn to_wei_with_exp(base: u32, exp: usize) -> U256 {
U256::from(base) * U256::exp10(exp)
}

pub fn to_wei(base: u32) -> U256 {
U256::from(base) * U256::exp10(18)
to_wei_with_exp(base, 18)
}

pub async fn hook_for_transaction<T>(tx: TransactionBuilder<T>) -> Hook
Expand Down Expand Up @@ -212,6 +216,16 @@ impl OnchainComponents {
}
}

pub async fn deployed(web3: Web3) -> Self {
let contracts = Contracts::deployed(&web3).await;

Self {
web3,
contracts,
accounts: Default::default(),
}
}

/// Generate next `N` accounts with the given initial balance.
pub async fn make_accounts<const N: usize>(&mut self, with_wei: U256) -> [TestAccount; N] {
let res = self.accounts.borrow_mut().take(N).collect::<Vec<_>>();
Expand Down Expand Up @@ -241,6 +255,42 @@ impl OnchainComponents {
solvers
}

/// Generate next `N` accounts with the given initial balance and
/// authenticate them as solvers on a forked network.
pub async fn make_solvers_forked<const N: usize>(
&mut self,
with_wei: U256,
) -> [TestAccount; N] {
let auth_manager = self
.contracts
.gp_authenticator
.manager()
.call()
.await
.unwrap();

let forked_node_api = self.web3.api::<ForkedNodeApi<_>>();

let auth_manager = forked_node_api
.impersonate(&auth_manager)
.await
.expect("could not impersonate auth_manager");

let solvers = self.make_accounts::<N>(with_wei).await;

for solver in &solvers {
self.contracts
.gp_authenticator
.add_solver(solver.address())
.from(auth_manager.clone())
.send()
.await
.expect("failed to add solver");
}

solvers
}

async fn deploy_tokens<const N: usize>(&self, minter: Account) -> [MintableToken; N] {
let mut res = Vec::with_capacity(N);
for _ in 0..N {
Expand Down
Loading

0 comments on commit 1f0f69b

Please sign in to comment.