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

Partially fillable orders in driver tests #2512

Merged
merged 20 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
65 changes: 64 additions & 1 deletion crates/driver/src/tests/cases/protocol_fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ use crate::{
ab_solution,
fee::{Policy, Quote},
ExpectedOrderAmounts,
Partial,
Test,
},
},
};

#[derive(Clone)]
struct Amounts {
sell: eth::U256,
buy: eth::U256,
Expand Down Expand Up @@ -50,13 +52,40 @@ async fn protocol_fee_test_case(test_case: TestCase) {
.sell_amount(test_case.execution.solver.sell)
.buy_amount(test_case.execution.solver.buy);
let pool = ab_adjusted_pool(quote);
// Check if the order is expected to be partially filled by calculating a
// difference between solver's and order's target amounts
let partially_executed = match test_case.order.side {
order::Side::Sell => test_case
.order
.sell_amount
.saturating_sub(test_case.execution.solver.sell),
order::Side::Buy => test_case
.order
.buy_amount
.saturating_sub(test_case.execution.solver.buy),
};
// If there is a difference, the order is considered to be partially fillable
let partially_executed: Option<eth::U256> =
(!partially_executed.is_zero()).then_some(partially_executed);
let partial = match partially_executed {
Some(executed) => Partial::Yes { executed },
None => Partial::No,
};
// Target amount to be executed by the solver in case of partially fillable
// order
let executed = match partial {
Partial::Yes { .. } => match test_case.order.side {
order::Side::Buy => Some(test_case.execution.solver.buy),
order::Side::Sell => Some(test_case.execution.solver.sell),
},
Partial::No => None,
};

// Amounts expected to be returned by the driver after fee processing
let expected_amounts = ExpectedOrderAmounts {
sell: test_case.execution.driver.sell,
buy: test_case.execution.driver.buy,
};

let order = ab_order()
.kind(order::Kind::Limit)
.sell_amount(test_case.order.sell_amount)
Expand All @@ -67,6 +96,8 @@ async fn protocol_fee_test_case(test_case: TestCase) {
.solver_fee(Some(test_case.execution.driver.sell / 100))
.side(test_case.order.side)
.fee_policy(test_case.fee_policy)
.executed(executed)
.partial(partial)
// Surplus is configured explicitly via executed/quoted amounts
.no_surplus()
.expected_amounts(expected_amounts);
Expand Down Expand Up @@ -114,6 +145,38 @@ async fn surplus_protocol_fee_buy_order_not_capped() {
protocol_fee_test_case(test_case).await;
}

#[tokio::test]
#[ignore]
async fn surplus_protocol_fee_buy_partial_order() {
let fee_policy = Policy::Surplus {
factor: 0.5,
// high enough so we don't get capped by volume fee
max_volume_factor: 1.0,
};
let test_case = TestCase {
fee_policy,
order: Order {
sell_amount: 50.ether().into_wei(),
buy_amount: 40.ether().into_wei(),
side: order::Side::Buy,
},
execution: Execution {
// 6 ETH surplus in sell token (after network fee), half of which is kept by the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simplify this example by using more round fractions (e.g. have the order be ratiod 1:1 or 1:2 and partially match it 50%)?

This way it's easy to see the surplus (here you need to do some non-trivial math)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified the amounts.

// protocol
solver: Amounts {
sell: 41.ether().into_wei(),
buy: 28.ether().into_wei(),
},
driver: Amounts {
sell: 38.ether().into_wei(),
buy: 28.ether().into_wei(),
},
},
};

protocol_fee_test_case(test_case).await;
}

#[tokio::test]
#[ignore]
async fn surplus_protocol_fee_sell_order_not_capped() {
Expand Down
47 changes: 46 additions & 1 deletion crates/driver/src/tests/setup/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,40 @@ impl QuotedOrder {
}
}

/// Calculates buy amount that should be received from a driver
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean? Why is this necessary and different to e.g. executed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

pub fn driver_buy_amount(&self) -> eth::U256 {
let executed_buy = match self.order.side {
order::Side::Buy => self.order.executed.unwrap_or(self.buy),
order::Side::Sell => self
.order
.executed
// Since `executed` is a target amount
.map(|executed_sell| self.buy * executed_sell / self.sell)
.unwrap_or(self.buy),
};
match self.order.side {
order::Side::Buy => executed_buy,
order::Side::Sell => executed_buy / self.order.surplus_factor,
}
}

/// Calculates sell amount that should be received from a driver
pub fn driver_sell_amount(&self) -> eth::U256 {
let executed_sell = match self.order.side {
order::Side::Sell => self.order.executed.unwrap_or(self.sell),
order::Side::Buy => self
.order
.executed
// Since `executed` is a target amount
.map(|executed_buy| self.sell * executed_buy / self.buy)
.unwrap_or(self.sell),
};
match self.order.side {
order::Side::Buy => executed_sell * self.order.surplus_factor,
order::Side::Sell => executed_sell,
}
}

/// The UID of the order.
pub fn order_uid(&self, blockchain: &Blockchain) -> tests::boundary::OrderUid {
self.boundary(blockchain).uid()
Expand Down Expand Up @@ -561,7 +595,7 @@ impl Blockchain {
/// Compute the execution of an order given the available liquidity
pub fn execution(&self, order: &Order) -> Execution {
let pair = self.find_pair(order);
let (sell, buy) = match (order.side, order.buy_amount) {
let (sell, buy) = match (order.side, order.executed.or(order.buy_amount)) {
// For buy order with explicitly specified amounts, use the buy amount
(order::Side::Buy, Some(buy_amount)) => (
pair.pool.in_given_out(Asset {
Expand All @@ -570,6 +604,17 @@ impl Blockchain {
}),
buy_amount,
),
// todo: simplify it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do. This conditional is now very hard to read. I think we should have two conditional, one checks if there is an explicit amount set (in which case we use it, using in_given_out for buys and out_given_in for sells, otherwise we use the whole sell amount out_given_in. Ideally we would even get rid of this optionality as I feel it's quite hard to understand already.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied your suggestion, thanks!

(order::Side::Sell, _) => {
let sell_amount = order.executed.unwrap_or(order.sell_amount);
(
sell_amount,
pair.pool.out_given_in(Asset {
amount: sell_amount,
token: order.sell_token,
}),
)
}
// Otherwise assume the full sell amount to compute the execution
(_, _) => (
order.sell_amount,
Expand Down
13 changes: 11 additions & 2 deletions crates/driver/src/tests/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ pub struct Order {
/// buy amount is divided depends on the order side. This is necessary to
/// keep the solution scores positive.
pub surplus_factor: eth::U256,
/// Override the executed amount of the order. Useful for testing liquidity
/// orders. Otherwise [`execution_diff`] is probably more suitable.
/// Override the executed target amount of the order. Useful for testing
/// liquidity orders. Otherwise [`execution_diff`] is probably more
/// suitable.
Comment on lines +131 to +133
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not testing liquidity order though, are we? I wonder if executed is even needed or if we can instead use ab_adjusted_pool(quote) to control how much is executed. Have you checked that?

Copy link
Contributor Author

@squadgazzz squadgazzz Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean to pass order's full amount into this function?

pub fn out_given_in(&self, input: Asset) -> eth::U256 {

That doesn't work since ab_adjusted_pool function adjusts one of the pool's reserves to align with the quote's ratio. For different inputs, the pool returns different results.

pub executed: Option<eth::U256>,
/// Provides explicit expected order executed amounts.
pub expected_amounts: Option<ExpectedOrderAmounts>,
Expand Down Expand Up @@ -253,6 +254,14 @@ impl Order {
}
}

pub fn partial(self, partial: Partial) -> Self {
Self { partial, ..self }
}

pub fn executed(self, executed: Option<eth::U256>) -> Self {
Self { executed, ..self }
}

fn surplus_fee(&self) -> eth::U256 {
match self.kind {
order::Kind::Limit => self.solver_fee.unwrap_or_default(),
Expand Down
4 changes: 2 additions & 2 deletions crates/driver/src/tests/setup/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ impl Solver {
"buyToken": hex_address(config.blockchain.get_token(buy_token)),
"sellAmount": match quote.order.side {
order::Side::Buy if config.quote => "22300745198530623141535718272648361505980416".to_owned(),
_ => quote.sell_amount().to_string(),
_ => quote.driver_sell_amount().to_string(),
},
"buyAmount": match quote.order.side {
order::Side::Sell if config.quote => "1".to_owned(),
_ => quote.buy_amount().to_string(),
_ => quote.driver_buy_amount().to_string(),
},
"feeAmount": quote.order.user_fee.to_string(),
"kind": match quote.order.side {
Expand Down
Loading