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

Price competition bang for buck #2145

Merged
merged 18 commits into from
Dec 11, 2023
Merged

Conversation

MartinquaXD
Copy link
Contributor

Description

We started to allow external solvers to provide quotes and incentivized them by giving them rewards for every trade where they reported the best quote.
At the moment we rank qoutes by the highest reported out_amount without taking gas costs of complex routes into account. Because solvers are now competing on the highest out_amount they started to propose more and more complex routes which would end up hurting the user because complex routes require a lot of gas to execute which would end up being deducted from the user in form of fees.
In summary the current situation leads to the UI showing very high fees which leads to fewer trades being placed.

Changes

Instead of most out_amount we now rank by bang-for-buck (i.e. out_amount - gas_fees). To do that our price estimators now also need to be aware of the native price for the out_token. That way we can rank estimates by the total eth_received.

In order to keep the interface unchanged I decided to make the regular price estimator use a native price estimator under the hood.
Because the CompetitionPriceEstimator was recently refactored to be usable for regular and native price estimates the native price estimator now also has the field native. This is obviously not great but at least you don't have to pass an actual native price estimator and it also ends up not being used.

How to test

Not sure how doable it is to set up an e2e test for this but I should at least add a unit test using mocks that verifies that we now pick best "bang-for-buck".

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.

I think this approach works as well, unfortunately we also need to pass information about the current gas price into the component as well in order to make things work.

If possible, I'd like to keep the concurrent logic of fetching quotes, native estimates and the gas estimate in parallel (which is why I though returning all responses and then picking the best given all three values may be easier to implement).

OrderKind::Buy => query.sell_token,
OrderKind::Sell => query.buy_token,
};
let native_price = self.native.estimate_native_price(out_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.

Can we move this await into the compare_results lambda and delay it as much as possible? This should increase our chances of not making estimates slower (the actual quote can run in parallel with the native estimate).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can do that. There are some notes, though:

  1. Currently we are passing in a NativePriceCache so the latency is only a concern for the first quote or whenever something got evicted from the cache so not that impactful
  2. This means the normal native price estimator will also call this code and we actually have to pass a Arc<dyn NativePriceEstimating> in the constructor whereas we can currently pass any type (I used ()) because we don't use it at all.

But I guess since you are in favor of making the NoopEstimator more explicit anyway you don't see 2 as a negative.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I gave it a go and the inner future was written so generically that we can't get the token out of the Query. 😓
So either introduce yet another trait just so we can get that from the query or I need to find another way to not block on the native price fetching.
Will investigate further.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tried to contain the damage (i.e. additional complexity) the best I could.
Instead of calling the native price estimator and gas estimator inside the generic function (which didn't work nicely) I opted for another argument in the generic function that takes a future that provides the needed context.
Now the supplied comparison function takes 2 results and this context.
The context is returned from a fused future (so we can await it multiple times) that awaits the result of a task that was spawned in the background so it does not delay the actual price estimation requests.

pub fn new(
inner: Vec<PriceEstimationStage<T>>,
successful_results_for_early_return: NonZeroUsize,
native: N,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can be option to not use a Noop estimator and make the 1:1 exchange rate for the native token estimates explicit?

pub fn out_amount_in_eth(&self, native_out_token_price: f64) -> U256 {
let eth_value = self.out_amount.to_f64_lossy() * native_out_token_price;
let eth_value = U256::from_f64_lossy(eth_value);
eth_value.saturating_sub(self.gas.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'm afraid this is the gas amount not the actual gas cost in ETH 🙈 (the new types in the driver/solver engine are really helpful to avoid this confusion).

let fee_parameters = FeeParameters {
gas_amount: trade_estimate.gas as _,
gas_price: gas_estimate.effective_gas_price(),
sell_token_price,
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Will add a gas price estimator to get the actual gas used. 👍

let competition_estimator = RacingCompetitionEstimator::new(
estimators,
results_required,
PriceRanking::MaxOutAmount,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This argument is still kind of fake. We could pass BestBangForBuck here and it would change nothing because the native price wrapper around the generic estimation function does not use this variable.
But at least it's less implicit what is happening here.
Not sure if we can get rid of this wart without untangling the generic price estimator implementation again.

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.

Looks alright to me, but I agree this is very confusing (and I doubt we will kill this component altogether when we move to verified quotes, so we might be accruing some real debt here).

Would it make things simpler if compare_results and provide_additional_context would be combined in a single Comparator trait which allows to pick the winner async from an Iterator<Result<R,E>>?

Then NativePriceEstimating can just use a simple implementation for the comparator and RacingCompetitionEstimator can implement the async context aware logic?

>(
&self,
query: Q,
kind: OrderKind,
get_single_result: impl Fn(&T, Q) -> futures::future::BoxFuture<'_, Result<R, E>>
+ Send
+ 'static,
compare_results: impl Fn(&Result<R, E>, &Result<R, E>) -> Ordering + Send + 'static,
compare_results: impl Fn(&Result<R, E>, &Result<R, E>, &C) -> Ordering + Send + 'static,
provide_additional_context: impl FusedFuture<Output = Result<C, E>> + Send + 'static,
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this have to be so generic? Don't we always return a concrete RankingContext ?

Copy link
Contributor Author

@MartinquaXD MartinquaXD Dec 8, 2023

Choose a reason for hiding this comment

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

We don't have any context in the generic price estimation case because it doesn't appear to make sense to give the user the option between MaxOutAmount and BestBangForBuck because for BestBangForBuck we need to have a native price which is what we are trying to compute in that call in the first case.
So unless I'm missing something it's not really possible to decide in that case whether it makes sense to use a less complex trade with lower out_amount because we don't really know yet how the token relates to native ETH.

OTOH we could force C to always be a RankingContext (by providing the noop context) but at this point I'm not sure how much that would really improves things. :/

@MartinquaXD MartinquaXD marked this pull request as ready for review December 10, 2023 17:17
@MartinquaXD MartinquaXD requested a review from a team as a code owner December 10, 2023 17:17
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.

Looks pretty good to me!

crates/shared/src/price_estimation/competition.rs Outdated Show resolved Hide resolved
crates/shared/src/price_estimation/competition.rs Outdated Show resolved Hide resolved
crates/shared/src/price_estimation/competition.rs Outdated Show resolved Hide resolved
@MartinquaXD MartinquaXD enabled auto-merge (squash) December 11, 2023 18:08
@MartinquaXD MartinquaXD merged commit 113ec60 into main Dec 11, 2023
8 checks passed
@MartinquaXD MartinquaXD deleted the price-competition-bang-for-buck branch December 11, 2023 18:12
@github-actions github-actions bot locked and limited conversation to collaborators Dec 11, 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.

3 participants