-
Notifications
You must be signed in to change notification settings - Fork 90
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
Conversation
44bf49b
to
8371acb
Compare
There was a problem hiding this 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.
I'm taking over this PR. |
42a36b9
to
e1cce76
Compare
cac3053
to
ccb886e
Compare
2fdc6a5
to
c7c4179
Compare
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. |
c7c4179
to
a4636e2
Compare
pub async fn solve(&self, auction: Auction) -> Result<Solved, Error> { | ||
let auction = auction.prioritize(&self.eth).await; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
// 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
This still needs to be merged for co-location. |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 driver
s). 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> { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
let sell = remaining | ||
.remaining(order.sell.amount.0) | ||
.context("remaining_sell")?; | ||
let fee = remaining | ||
.remaining(order.fee.user.0) | ||
.context("remaining_fee")?; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
pub async fn solve(&self, auction: Auction) -> Result<Solved, Error> { | ||
let auction = auction.prioritize(&self.eth).await; |
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be mandatory?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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 { |
There was a problem hiding this comment.
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:
- that represents the
Auction
we get for the protocol to solve - 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; |
There was a problem hiding this comment.
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.
|
||
pub fn ethflow_address(&self) -> Option<eth::ContractAddress> { | ||
self.ethflow | ||
} |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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 dropethflow
specific conditions and logic (which I would like to keep out of the driver).
2c7086c
to
8ee8b73
Compare
Merging to unblock further roll out for now. |
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:
Test Plan
Added automated test.