Skip to content

fix: OZ 2.3.0 audit fixes on the router path (L-02, N-01, N-05, N-08) - #608

Merged
dianakocsis merged 8 commits into
dev-ur-2.3.0from
fix/oz-ur-2.3.0-audit-findings
Oct 1, 2026
Merged

dianakocsis merged 8 commits into
dev-ur-2.3.0from
fix/oz-ur-2.3.0-audit-findings

Conversation

@ccashwell

@ccashwell ccashwell commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

What

Fixes for the v4-periphery side of OpenZeppelin's Universal Router v2.3.0 audit (report dated 2026-09-21), one commit per finding, stacked on dev-ur-2.3.0.

Finding Change Commit
L-02 _swapExactInput and _swapExactOutput revert with EmptyPath on a zero-length path instead of completing without a swap e975a37
N-01 The exact-output comment no longer claims a hook pool can over-deliver; v4-core folds the specified-side hook delta into the swap amount and afterSwap only touches the unspecified side 5a802db
N-05 The V4Quoter fixes from #588, cherry-picked with -x: SafeCast on the delta conversions and the reverse-traversal break when a hook funds the whole input 61de38b, c82e53d
N-08 CalldataDecoder.toBytes checks that the head word for element _arg lies inside the slice before reading it, in whole words so a huge _arg cannot wrap 8dd956a
Gas snapshots regenerated 02ffbe9

Notes

  • N-08 changes PositionManager bytecode as well, since its five decoders rely on toBytes.
  • fix(lens): SafeCast the swap delta conversions in V4Quoter #588 targets main; with its commits carried here it can be closed once this merges, or retargeted if you would rather keep its review.
  • Universal Router PR (link) pins this branch and carries the router-side findings.

gretzke and others added 6 commits September 30, 2026 12:31
V4Quoter read the unspecified side of a swap's BalanceDelta with a bare
truncating uint128(...). Only the specified side is guaranteed to have the
expected sign. The unspecified side is whatever remains after Hooks.afterSwap
applies the hook's returned delta, and a hook holding AFTER_SWAP_FLAG plus
AFTER_SWAP_RETURNS_DELTA_FLAG can drive it past zero.

The only existing guard, BaseV4Quoter._swap's NotEnoughLiquidity check, compares
the specified side against the requested amount, so it cannot see this. The cast
wrapped and the quoter returned roughly 2^128 as a successful quote with no
revert, for a pool that in reality pays out nothing. A consumer that ranks
candidate pools by raw quoter output picks that pool over every honest one.

V4Router already handles the same two quantities correctly, so the quoter now
uses those exact forms. The output sides at _quoteExactInput and
_quoteExactInputSingle take the int128 overload of toUint128, matching
_swapOutput. The input sides at _quoteExactOutput and _quoteExactOutputSingle
widen before negating, uint256(-int256(x)).toUint128(), matching _swapInput.

The widening matters on its own. Negating an int128 first would panic on
type(int128).min before SafeCast ever ran, and that input is a legitimate
magnitude, not a corrupted one. The widened form yields 2^127.

Keeping the router's forms also preserves the hook-funded input case added in
bbd4346: an input delta of exactly zero still quotes as an amountIn of 0
rather than reverting, so a route the router can execute stays quotable.

Two related sites are fixed alongside. _quoteExactInput and
_quoteExactInputSingle built the swap amount as -int256(int128(amountIn)) from a
uint128. Above type(int128).max that reinterprets as negative, and the outer
negation flips the sign, so an exact-input quote silently simulated an
exact-output swap. Both now widen through uint256 as the router does.

A hook that corrupts the delta makes its own pool unquotable rather than
returning a clamped zero. Clamping would be wrong: a negative output delta means
the caller owes the output currency, not that the pool pays nothing, and the
router rejects that state too. Reverting keeps the quote and the swap in
agreement about which pools are usable.

Behavior on rejection is the existing shape. SafeCastOverflow bubbles out of the
simulation and parseQuoteAmount wraps it in UnexpectedRevertBytes, the same
outer error NotEnoughLiquidity already produces. No interface change.

One divergence from the router is left in place. In multi-hop exact output, a
fully hook-funded hop makes the router break out of its loop, while the quoter
propagates the zero into the next swap and hits SwapAmountCannotBeZero. That is
a revert rather than a wrong number. A test records the current behavior so a
future change to it is visible.

Reverting src/lens/V4Quoter.sol fails 8 of the 10 new tests. Four fail with "did
not revert as expected", which is the wrapped value being returned as a valid
quote, and the type(int128).min case fails with an arithmetic overflow panic.

Snapshots regenerated with Foundry v1.3.6 to match the version CI pins.

(cherry picked from commit 8d163f1)
…e input

V4Router._swapExactOutput breaks out of its reverse loop once a hop's input
delta reaches zero, since a hook that fully funds the input leaves the
upstream pools with nothing to produce. V4Quoter._quoteExactOutput never
gained the mirror guard, so it propagated the zero into the preceding pool
and called swap with an amountSpecified of zero, which PoolManager rejects.
The QuoteSwap payload was then replaced by SwapAmountCannotBeZero on the way
out, surfacing as UnexpectedRevertBytes instead of a zero-input quote.

Routes the router executes for free were therefore unquotable, so
quote-dependent integrations excluded valid subsidy paths.

test_quoteExactOutput_fullyFundedHop_revertsSwapAmountCannotBeZero asserted
the broken behavior, so it passed against the bug. It is replaced by
test_quoteExactOutput_fullyFundedHop_quotesZeroInput, which pins the quote to
zero and matches the router's
test_exactOutput_multiHop_hookFundsFinalHop_skipsUpstreamHops.

Costs 29 gas per exact-output hop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7a1a256)
An exact-input or exact-output swap with a zero-length path ran its loop
zero times and completed without swapping anything, so a route could
succeed while delivering no output. Both multi-hop helpers now revert with
EmptyPath before touching the register or the delta. OZ 2.3.0 audit L-02.
The comment claimed a hook pool could deliver more output than requested.
It cannot: v4-core folds a hook's specified-side delta into the amount it
swaps and afterSwap can only adjust the unspecified side, so the caller's
output is at most the request. OZ 2.3.0 audit N-01.
toBytes read the offset word for element `_arg` without checking that the
word lies inside the slice, so a params blob shorter than its declared
layout had its hookData offset read from whatever followed it in calldata.
Check the head word in whole words before reading it, which also covers a
`_arg` large enough to wrap the byte offset. The five PositionManager
decoders that rely on toBytes are the callers affected. OZ 2.3.0 audit N-08.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T17:12:06.910087Z 02ffbe9 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

cursoragent and others added 2 commits October 1, 2026 15:58
Co-authored-by: Eric Zhong <eric.zhong@uniswap.org>
Co-authored-by: Eric Zhong <eric.zhong@uniswap.org>
@dianakocsis
dianakocsis merged commit 7ba1767 into dev-ur-2.3.0 Oct 1, 2026
5 checks passed
@dianakocsis
dianakocsis deleted the fix/oz-ur-2.3.0-audit-findings branch October 1, 2026 20: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.

5 participants