Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Driver] Allow stale liquidity for quote requests #1924

Merged
merged 4 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions crates/driver/src/boundary/liquidity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ impl Fetcher {
pub async fn fetch(
&self,
pairs: &HashSet<liquidity::TokenPair>,
block: infra::liquidity::AtBlock,
) -> Result<Vec<liquidity::Liquidity>> {
let pairs = pairs
.iter()
Expand All @@ -137,12 +138,15 @@ impl Fetcher {
TokenPair::new(a.into(), b.into()).expect("a != b")
})
.collect();
let block_number = self.blocks.borrow().number;

let liquidity = self
.inner
.get_liquidity(pairs, recent_block_cache::Block::Number(block_number))
.await?;
let block = match block {
infra::liquidity::AtBlock::Recent => recent_block_cache::Block::Recent,
infra::liquidity::AtBlock::Latest => {
let block_number = self.blocks.borrow().number;
recent_block_cache::Block::Number(block_number)
}
};
let liquidity = self.inner.get_liquidity(pairs, block).await?;

let liquidity = liquidity
.into_iter()
Expand Down
9 changes: 8 additions & 1 deletion crates/driver/src/domain/competition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,14 @@ impl Competition {
/// Solve an auction as part of this competition.
pub async fn solve(&self, auction: &Auction) -> Result<Solved, Error> {
let liquidity = match self.solver.liquidity() {
solver::Liquidity::Fetch => self.liquidity.fetch(&auction.liquidity_pairs()).await,
solver::Liquidity::Fetch => {
self.liquidity
.fetch(
&auction.liquidity_pairs(),
infra::liquidity::AtBlock::Latest,
)
.await
}
solver::Liquidity::Skip => Default::default(),
};

Expand Down
6 changes: 5 additions & 1 deletion crates/driver/src/domain/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ impl Order {
tokens: &infra::tokens::Fetcher,
) -> Result<Quote, Error> {
let liquidity = match solver.liquidity() {
solver::Liquidity::Fetch => liquidity.fetch(&self.liquidity_pairs()).await,
solver::Liquidity::Fetch => {
liquidity
.fetch(&self.liquidity_pairs(), infra::liquidity::AtBlock::Recent)
.await
}
solver::Liquidity::Skip => Default::default(),
};
let timeout = self.deadline.timeout()?;
Expand Down
24 changes: 22 additions & 2 deletions crates/driver/src/infra/liquidity/fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ pub struct Fetcher {
inner: Arc<boundary::liquidity::Fetcher>,
}

/// Specifies at which block liquidity should be fetched.
pub enum AtBlock {
/// Fetches liquidity at a recent block. This will prefer reusing cached
/// liquidity even if it is stale by a few blocks instead of fetching the
/// absolute latest state from the blockchain.
///
/// This is useful for quoting where we want an up-to-date, but not
/// necessarily exactly correct price. In the context of quote verification,
/// this is completely fine as the exactly input and output amounts will be
/// computed anyway. At worse, we might provide a slightly sub-optimal
/// route in some cases, but this is an acceptable trade-off.
Recent,
/// Fetches liquidity liquidity for the latest state of the blockchain.
Latest,
}

impl Fetcher {
/// Creates a new liquidity fetcher for the specified Ethereum instance and
/// configuration.
Expand All @@ -25,9 +41,13 @@ impl Fetcher {

/// Fetches all relevant liquidity for the specified token pairs. Handles
/// failures by logging and returning an empty vector.
pub async fn fetch(&self, pairs: &HashSet<liquidity::TokenPair>) -> Vec<liquidity::Liquidity> {
pub async fn fetch(
&self,
pairs: &HashSet<liquidity::TokenPair>,
block: AtBlock,
) -> Vec<liquidity::Liquidity> {
observe::fetching_liquidity();
match self.inner.fetch(pairs).await {
match self.inner.fetch(pairs, block).await {
Ok(liquidity) => {
observe::fetched_liquidity(&liquidity);
liquidity
Expand Down
5 changes: 4 additions & 1 deletion crates/driver/src/infra/liquidity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@
pub mod config;
pub mod fetcher;

pub use self::{config::Config, fetcher::Fetcher};
pub use self::{
config::Config,
fetcher::{AtBlock, Fetcher},
};
41 changes: 41 additions & 0 deletions crates/e2e/src/setup/onchain_components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use {
order::Hook,
signature::{EcdsaSignature, EcdsaSigningScheme},
DomainSeparator,
TokenPair,
},
secp256k1::SecretKey,
shared::ethrpc::Web3,
Expand Down Expand Up @@ -505,6 +506,46 @@ impl OnchainComponents {
tokens
}

/// Mints `amount` tokens to its `token`-WETH Uniswap V2 pool.
///
/// This can be used to modify the pool reserves during a test.
pub async fn mint_token_to_weth_uni_v2_pool(&self, token: &MintableToken, amount: U256) {
let pair = contracts::IUniswapLikePair::at(
&self.web3,
self.contracts
.uniswap_v2_factory
.get_pair(self.contracts.weth.address(), token.address())
.call()
.await
.expect("failed to get Uniswap V2 pair"),
);
assert!(!pair.address().is_zero(), "Uniswap V2 pair is not deployed");

// Mint amount + 1 to the pool, and then swap out 1 of the minted token
// in order to force it to update its K-value.
token.mint(pair.address(), amount + 1).await;
let (out0, out1) = if TokenPair::new(self.contracts.weth.address(), token.address())
.unwrap()
.get()
.0
== token.address()
{
(1, 0)
} else {
(0, 1)
};
pair.swap(
out0.into(),
out1.into(),
token.minter.address(),
Default::default(),
)
.from(token.minter.clone())
.send()
.await
.expect("Uniswap V2 pair couldn't mint");
}

pub async fn deploy_cow_token(&self, holder: Account, supply: U256) -> CowToken {
let contract =
CowProtocolToken::builder(&self.web3, holder.address(), holder.address(), supply)
Expand Down
85 changes: 85 additions & 0 deletions crates/e2e/tests/e2e/colocation_quoting.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use {
e2e::{setup::*, tx, tx_value},
ethcontract::U256,
model::quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount},
number::nonzero::U256 as NonZeroU256,
shared::ethrpc::Web3,
};

#[tokio::test]
#[ignore]
async fn local_node_uses_stale_liquidity() {
run_test(uses_stale_liquidity).await;
}

async fn uses_stale_liquidity(web3: Web3) {
tracing::info!("Setting up chain state.");
let mut onchain = OnchainComponents::deploy(web3.clone()).await;

let [solver] = onchain.make_solvers(to_wei(10)).await;
let [trader] = onchain.make_accounts(to_wei(2)).await;
let [token] = onchain
.deploy_tokens_with_weth_uni_v2_pools(to_wei(1_000), to_wei(1_000))
.await;

tx!(
trader.account(),
onchain
.contracts()
.weth
.approve(onchain.contracts().allowance, to_wei(1))
);
tx_value!(
trader.account(),
to_wei(1),
onchain.contracts().weth.deposit()
);

tracing::info!("Starting services.");
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![
"--price-estimation-drivers=solver|http://localhost:11088/test_solver".to_string(),
])
.await;

let quote = OrderQuoteRequest {
from: trader.address(),
sell_token: onchain.contracts().weth.address(),
buy_token: token.address(),
side: OrderQuoteSide::Sell {
sell_amount: SellAmount::AfterFee {
value: NonZeroU256::new(to_wei(1)).unwrap(),
},
},
..Default::default()
};

tracing::info!("performining initial quote");
let first = services.submit_quote(&quote).await.unwrap();

// Now, we want to manually unbalance the pools and assert that the quote
// doesn't change (as the price estimation will use stale pricing data).
onchain
.mint_token_to_weth_uni_v2_pool(&token, to_wei(1_000))
.await;

tracing::info!("performining second quote, which should match first");
let second = services.submit_quote(&quote).await.unwrap();
assert_eq!(first.quote.buy_amount, second.quote.buy_amount);

tracing::info!("waiting for liquidity state to update");
wait_for_condition(TIMEOUT, || async {
let next = services.submit_quote(&quote).await.unwrap();
next.quote.buy_amount != first.quote.buy_amount
})
.await
.unwrap();
}
1 change: 1 addition & 0 deletions crates/e2e/tests/e2e/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod app_data;
mod colocation_ethflow;
mod colocation_hooks;
mod colocation_partial_fill;
mod colocation_quoting;
mod colocation_univ2;
mod database;
mod eth_integration;
Expand Down