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: filter orders based on user balances #1687

Merged
merged 20 commits into from
Aug 28, 2023

Conversation

cowfee
Copy link
Contributor

@cowfee cowfee commented Jul 19, 2023

Progress on #1672.

Filters orders based on trader balances. If the traders don't have enough balance to cover the order, it will be removed before being sent to the solver.

TODO after merging:

  • Configure ethflow address in infra.
  • Extend the test.

Test Plan

Added automated test.

@cowfee cowfee requested a review from a team as a code owner July 19, 2023 11:32
@cowfee cowfee force-pushed the driver-filter-orders-from-user-balances branch from 44bf49b to 8371acb Compare July 19, 2023 11:34
Copy link
Contributor

@vkgnosis vkgnosis left a comment

Choose a reason for hiding this comment

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

This code does some things differently than the code in crates/solver/src/order_balance_filter.rs but the goal of both modules is the same so the differences should be explained.

The goal of the module (IMO) is to ensure that no matter what orders a solver picks, the resulting settlement will not be invalid because a picked order doesn't have enough balance. In order to do that we need to be pessimistic about balance assignment.

crates/driver/src/domain/competition/auction.rs Outdated Show resolved Hide resolved
crates/driver/src/domain/competition/auction.rs Outdated Show resolved Hide resolved
crates/driver/src/domain/competition/auction.rs Outdated Show resolved Hide resolved
@vkgnosis
Copy link
Contributor

I'm taking over this PR.

@vkgnosis vkgnosis marked this pull request as draft July 24, 2023 13:15
@vkgnosis vkgnosis force-pushed the driver-filter-orders-from-user-balances branch from 42a36b9 to e1cce76 Compare July 27, 2023 10:47
@vkgnosis vkgnosis force-pushed the driver-filter-orders-from-user-balances branch from cac3053 to ccb886e Compare July 27, 2023 12:12
@vkgnosis vkgnosis force-pushed the driver-filter-orders-from-user-balances branch from 2fdc6a5 to c7c4179 Compare July 28, 2023 12:12
@vkgnosis
Copy link
Contributor

This is ready for review now. The test needs to be improved to test more of the cases we care about. I didn't do this because I am uncertain about how to organize the test code right and I don't want that to block the main functionality.

