Skip to content

Conversation

@theghostmac
Copy link

@theghostmac theghostmac commented Jan 25, 2026

Description

This PR introduces a new /api/v2/quote endpoint that simplifies order creation for frontend applications by providing a complete, ready-to-sign response.

The existing /quote endpoint returns raw price and fee data, requiring frontends to manually calculate slippage protection and apply fees. This has been a common source of confusion and errors.

The new v2 endpoint solves this by returning:

  • a fully formed OrderCreation object ready to sign (with feeAmount set to 0 per the solver-competition model.
  • three-tier amount breakdown for UI display: beforeAllFees, afterNetworkCosts, and afterSlippage
  • detailed cost breakdown: network fees, partner fees, protocol fees
  • smart slippage recommendation based on fee volatility and market movement (heavily unit tested)
  • user-provided slippage applied server-side to protect against MEV

Changes

  • Added OrderQuoteRequestV2, OrderQuoteV2, QuoteBreakdown, CostBreakdown types in crates/model/src/quote.rs.
  • Implemented calculate_quote_v2 in QuoteHandler (crates/orderbook/src/quoter.rs) to handle slippage math and zero out fees.
  • Added unit tests (all 23 are passing and cover all functions)
  • Added POST /api/v2/quote endpoint in crates/orderbook/src/api/post_quote.rs.
  • Registered the new v2/quote route in the orderbook API router (crates/orderbook/src/api.rs).

How to test

  1. Unit tests: cargo test -p orderbook api::post_quote cargo test -p orderbook quoter::tests
  2. Manual Verification: Using curl to access the local server at /api/v2/quote with a payload containing slippageBps.

Architectural notes:

  • refactored and reused v1 infrastructure calculate_quote_internal() now feeds calculate_quote() and calculate_quote_v2()
  • i was calculating protocol fees from scratch, then discovered the unadjusted_quote(), so i avoided double-counting by reusing that instead.
  • network fee conversions are approximated to use quote exchange rate. in future, it should require buy_token_price in price feed for the exact conversion
  • slippage clamping: min 10bps (0.1%), max 10000 bps (100%) to prevent unreasonable values.

Future work

  • Presign signing method (returns tx to call setPReSignature
  • ETH flow support (returns tx to wrap native ETH)
  • Composable CoW Flow support
  • I have a question: its best to implement these as separate endpoints right? (/api/v2/quote/presign, /api/v2/quote/ethflow)

Related Issues

Fixes # #4058

@theghostmac theghostmac requested a review from a team as a code owner January 25, 2026 07:04
@github-actions
Copy link

github-actions bot commented Jan 25, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@theghostmac
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

github-actions bot added a commit that referenced this pull request Jan 25, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds a new /v2/quote endpoint to simplify order creation. The implementation reuses the existing /v1/quote logic and then applies slippage. My review found a critical vulnerability in the slippage calculation that could lead to creating incorrectly priced orders. The details are in the specific comment.

Comment on lines 228 to 249
match order.kind {
OrderKind::Sell => {
// buyAmount = buyAmount * (10000 - slippageBps) / 10000
order.buy_amount = U256::uint_try_from(
order
.buy_amount
.widening_mul(U256::from(MAX_BPS - slippage_factor))
/ U512::from(MAX_BPS),
)
.unwrap_or(U256::MAX);
}
OrderKind::Buy => {
// sellAmount = sellAmount * (10000 + slippageBps) / 10000
order.sell_amount = U256::uint_try_from(
order
.sell_amount
.widening_mul(U256::from(MAX_BPS + slippage_factor))
/ U512::from(MAX_BPS),
)
.unwrap_or(U256::MAX);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The calculation MAX_BPS - slippage_factor can underflow if slippage_bps is greater than 10000. This would cause an integer underflow in release builds, resulting in an incorrect and potentially very large buy_amount for sell orders. This is a critical issue.

Using saturating_sub prevents this underflow by clamping the result at zero, which is the correct behavior for slippage >= 100%.

        match order.kind {
            OrderKind::Sell => {
                // buyAmount = buyAmount * (10000 - slippageBps) / 10000
                order.buy_amount = U256::uint_try_from(
                    order
                        .buy_amount
                        .widening_mul(U256::from(MAX_BPS.saturating_sub(slippage_factor)))
                        / U512::from(MAX_BPS),
                )
                .unwrap_or(U256::MAX);
            }
            OrderKind::Buy => {
                // sellAmount = sellAmount * (10000 + slippageBps) / 10000
                order.sell_amount = U256::uint_try_from(
                    order
                        .sell_amount
                        .widening_mul(U256::from(MAX_BPS + slippage_factor))
                        / U512::from(MAX_BPS),
                )
                .unwrap_or(U256::MAX);
            }
        }
References
  1. This comment focuses on a logic error (integer underflow) which is a critical issue. (link)

Copy link
Author

Choose a reason for hiding this comment

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

great catch!

i should probably also add validation to reject unreasonable slippage:

if request.slippage_bps > 1000 { // 10% max slippage
    return Err(OrderQuoteError::InvalidSlippage);
}

@jmg-duarte
Copy link
Contributor

NOTE: I noticed compilation errors in crates/shared/src/order_quoting.rs related to widening_mul type inference.

Can you elaborate on this?

@anxolin
Copy link

anxolin commented Jan 26, 2026

Hello, I would like to share this document which outlines the potential features of the v2.
I would think, such an important change requires us to have some UML/documentation discussions.

I want to be part of verifying the new interface proposal, and also front-end as creator of the SDK.

Here is a write up about the feature https://www.notion.so/cownation/Quote-API-v2-2f18da5f04ca8032b09de7e2f409e46b?source=copy_link

@theghostmac
Copy link
Author

theghostmac commented Jan 26, 2026

NOTE: I noticed compilation errors in crates/shared/src/order_quoting.rs related to widening_mul type inference.

Can you elaborate on this?

@jmg-duarte

here:

cargo build
   Compiling shared v0.1.0 (/.../services/crates/shared)
error[E0658]: const arguments cannot yet be inferred with `_`
   --> crates/shared/src/order_quoting.rs:150:33
    |
150 |                 .widening_mul::<_, _, 512, 8>(sell_amount)
    |                                 ^
    |
    = note: see issue #85077 <https://github.com/rust-lang/rust/issues/85077> for more information

error[E0658]: const arguments cannot yet be inferred with `_`
   --> crates/shared/src/order_quoting.rs:150:36
    |
150 |                 .widening_mul::<_, _, 512, 8>(sell_amount)
    |                                    ^
    |
    = note: see issue #85077 <https://github.com/rust-lang/rust/issues/85077> for more information

For more information about this error, try `rustc --explain E0658`.
error: could not compile `shared` (lib) due to 2 previous errors

It does compile with just:

.widening_mul(sell_amount)

And tests still pass.

Making the fix here would be outside the scope of this PR, hence why I ignored.

@jmg-duarte
Copy link
Contributor

@theghostmac it compiles just fine in main, what Rust version are you using?

Regardless, that case should always be ok because Rust knows the size of all parties involved, LHS would be the self.data.quoted_buy_amount so U256; RHS is sell_amount so U256; and the RES would be U512, inferred from the following division

@theghostmac
Copy link
Author

true.
i just updated rustc from 1.88 to 1.93.
all good here.

i would go ahead and give @anxolin's document a read.

although i do understand that some of it is internal.

@anxolin
Copy link

anxolin commented Jan 27, 2026

Hei @theghostmac, sorry there was probably a misunderstanding on my part. By reading the PR title "add ergonomic /quote v2 endpoint" I thought you were going to work on the evolution of the API we envisioned for easy integrations.

The document I shared is not complete, but would give you an overview of the problem. Our SDK solves most of it, so its a great source for inspiration. The screenshots at the end try to show the 3 main prices UIs show and therefore, its handy to return in the endpoint DTO.

I understand now you were not intended to solve exactly this problem and your PR had a very different scope.
If by any chance, you would like to work on the full v2 specification and implementation, let us now. Otherwise, you can ignore my previous comment.

@theghostmac
Copy link
Author

Hi @anxolin,

Yes, I would love to work on the full spec. I paused because I thought it was internal.

I can continue now.

@github-actions
Copy link

This pull request has been marked as stale because it has been inactive a while. Please update this pull request or it will be automatically closed.

@github-actions github-actions bot added the stale label Feb 10, 2026
@theghostmac
Copy link
Author

hi @MartinquaXD, @anxolin, anybody... is it possible to get feedback about this before it gets closed by GA bot?

@github-actions github-actions bot removed the stale label Feb 11, 2026
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.

3 participants