Settle fast-path quote solutions via the existing /settle - #4710
Settle fast-path quote solutions via the existing /settle#4710AryanGodara wants to merge 10 commits into
Conversation
|
Reminder: Please consider backward compatibility when modifying the API specification.
Resolved |
ad645bf to
1b8d082
Compare
|
Claude finished @AryanGodara's task in 5m 23s —— View job PR Review — Settle fast-path quote solutions
The design is sound and, importantly, fail-safe: Two minor points posted inline (silent dropping of invalid native prices → missing slippage buffer; and all fast-path failures collapsing into One item not inline-able (file not in diff):
Nothing here blocks merge — the amount-mismatch guard makes the risky part safe. LGTM once the openapi doc is updated. |
MartinquaXD
left a comment
There was a problem hiding this comment.
I'm positively surprised. A few things seem more complicated than they need to and some edge cases are not handled but overall the approach seems relatively non invasive. 👍
| .cloned() | ||
| .ok_or(Error::SolutionNotAvailable)?; | ||
| let solution = cached.solution.rebind_quote_order(order.clone())?; | ||
| let tokens = Arc::new(cached.auction.tokens.with_native_prices(&prices)); |
There was a problem hiding this comment.
The cached solution already has the necessary native prices. I think they should probably not be updated.
There was a problem hiding this comment.
I might've misunderstood this part. I had the confusion that is clearing prices vs native prices. the cached solution has the solver's clearing prices, but the driver's quote path never receives native prices (unlike /solve, the /quote request seems to carry None and there's no estimator in the driver), so cached.auction.native_prices() is empty apart fromfor ETH.
The prices on /settle is the only thing that sizes the slippage buffer by native value.
If I'm not incorrect 👆🏼 then maybe we can also have the orderbok send native prices on the quote request, so we can cache them in as well. You call 👀
There was a problem hiding this comment.
Hmm, you are right. Sending the native prices in the quote request is cleaner but can also incur a latency hit because we'd then have to resolve the native price before we can send the quote request. Also we'd have to update a bunch of interfaces.
Let's keep the native prices in the /settle request for now but I suspect before the final release we should pivot to native prices in the quote request.
| async move { | ||
| observe::settling(); | ||
| if let Some(order) = req.order { | ||
| let app_data = AppData::Hash(AppDataHash::from(order.app_data)); |
There was a problem hiding this comment.
It looks like the appdata is not handled sufficiently. In practice the order will be placed with an appdata hash that the driver did not resolve yet. When reencoding the solution the driver has to use the appdata cache to either look up the full appdata or fetch it from the API just in time.
There was a problem hiding this comment.
I'm thinking of doing this separately (since this'll also contain handling pre/post hooks, flashloans, etc). I'm thinking of doing this on a stacked PR on top of this one to keep things clean and isolated. (Or maybe I'm overthinking this 🤷🏼 )
There was a problem hiding this comment.
update: decided to just do it here 😅
MartinquaXD
left a comment
There was a problem hiding this comment.
Things are more complicated than I thought. Will have to think more about it and do another pass tomorrow.
| return Err(error::Error::FastPathOrderMismatch); | ||
| } | ||
| *user = user.with_order(order)?; | ||
| let (flashloans, wrappers) = { |
There was a problem hiding this comment.
doesn't seem like the extra scope helps here TBH. This also causes you to mix validation logic with the actual work the function does.
| let flashloans = order | ||
| .app_data | ||
| .flashloan() | ||
| .filter(|_| flashloans_enabled) |
There was a problem hiding this comment.
the config flag feeding this boolean controls whether flashloan hints should be sent to the solver. In this case we are already able to resolve the flashloan and the order will not work without it.
| pub fn rebind_quote_order(&self, order: competition::Order) -> Result<Self, error::Error> { | ||
| /// `order`, keeping the cached route and clearing prices, and recover the | ||
| /// order's flashloans/wrappers from its app-data. | ||
| pub fn rebind_quote_order( |
There was a problem hiding this comment.
I still think the name isn't really helpful here.
WDYT about finalize_fast_path_solution()?
| observe::settling(); | ||
| if let Some(order) = req.order { | ||
| let app_data = AppData::Hash(AppDataHash::from(order.app_data)); | ||
| let app_data = state |
There was a problem hiding this comment.
There should not be a lot of code inside the API request handlers. Ideally they are only used to define the REST API and dispatch to the business logic. Let's try to clean this up a bit. Looks like you could turn all this into 1 public function on the competition struct that in turn calls a few private functions.
The whole logic that turns a cached solution into a fully encode solution is a bit hard to follow at the moment.
| Trade::Fulfillment(fulfillment) => Some(fulfillment), | ||
| Trade::Jit(_) => None, | ||
| }) | ||
| .expect("exactly one user trade counted above"); |
There was a problem hiding this comment.
you can avoid this expect and simplify the error handling above by using Itertools:
let Ok(user_trade) = self.user_trades().exactly_one() else {
// error about unexpected number of user trades
};
| pub async fn resolve_app_data( | ||
| &self, | ||
| hash: &order::app_data::AppDataHash, | ||
| ) -> Option<Arc<app_data::ValidatedAppData>> { |
There was a problem hiding this comment.
You are adding a purpose built function here which has an API that's so generic that it's not very useful for the 1 caller it has.
I think an API like this would be much nicer:
/// Resolves the order's appdata hash to the underlying JSON if it isn't already.
pub async fn resolve_app_data(
&self,
order: &mut Order,
)
| } | ||
| } | ||
|
|
||
| pub async fn resolve_app_data( |
There was a problem hiding this comment.
This function does not have to exist if you move the logic that currently happens in the REST API handler into the competition struct.
| .cloned() | ||
| .ok_or(Error::SolutionNotAvailable)?; | ||
| let solution = cached.solution.rebind_quote_order(order.clone())?; | ||
| let tokens = Arc::new(cached.auction.tokens.with_native_prices(&prices)); |
There was a problem hiding this comment.
Hmm, you are right. Sending the native prices in the quote request is cleaner but can also incur a latency hit because we'd then have to resolve the native price before we can send the quote request. Also we'd have to update a bunch of interfaces.
Let's keep the native prices in the /settle request for now but I suspect before the final release we should pivot to native prices in the quote request.
| quoted.sell.token, | ||
| quoted.buy.token, | ||
| quoted.side, | ||
| quoted.target(), |
There was a problem hiding this comment.
Checking for quality in the target amount is probably okay.
However, we also need to check that the amount we need to provide is <= what we originally computed. Also it's not enough to just provide the user order as is in the /settle endpoint we also need to provide that limit price the solver has to fulfill. The reason is that the user will apply some slippage to the order amounts but during the fast path order should get filled at the originally quoted amount - not just the order's limit price.
So by now we have 3 pieces of data that normally don't exist in the settle path:
- order
- limit prices
- native price
Since all of those are only populated in the fast path case they should also be bundled together in the settle POST body.
Description
Completes the driver half of BE-58. A fast-path quote already caches its
Solution(in #4678). This works on the/settlepart. At settle time the driver re-encodes the cached solution against the real signed order and submits via the unchanged mempool path.Changes
orderand its nativepricesto/settle. When the order is present, the driver re-encodes the cached quote solution against it before the (unchanged) settle path submits.(auction_id, solution_id), so the existing/settlelookup + mempool submission consume it unchanged.How to test