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

Protocol fee e2e tests clean-up #2561

Merged
merged 28 commits into from
Apr 4, 2024
Merged

Conversation

squadgazzz
Copy link
Contributor

@squadgazzz squadgazzz commented Mar 22, 2024

Description

Removes exhaustive protocol fee e2e tests since tests implemented in the driver crate cover many other cases. Each protocol fee now has a single e2e test.

Fixes #2468

@squadgazzz squadgazzz force-pushed the 2468/protocol-fee-e2e-tests-cleanup branch from 4753ef3 to fc910f3 Compare March 26, 2024 10:26
@squadgazzz squadgazzz changed the title WIP: Protocol fee e2e refactoring Protocol fee e2e tests clean-up Mar 26, 2024
@squadgazzz squadgazzz marked this pull request as ready for review March 26, 2024 10:33
@squadgazzz squadgazzz requested a review from a team as a code owner March 26, 2024 10:33
.await;
}

async fn partner_fee_with_not_default_cap_sell_order_test(web3: Web3) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it intended that this fee still has a fee policy for market orders enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Partner fee should replace any configured protocol fee. Why not keep the Market fee here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Partner fee should replace any configured protocol fee.

Well, not really. It's just an implementation detail right now that it does. I'd hope that we can support multiple fee policies very soon so I'm not sure we want to explicitly test for this yet (but also no strong feeling).

Copy link
Contributor

@m-lord-renkse m-lord-renkse Mar 27, 2024

Choose a reason for hiding this comment

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

We have this task for it: #2495, hopefully we will implement it soon

Comment on lines 95 to 106
let protocol_fees = vec![
ProtocolFee {
policy: fee_policy.clone(),
policy_order_class: FeePolicyOrderClass::Market,
},
// Tests ability of providing multiple fees in the config. In fact, the Market one is used
// only since the order is in-market.
ProtocolFee {
policy: fee_policy,
policy_order_class: FeePolicyOrderClass::Limit,
},
];
Copy link
Contributor

Choose a reason for hiding this comment

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

How about making a single test case that contains a limit order with surplus fee and a market order with price improvement fee (and potentially a third order with partner fee) in a batch to make sure all fee models that are used in practice work as expected?

Copy link
Contributor Author

@squadgazzz squadgazzz Mar 26, 2024

Choose a reason for hiding this comment

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

I tried to configure out-of-market order and couldn't make it work yet. Assuming it is more involved change, wanted to tackle it in scope of the #2468 as a separate PR(probably, after the market order fees release and 0x liquidity)

Copy link
Contributor Author

@squadgazzz squadgazzz Mar 26, 2024

Choose a reason for hiding this comment

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

Also, not sure how to keep this assertion with a single batch of multiple orders. Maybe execute them sequentially.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, not sure how to keep this assertion with a single batch of multiple orders

How about using different tokens for the different orders?

I tried to configure out-of-market order and couldn't make it work yet.

The easiest way to make an out of market order "work" IMO is to create an AMM with balances A and B, get a quote and place an order this is "out of market" given that quote. Then you send a ton of A into the AMM (making the price change and the order now being executable).

@squadgazzz squadgazzz marked this pull request as draft March 28, 2024 08:54
@squadgazzz squadgazzz marked this pull request as ready for review March 28, 2024 15:58
async fn local_node_partner_fee_with_not_default_cap_sell_order_test() {
run_test(partner_fee_with_not_default_cap_sell_order_test).await;
}
let mut onchain = OnchainComponents::deploy(web3.clone()).await;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

GitHub shows a pretty ugly and misleading diff 🙈

Copy link
Contributor

@fleupold fleupold left a comment

Choose a reason for hiding this comment

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

Generally I think the test looks good, but let's please get rid of those magic numbers at the bottom and use more meaningful assertions that relate to the actual logic we are testing.

])
.await;