@vkgnosis vkgnosis marked this pull request as ready for review July 28, 2023 12:14
@vkgnosis vkgnosis requested a review from a team July 28, 2023 12:14
@vkgnosis vkgnosis force-pushed the driver-filter-orders-from-user-balances branch from c7c4179 to a4636e2 Compare July 28, 2023 12:58
crates/driver/src/domain/competition/auction.rs Outdated Show resolved Hide resolved
Comment on lines 49 to 50
pub async fn solve(&self, auction: Auction) -> Result<Solved, Error> {
let auction = auction.prioritize(&self.eth).await;
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like changing the auction after passing it into this function results in a few edge cases.
Could this be done before hand instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

Any more thoughts on this?

crates/driver/src/tests/cases/order_prioritization.rs Outdated Show resolved Hide resolved
Comment on lines +118 to +120
// TODO: We should use balance fetching that takes interactions into account
// from `crates/shared/src/account_balances/simulation.rs` instead of hardcoding
// an Ethflow exception. https://github.com/cowprotocol/services/issues/1595
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe we already have the pre-interaction aware balance fetcher, right?
In that case it might be less effort to immediately use that instead of first introducing all the ethflow machinery for this exception and later dropping that again.

Copy link
Contributor

Choose a reason for hiding this comment

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

Leaving that unaddressed for now as things are not as straight forward than I thought.

@nlordell
Copy link
Contributor

This still needs to be merged for co-location.

@MartinquaXD MartinquaXD self-assigned this Aug 18, 2023
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.

Logic looks good to me, although a lot of the architectural design here is still quite new to me.

if !orders.iter().all(|order| {
tokens.0.contains_key(&order.buy.token.wrap(weth))
&& tokens.0.contains_key(&order.sell.token.wrap(weth))
}) {
return Err(InvalidTokens);
return Err(Error::InvalidTokens);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: may be good to at least log which tokens are missing (I imagine this being otherwise very complex to debug)

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally, I'm OK with no log in the long run:

  • We get a InvalidTokens error
  • We look at the stored auction JSON from the autopilot.

Now, that being said - I don't think we are saving the auction JSON that is being sent to the drivers for every run. I really think we should in the context of co-location (as it would want to store everything we send to externally operated drivers). Once we do store these auction JSON documents, however, I think the extra log here isn't super useful (you will always check the auction JSON anyway to check if the missing token is indeed missing, so I don't think it saves you much time).


/// [`None`] if this auction applies to a quote. See
/// [`crate::domain::quote`].
pub fn id(&self) -> Option<Id> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Separate, but solvers have been asking us to also send IDs for quote instances (so they can easily debug and get back to us with identifiable instances). One idea we had was to "pre-generate" the quote_id and then attach it to each request. This will even allow solvers to check independently what they quoted for a given order (assuming we expose quote_id in our API)

Copy link
Contributor

@nlordell nlordell Aug 18, 2023

Choose a reason for hiding this comment

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

I think some solvers use the id field to disambiguate between a solve and a quote (I think Quasimodo does this specifically). This might be a backwards incompatible change (just warning).

Just to clarify, I 100% agree we should do this - just wanted to bring that up to keep in mind.

Also note that this id is the id of an auction to solve. Driver's have a separate API for handling quotes.

Comment on lines +160 to +165
let sell = remaining
.remaining(order.sell.amount.0)
.context("remaining_sell")?;
let fee = remaining
.remaining(order.fee.user.0)
.context("remaining_fee")?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Honestly it's not really clear to me what remaining.remaining is supposed to do, but I guess one is expected to dig into the code to understand this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally you shouldn't have to dig into the code. But the naming for this type and function is not straight forward.
Basically the idea is that the remaining variable holds all the information needed to know how much of an order was executed. And the remaining method then allows you to scale the initial amount to the amount that is currently left in the order.

Comment on lines 49 to 50
pub async fn solve(&self, auction: Auction) -> Result<Solved, Error> {
let auction = auction.prioritize(&self.eth).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 more thoughts on this?

/// Sets the Ethflow contract address. Without this we cannot detect Ethflow
/// orders, which leads to such orders not being solved because it appears
/// that the user doesn't have enough sell token balance.
pub ethflow: Option<eth::H160>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be mandatory?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, we can make this mandatory. We just have to make sure the required infra changes are already there before merging the PR to avoid panics.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would like to avoid this configuration change if possible. I think it should be possible if we correctly simulate balances with pre-interactions for orders.

Comment on lines 75 to 79
// Prioritize the orders such that those which are more likely to be fulfilled
// come before less likely orders. Filter out orders which the trader doesn't
// have enough balance to pay for.
//
// Prioritization is skipped during quoting. It's only used during competition.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Prioritize the orders such that those which are more likely to be fulfilled
// come before less likely orders. Filter out orders which the trader doesn't
// have enough balance to pay for.
//
// Prioritization is skipped during quoting. It's only used during competition.
/// Prioritize the orders such that those which are more likely to be fulfilled
/// come before less likely orders. Filter out orders which the trader doesn't
/// have enough balance to pay for.
///
/// Prioritization is skipped during quoting. It's only used during competition.

// have enough balance to pay for.
//
// Prioritization is skipped during quoting. It's only used during competition.
pub async fn prioritize(mut self, eth: &Ethereum) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment makes me think that we should have 2 auction types:

  1. that represents the Auction we get for the protocol to solve
  2. the Augmented auction that is the result of:
  • fetching token data
  • fetching liquidity
  • prioritization and filtering

If we do have to separate types, then it becomes clear at a type level which "methods" are applied on an Auction for solving and for quoting:

  • async Auction -> auction::Augmented transformation does order prioritization.
  • async quote::Order -> auction::Augmented transformation does not do order prioritization.

Not a suggestion for this PR, though. Just a general suggestion to clean up (what is IMO) a code smell that is starting to appear as we add some more logic to these domain types.

(order::Trader, eth::TokenAddress),
Result<eth::TokenAmount, crate::infra::blockchain::Error>,
> = join_all(tokens_by_trader.map(|(trader, token)| async move {
let balance = eth.balance_of(trader.into(), token).await;
Copy link
Contributor

Choose a reason for hiding this comment

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

We would need to pull in the BalanceFetching implementation where we simulate with hooks here to not break the current hooks implementation.

Comment on lines +60 to +63

pub fn ethflow_address(&self) -> Option<eth::ContractAddress> {
self.ethflow
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This wouldn't be needed with hooks support right? Ideally, we would keep this out of the driver.

Copy link
Contributor

Choose a reason for hiding this comment

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

Although the ethflow contract holds enough ETH for all the trades it may not hold enough WETH for all the trades. So unless I'm missing something this is needed unfortunately.
I guess one thing to get around this is to use the balance fetching implementation that considers pre-interactions for all orders that need them and not updating the user's regular token balances when including orders because we don't know whether the pre-interaction would actually use some of the user's balance.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I'm not sure I understand. Ethflow orders use pre-interactions that wrap WETH. So if we simulate the WETH balance after executing the order's pre-interaction, we will get sufficient WETH to trade all user's Ethflow orders (since it always wraps all of its ETH).

Copy link
Contributor

Choose a reason for hiding this comment

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

The point I was trying to make was more general. Right now we:

  • collect all tuples (owner, sell_token)
  • fetch these balances
  • go through all orders and decrement the corresponding user balance until nothing is left

If we want to use the balance fetcher we have to simulate the balance of each order individually because grouping them doesn't really make sense because they could have vastly different pre-interactions which get the required sell_token balance.
That's why I suggested to keep doing what are currently doing for orders that don't use any pre-interactions and for all the other orders we could simply check if the user has the expected balance after simulating the pre-interaction.
But this could be problematic when a user has 10 WETH, 1 order selling 10 WETH and 1 order selling 10 WETH with some random pre-interaction.

The point I'm trying to make is that we have way less guarantees about sell token balances with orders that include pre-interactions.

Copy link
Contributor

Choose a reason for hiding this comment

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

BTW I tried going down the road of making pre-interaction aware balance fetching available in the driver but I think I first have to implement the old CodeSimulating trait for the driver infra::Simulator and for that I think it would be beneficial to move ethrpc into its own crate first.

Copy link
Contributor

@nlordell nlordell left a comment

Choose a reason for hiding this comment

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

LGTM.

Two comments that I think should lead to follow ups:

  • I think we need two Auction types - one for the auction we get from the protocol, and one for the augmented auction to send to solvers.
  • We need to use the BalanceFetching implementation that supports simulations of token balances with pre/post interactions. This would allow us to drop ethflow specific conditions and logic (which I would like to keep out of the driver).

@MartinquaXD MartinquaXD force-pushed the driver-filter-orders-from-user-balances branch from 2c7086c to 8ee8b73 Compare August 28, 2023 09:58
@MartinquaXD MartinquaXD enabled auto-merge (squash) August 28, 2023 11:08
@MartinquaXD
Copy link
Contributor

Merging to unblock further roll out for now.
Will follow up with the refactoring required to drop the ethflow edge cases.

@MartinquaXD MartinquaXD merged commit a98a724 into main Aug 28, 2023
@MartinquaXD MartinquaXD deleted the driver-filter-orders-from-user-balances branch August 28, 2023 11:17
@github-actions github-actions bot locked and limited conversation to collaborators Aug 28, 2023
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.

5 participants