Skip to content

Commit

Permalink
Allow for no liquidity limit order placement (#2854)
Browse files Browse the repository at this point in the history
# Description

We currently require all orders to have quote in order to accurately
compute fee policies (price improvement policies requires quote data).
In the absence of the quote we don't allow order placement and exclude
the order from solvable orders (inconsistently on a one-off code path
outside of the solvable orders cache where it doesn't get flagged as
"filtered" or "invalid" thus being silently ignored).

This seems unnecessarily restrictive. In the absence of a quote we could
simply assume that the limit price itself was the quote (ie apply a pure
surplus fee policy) to the order. This would allow placing limit orders
for tokens which are not yet tradables but will become tradable soon.

@sunce86 am I missing any other reason why we need a quote?

# Changes

- [x] Allow order placement if a quote wasn't found due to a pricing
error
- [x] Don't filter out orders from the auction for which we don't have a
quote
- [x] Assume the limit amounts are equal to the quote amounts for fee
policy application in case we cannot find a quote

## How to test
Introduced an e2e test showing that this works
  • Loading branch information
fleupold authored Aug 9, 2024
1 parent c48bfe0 commit ca9fc43
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 114 deletions.
17 changes: 14 additions & 3 deletions crates/autopilot/src/domain/fee/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use {
std::{collections::HashSet, str::FromStr},
};

#[derive(Debug)]
enum OrderClass {
Market,
Limit,
Expand Down Expand Up @@ -81,7 +82,7 @@ impl ProtocolFees {
pub fn apply(
&self,
order: boundary::Order,
quote: &domain::Quote,
quote: Option<domain::Quote>,
surplus_capturing_jit_order_owners: &[eth::Address],
) -> domain::Order {
let partner_fee = order
Expand Down Expand Up @@ -114,16 +115,26 @@ impl ProtocolFees {
buy: order.data.buy_amount,
fee: order.data.fee_amount,
};

// In case there is no quote, we assume 0 buy amount so that the order ends up
// being considered out of market price.
let quote = quote.unwrap_or(domain::Quote {
order_uid: order.metadata.uid.into(),
sell_amount: order.data.sell_amount.into(),
buy_amount: U256::zero().into(),
fee: order.data.fee_amount.into(),
});

let quote_ = boundary::Amounts {
sell: quote.sell_amount.into(),
buy: quote.buy_amount.into(),
fee: quote.fee.into(),
};

if self.enable_protocol_fees {
self.apply_multiple_policies(order, quote, order_, quote_, partner_fee)
self.apply_multiple_policies(order, &quote, order_, quote_, partner_fee)
} else {
self.apply_single_policy(order, quote, order_, quote_, partner_fee)
self.apply_single_policy(order, &quote, order_, quote_, partner_fee)
}
}

Expand Down
17 changes: 8 additions & 9 deletions crates/autopilot/src/solvable_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,17 +267,16 @@ impl SolvableOrdersCache {
latest_settlement_block: db_solvable_orders.latest_settlement_block,
orders: orders
.into_iter()
.filter_map(|order| {
if let Some(quote) = db_solvable_orders.quotes.get(&order.metadata.uid.into()) {
Some(self.protocol_fees.apply(order, quote, &surplus_capturing_jit_order_owners))
} else {
tracing::warn!(order_uid = %order.metadata.uid, "order is skipped, quote is missing");
None
}
.map(|order| {
let quote = db_solvable_orders
.quotes
.get(&order.metadata.uid.into())
.cloned();
self.protocol_fees
.apply(order, quote, &surplus_capturing_jit_order_owners)
})
.collect(),
prices:
prices
prices: prices
.into_iter()
.map(|(key, value)| {
Price::new(value.into()).map(|price| (eth::TokenAddress(key), price))
Expand Down
2 changes: 1 addition & 1 deletion crates/e2e/src/setup/colocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl LiquidityProvider {
[[liquidity.uniswap-v2]]
router = "{:?}"
pool-code = "{:?}"
missing-pool-cache-time = "1h"
missing-pool-cache-time = "0s"
"#,
contracts.uniswap_v2_router.address(),
contracts.default_pool_code()
Expand Down
75 changes: 75 additions & 0 deletions crates/e2e/src/setup/fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
pub struct ProtocolFeesConfig(pub Vec<ProtocolFee>);

#[derive(Clone)]
pub struct ProtocolFee {
pub policy: FeePolicyKind,
pub policy_order_class: FeePolicyOrderClass,
}

#[derive(Clone)]
pub enum FeePolicyOrderClass {
Market,
Limit,
Any,
}

impl std::fmt::Display for FeePolicyOrderClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FeePolicyOrderClass::Market => write!(f, "market"),
FeePolicyOrderClass::Limit => write!(f, "limit"),
FeePolicyOrderClass::Any => write!(f, "any"),
}
}
}

#[derive(Clone)]
pub enum FeePolicyKind {
/// How much of the order's surplus should be taken as a protocol fee.
Surplus { factor: f64, max_volume_factor: f64 },
/// How much of the order's volume should be taken as a protocol fee.
Volume { factor: f64 },
/// How much of the order's price improvement should be taken as a protocol
/// fee where price improvement is a difference between the executed price
/// and the best quote.
PriceImprovement { factor: f64, max_volume_factor: f64 },
}

impl std::fmt::Display for ProtocolFee {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let order_class_str = &self.policy_order_class.to_string();
match &self.policy {
FeePolicyKind::Surplus {
factor,
max_volume_factor,
} => write!(
f,
"surplus:{}:{}:{}",
factor, max_volume_factor, order_class_str
),
FeePolicyKind::Volume { factor } => {
write!(f, "volume:{}:{}", factor, order_class_str)
}
FeePolicyKind::PriceImprovement {
factor,
max_volume_factor,
} => write!(
f,
"priceImprovement:{}:{}:{}",
factor, max_volume_factor, order_class_str
),
}
}
}

impl std::fmt::Display for ProtocolFeesConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let fees_str = self
.0
.iter()
.map(|fee| fee.to_string())
.collect::<Vec<_>>()
.join(",");
write!(f, "--fee-policies={}", fees_str)
}
}
1 change: 1 addition & 0 deletions crates/e2e/src/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod colocation;
mod deploy;
#[macro_use]
pub mod onchain_components;
pub mod fee;
mod services;
mod solver;

Expand Down
20 changes: 15 additions & 5 deletions crates/e2e/src/setup/onchain_components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,10 +303,12 @@ impl OnchainComponents {
solvers
}

async fn deploy_tokens<const N: usize>(&self, minter: Account) -> [MintableToken; N] {
/// Deploy `N` tokens without any onchain liquidity
pub async fn deploy_tokens<const N: usize>(&self, minter: &Account) -> [MintableToken; N] {
let mut res = Vec::with_capacity(N);
for _ in 0..N {
let contract = ERC20Mintable::builder(&self.web3)
.from(minter.clone())
.deploy()
.await
.expect("MintableERC20 deployment failed");
Expand All @@ -333,9 +335,19 @@ impl OnchainComponents {
.expect("getting accounts failed")[0],
None,
);
let tokens = self.deploy_tokens::<N>(minter).await;
let tokens = self.deploy_tokens::<N>(&minter).await;
self.seed_weth_uni_v2_pools(tokens.iter(), token_amount, weth_amount)
.await;
tokens
}

for MintableToken { contract, minter } in &tokens {
pub async fn seed_weth_uni_v2_pools(
&self,
tokens: impl IntoIterator<Item = &MintableToken>,
token_amount: U256,
weth_amount: U256,
) {
for MintableToken { contract, minter } in tokens {
tx!(minter, contract.mint(minter.address(), token_amount));
tx_value!(minter, weth_amount, self.contracts.weth.deposit());

Expand Down Expand Up @@ -369,8 +381,6 @@ impl OnchainComponents {
)
);
}

tokens
}

/// Mints `amount` tokens to its `token`-WETH Uniswap V2 pool.
Expand Down
116 changes: 116 additions & 0 deletions crates/e2e/tests/e2e/limit_orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use {
driver::domain::eth::NonZeroU256,
e2e::{nodes::forked_node::ForkedNodeApi, setup::*, tx},
ethcontract::{prelude::U256, H160},
fee::{FeePolicyOrderClass, ProtocolFee, ProtocolFeesConfig},
model::{
order::{OrderClass, OrderCreation, OrderKind},
quote::{OrderQuoteRequest, OrderQuoteSide, SellAmount},
Expand Down Expand Up @@ -37,6 +38,12 @@ async fn local_node_limit_does_not_apply_to_in_market_orders_test() {
run_test(limit_does_not_apply_to_in_market_orders_test).await;
}

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

/// The block number from which we will fetch state for the forked tests.
const FORK_BLOCK_MAINNET: u64 = 18477910;
/// USDC whale address as per [FORK_BLOCK_MAINNET].
Expand Down Expand Up @@ -695,3 +702,112 @@ async fn forked_gnosis_single_limit_order_test(web3: Web3) {
assert!(sell_token_balance_before > sell_token_balance_after);
assert!(buy_token_balance_after >= buy_token_balance_before + to_wei(500));
}

async fn no_liquidity_limit_order(web3: Web3) {
let mut onchain = OnchainComponents::deploy(web3.clone()).await;

let [solver] = onchain.make_solvers(to_wei(10_000)).await;
let [trader_a] = onchain.make_accounts(to_wei(1)).await;
let [token_a] = onchain.deploy_tokens(solver.account()).await;

// Fund trader accounts
token_a.mint(trader_a.address(), to_wei(10)).await;

// Approve GPv2 for trading
tx!(
trader_a.account(),
token_a.approve(onchain.contracts().allowance, to_wei(10))
);

// Setup services
let protocol_fees_config = ProtocolFeesConfig(vec![
ProtocolFee {
policy: fee::FeePolicyKind::Surplus {
factor: 0.5,
max_volume_factor: 0.01,
},
policy_order_class: FeePolicyOrderClass::Limit,
},
ProtocolFee {
policy: fee::FeePolicyKind::PriceImprovement {
factor: 0.5,
max_volume_factor: 0.01,
},
policy_order_class: FeePolicyOrderClass::Market,
},
])
.to_string();

let services = Services::new(onchain.contracts()).await;
services
.start_protocol_with_args(
ExtraServiceArgs {
autopilot: vec![
protocol_fees_config,
"--enable-multiple-fees=true".to_string(),
],
..Default::default()
},
solver,
)
.await;

// Place order
let order = OrderCreation {
sell_token: token_a.address(),
sell_amount: to_wei(10),
buy_token: onchain.contracts().weth.address(),
buy_amount: to_wei(1),
valid_to: model::time::now_in_epoch_seconds() + 300,
kind: OrderKind::Sell,
..Default::default()
}
.sign(
EcdsaSigningScheme::Eip712,
&onchain.contracts().domain_separator,
SecretKeyRef::from(&SecretKey::from_slice(trader_a.private_key()).unwrap()),
);
let order_id = services.create_order(&order).await.unwrap();
let limit_order = services.get_order(&order_id).await.unwrap();
assert_eq!(limit_order.metadata.class, OrderClass::Limit);

// Create liquidity
onchain
.seed_weth_uni_v2_pools([&token_a].iter().copied(), to_wei(1000), to_wei(1000))
.await;

// Drive solution
tracing::info!("Waiting for trade.");
let balance_before = onchain
.contracts()
.weth
.balance_of(trader_a.address())
.call()
.await
.unwrap();
wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 1 })
.await
.unwrap();

wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 0 })
.await
.unwrap();

let balance_after = onchain
.contracts()
.weth
.balance_of(trader_a.address())
.call()
.await
.unwrap();
assert!(balance_after.checked_sub(balance_before).unwrap() >= to_wei(5));

let trades = services.get_trades(&order_id).await.unwrap();
assert_eq!(
trades.first().unwrap().fee_policies,
vec![model::fee_policy::FeePolicy::Surplus {
factor: 0.5,
max_volume_factor: 0.01
}],
);
}
Loading

0 comments on commit ca9fc43

Please sign in to comment.