let market_price_improvement_order = OrderCreation {
Copy link
Contributor

Choose a reason for hiding this comment

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

How do I know this is a market order? I think it would be nice to quote this order and then use limits that are worse than the quote.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Used a slightly decreased value from the quote.

Comment on lines 226 to 244
tracing::info!("Waiting for liquidity state to update");
wait_for_condition(TIMEOUT, || async {
// Mint blocks until we evict the cached liquidity and fetch the new state.
onchain.mint_block().await;
let next = services.submit_quote(&quote_request).await.unwrap();
next.quote.buy_amount != quote.quote.buy_amount
})
.await
.unwrap();

tracing::info!("Waiting for trade.");
wait_for_condition(TIMEOUT, || async { services.solvable_orders().await == 3 })
.await
.unwrap();

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Why are all of these waits important? Couldn't we just wait for the limit order to trade? This could make this section shorter to read.

Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect specifically the auction_size == 3 condition is there to make a batch happen. But couldn't this result in a flaky test? For example is it guaranteed that none of the other 2 orders can be settled while we wait for the rebalancing to happen. I believe to make this bullet proof we might have to have a driver endpoint used for quoting and a driver endpoint for solving and only enable the solving endpoint after the auction has the expected size.

Copy link
Contributor Author

@squadgazzz squadgazzz Mar 29, 2024

Choose a reason for hiding this comment

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

Since starting the driver's solving endpoint separately isn't straightforward, replaced with the following assertion, which should probably wait until all the orders are executed and filled: 54507fc

.unwrap();
assert_approximately(
market_price_improvement.metadata.executed_surplus_fee,
202972993230628u128.into(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we actually seeing any price improvement here? Nothing about the pool changed, so this order should be executed as expected. I think we should:

  1. Get a quote for the trade and use that as limit price (expected price)
  2. Place the order
  3. Inject more liquidity into the pool
  4. Get another quote (now with expected price improvement).

Assert that executed_surplus_fee > 1/2 of the difference in the two quotes

.call()
.await
.unwrap();
assert_approximately(balance_after, 66641691187129028u128.into());
Copy link
Contributor

Choose a reason for hiding this comment

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

I think instead of using a magic number here (which we likely computed by running the test once and then blindly accepting its outcome as the right value to assert), I think a more robust test would be to assert that balance_after is > 2% of the sell amount.

This would give a more meaningful and robust assertion.

@squadgazzz squadgazzz marked this pull request as draft March 29, 2024 10:24
@squadgazzz squadgazzz marked this pull request as ready for review March 29, 2024 17:10
@squadgazzz squadgazzz requested a review from m-lord-renkse April 1, 2024 10:56
Copy link
Contributor

@fleupold fleupold left a comment

Choose a reason for hiding this comment

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

Assertions look good. I think given the size of this test, the PR would benefit from doing another "clean-up" pass, where we try to minimize code that is not strictly necessary and put ourselves into the shoes of a future maintainer that has to understand what this test does in 6 months from now (adding more context and comments).

I commented with some suggestions in line.

Comment on lines 123 to 138
let services = Services::new(onchain.contracts()).await;
let solver_endpoint =
colocation::start_baseline_solver(onchain.contracts().weth.address()).await;
colocation::start_driver(
onchain.contracts(),
vec![SolverEngine {
name: "test_solver".into(),
account: solver.clone(),
endpoint: solver_endpoint,
}],
);
services
.start_api(vec![
"--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(),
])
.await;
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason we cannot use start_protocol_with_args (shorter)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we want to batch all the orders, autopilot must be started after all the orders are created.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is there anything about the batching that is relevant to this test though?

vec![ProtocolFeesConfig(vec![protocol_fee])],
.await
.unwrap();
let partner_fee_quote_before = get_quote(
Copy link
Contributor

Choose a reason for hiding this comment

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

Does partner fee need a quote?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably, not. Removed as well as the AMM pool rebalancing for the partner fee order.

config.extend(autopilot_config);
services.start_autopilot(None, config);

tracing::info!("Rebalancing limit order AMM pool.");
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 not rebalance all pools in one go?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you mean by that?

Copy link
Contributor

Choose a reason for hiding this comment

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

Right now you have two blocks of tracing::info!("Rebalancing ..."); (line ~200 and line ~255). I think it would be cleaner to do all AMM rebalancing right after one another.

let market_quote_diff = market_quote_after
.buy_amount
.saturating_sub(market_quote_before.quote.buy_amount);
assert!(market_executed_surplus_fee_in_buy_token >= market_quote_diff * 3 / 10);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why (* 3 / 10). Can you either comment this for future maintainer to know where this value comes from without having to read 300 lines of code or factor this in a variable that is shared between the config and here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added comments.

Comment on lines 358 to 360
market_price_improvement_order.metadata.executed_surplus_fee
* market_quote_after.buy_amount
/ market_quote_after.sell_amount;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe factor into a method that can be reused.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored

/ partner_fee_quote_after.sell_amount;
assert!(
partner_fee_executed_surplus_fee_in_buy_token
>= partner_fee_quote_after.buy_amount * 2 / 100
Copy link
Contributor

Choose a reason for hiding this comment

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

If someone changes the fee policy above this test may pass because there is no upper bound check. Can we somehow approximate this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The difference here comes from the network fee, which is computed by the baseline solver. Not sure it is possible to predict the value based on the given data. The value can be hardcoded. Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

After reading the test again, I think the fact that we exactly assert on the settlement balances ensures that even if we drastically overcomputed the fee, the test would fail, so I think it's fine.

Copy link
Contributor

@MartinquaXD MartinquaXD left a comment

Choose a reason for hiding this comment

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

Code can probably be condensed a lot.

async fn local_node_volume_fee_buy_order() {
run_test(volume_fee_buy_order_test).await;
}
limit_order_token.mint(solver.address(), to_wei(1000)).await;
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably slightly nicer to do sth like:

for token in [limit_order_token, market_order_token, partner_fee_order_token] {
    // mint token to solver address
    // approve router contract
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thx, refactored.

let market_price_improvement_order = OrderCreation {
sell_token: onchain.contracts().weth.address(),
sell_amount,
buy_token: market_order_token.address(),
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Since we already have the quote it seems more error resistant to re-use the values from the quote whenever possible.
I.e. buy_token: market_quote_before.quote.buy_token or maybe even better sth like:

OrderCreation {
    buy_amount: market_quote_before.quote.buy_amount * 2 / 3,
    ..order_from_quote(market_quote_before)
}

That way it's absolutely clear that this order was created from a quote and explicitly overwrites a single value to make it work for the test.

Also since we have fixed size arrays we can probably do sth like:

let [market_quote, limit_quote, _parter_fee_quote] = [market_token, limit_token, partner_fee_token].map(|token| /*fetch quote*/);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored

.mint_token_to_weth_uni_v2_pool(&limit_order_token, to_wei(1000))
.await;

tracing::info!("Waiting for liquidity state to update");
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we still have the (maybe only theoretical) race condition that we start the autopilot before all liquidity pools are configured such that orders can be settled right away?
If so we don't have any guarantees that these orders actually get settled in the same batch like the test intends. Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like Felix beat me, and now his commit addresses this.

tracing::info!("Waiting for orders metadata update.");
let metadata_updated = || async {
onchain.mint_block().await;
let market_order_updated = services
Copy link
Contributor

Choose a reason for hiding this comment

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

Can also use an iterator.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored

.saturating_sub(limit_surplus_order.data.buy_amount);
assert!(limit_executed_surplus_fee_in_buy_token >= limit_quote_diff * 3 / 10);

let balance_after = market_order_token
Copy link
Contributor

Choose a reason for hiding this comment

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

Could also use iterators.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While iterators really help with preparing the data, it will be painful to find out which assertion fails.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I only meant to fetch the balances in a loop.

Copy link
Contributor

@fleupold fleupold left a comment

Choose a reason for hiding this comment

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

LGTM (I pushed a minor cleanup on the latest PR)

Copy link
Contributor

@fleupold fleupold left a comment

Choose a reason for hiding this comment

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

@squadgazzz squadgazzz merged commit 5e43504 into main Apr 4, 2024
9 checks passed
@squadgazzz squadgazzz deleted the 2468/protocol-fee-e2e-tests-cleanup branch April 4, 2024 08:59
@github-actions github-actions bot locked and limited conversation to collaborators Apr 4, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

chore: Remove exhaustive e2e tests for surplus fee
4 participants