Skip to content

fix: reject hard quotes whose outputs span multiple tokens - #480

Open
codyborn wants to merge 1 commit into
mainfrom
fix/reject-mixed-output-tokens
Open

fix: reject hard quotes whose outputs span multiple tokens#480
codyborn wants to merge 1 commit into
mainfrom
fix/reject-mixed-output-tokens

Conversation

@codyborn

@codyborn codyborn commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Companion to uniswapx-service#693. That PR stops the order service accepting an order whose outputs span multiple tokens. This one stops us asking fillers to quote it in the first place.

The problem

HardQuoteRequest.toCleanJSON collapses the signed order into a single (tokenIn, tokenOut, amount) request, where tokenOut is outputs[0].token and amount comes from totalOutputAmountStart, which raw-sums every output's startAmount. That projection is only meaningful when the outputs agree on a token. When they don't, the scalar we ask quoters to beat mixes assets of different denominations, and the quoter is pricing something the order does not contain. Two orders with completely different output vectors produce a byte-identical quote request.

The cost of that lands on the quoter, which is the reason this is worth fixing here and not only in the order service. A quoter that wins on a price it could not have computed gets exclusivity on an order it then has to fade, spending fill rate and circuit-breaker standing on a request it was never able to price.

The change

HardQuoteRequest.hasUniformOutputTokens checks every output against outputs[0].token, checksummed so casing doesn't matter. The handler rejects on it immediately after parsing the inner order, which is:

  • before the KMS getAddress round trip,
  • before the order is cosigned,
  • before any quoter is contacted.

Rejection is a 400 VALIDATION_ERROR via a new MixedOutputTokensError, and emits QUOTE_MIXED_OUTPUT_TOKENS. I expect that metric to sit at zero; it is there so we find out if it doesn't, before anyone notices as a fade.

Multi-output orders are unaffected. Fee outputs use the swapper output's token, which is the assumption the raw sum was already relying on (#271 raised it at review time and it was never enforced). Applies to both Dutch V2 and V3, since both go through this handler.

Testing

yarn test:unit green, 269 tests. New coverage on HardQuoteRequest: uniform single output, uniform fee output, casing insensitivity, and a mixed vector. New coverage on the handler: 400 with the right detail, quote never called on the quoter, signDigest never called, and a same-token fee output still returning 200.

🤖 Generated with Claude Code

We collapse a hard quote into a single (tokenIn, tokenOut, amount) request, where
tokenOut is outputs[0].token and amount raw-sums every output's startAmount. That
projection is only meaningful when the outputs agree on a token. When they don't,
the scalar we ask quoters to beat mixes assets of different denominations, and a
quoter is bidding on something the order does not contain.

The cost lands on the quoter. Winning that auction hands it exclusivity on an order
it has to fade, which spends its fill rate and circuit-breaker standing on a request
it was never able to price. uniswapx-service rejects the same shape at POST /order
(#693), so the fill could not have happened anyway.

Rejected in the handler immediately after parsing the inner order, before the KMS
round trip, before the order is cosigned and before any quoter is contacted, with a
QUOTE_MIXED_OUTPUT_TOKENS metric so we can see whether this ever fires. Multi-output
orders are unaffected: fee outputs use the swapper output's token.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codyborn
codyborn requested a review from alanhwu August 7, 2026 21:49
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.

1 participant