Skip to content

Settle fast-path quote solutions via the existing /settle - #4710

Open
AryanGodara wants to merge 10 commits into
mainfrom
aryan/be-58-settle-fastpath-quote
Open

Settle fast-path quote solutions via the existing /settle#4710
AryanGodara wants to merge 10 commits into
mainfrom
aryan/be-58-settle-fastpath-quote

Conversation

@AryanGodara

Copy link
Copy Markdown
Member

Description

Completes the driver half of BE-58. A fast-path quote already caches its Solution (in #4678). This works on the /settle part. At settle time the driver re-encodes the cached solution against the real signed order and submits via the unchanged mempool path.

Changes

  • Added an optional real signed order and its native prices to /settle. When the order is present, the driver re-encodes the cached quote solution against it before the (unchanged) settle path submits.
  • Re-encoding reuses the quote's work instead of re-solving: it swaps the quote's placeholder order for the real signed order, keeps the cached route + clearing prices, and adds the native prices onto the auction's tokens so the settlement gets a slippage buffer.
  • The resulting settlement is cached by (auction_id, solution_id), so the existing /settle lookup + mempool submission consume it unchanged.

How to test

  • unit and e2e tests

@AryanGodara AryanGodara self-assigned this Aug 6, 2026
@linear-code

linear-code Bot commented Aug 6, 2026

Copy link
Copy Markdown

BE-58

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Reminder: Please consider backward compatibility when modifying the API specification.
If breaking changes are unavoidable, ensure:

  • You explicitly pointed out breaking changes.
  • You communicate the changes to affected teams (at least Frontend team and SAFE team).
  • You provide proper versioning and migration mechanisms.

Resolved

@AryanGodara
AryanGodara changed the base branch from main to aryan/be-58-cache-quote-solutions August 6, 2026 14:34
@AryanGodara
AryanGodara force-pushed the aryan/be-58-settle-fastpath-quote branch from ad645bf to 1b8d082 Compare August 6, 2026 14:41
@AryanGodara
AryanGodara marked this pull request as ready for review August 6, 2026 14:59
@AryanGodara
AryanGodara requested a review from a team as a code owner August 6, 2026 14:59
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AryanGodara's task in 5m 23s —— View job


PR Review — Settle fast-path quote solutions

  • Read pr-review skill
  • Gather diff & context
  • Review changed files
  • Post inline comments
  • Post summary

The design is sound and, importantly, fail-safe: rebind_quote_order only swaps the order while keeping the cached route/prices, and Fulfillment::new re-validates executed_with_fee == order.target(), so a real order whose amounts/side/tokens/kind don't match the quote is rejected rather than mis-settled. The settle/reveal lookup keys on (auction_id, solution_id), which the re-encoded settlement inherits from the cached quote auction, so the existing mempool path consumes it unchanged. The solve_request.rs change is a clean extract-to-into_domain refactor with no behavior change. Nice test coverage for the happy path, missing-order, and mismatch cases.

Two minor points posted inline (silent dropping of invalid native prices → missing slippage buffer; and all fast-path failures collapsing into Kind::Unknown, hurting prod diagnosability). Both are non-blocking.

One item not inline-able (file not in diff):

  • crates/driver/openapi.ymlSettleRequest (around line 593) was not updated with the new optional order and prices fields. Given the API-change reminder on this PR, worth adding them (documented as optional / fast-path-only) so the spec stays in sync.

Nothing here blocks merge — the amount-mismatch guard makes the risky part safe. LGTM once the openapi doc is updated.

Comment thread crates/driver/src/infra/api/routes/settle/mod.rs Outdated
Comment thread crates/driver/src/infra/api/error.rs

@MartinquaXD MartinquaXD left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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. 👍

Comment thread crates/driver/src/domain/competition/solution/mod.rs Outdated
.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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The cached solution already has the necessary native prices. I think they should probably not be updated.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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 👀

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Comment thread crates/driver/src/domain/competition/mod.rs Outdated
Comment thread crates/driver/src/infra/api/routes/solve/dto/solve_request.rs Outdated
Comment thread crates/driver/src/tests/cases/quote.rs Outdated
Comment thread crates/driver/src/infra/api/routes/settle/dto/settle_request.rs
async move {
observe::settling();
if let Some(order) = req.order {
let app_data = AppData::Hash(AppDataHash::from(order.app_data));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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 🤷🏼 )

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

update: decided to just do it here 😅

@AryanGodara
AryanGodara requested a review from MartinquaXD August 6, 2026 19:56

@MartinquaXD MartinquaXD left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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) = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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
        };

Comment on lines +84 to +87
pub async fn resolve_app_data(
&self,
hash: &order::app_data::AppDataHash,
) -> Option<Arc<app_data::ValidatedAppData>> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Base automatically changed from aryan/be-58-cache-quote-solutions to main August 7, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants