Skip to content

chore: merge main into dev-2.3.0, keeping the 2.3.0 work intact - #519

Merged
dianakocsis merged 4 commits into
dev-2.3.0from
chore/merge-main-into-dev-2.3.0
Sep 22, 2026
Merged

dianakocsis merged 4 commits into
dev-2.3.0from
chore/merge-main-into-dev-2.3.0

Conversation

@dianakocsis

@dianakocsis dianakocsis commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR for the reviewer

This merge changes no contract code. The diff is 34 files: 24 deploy-address records and 10 deploy scripts, all from #516 and #517, each byte-identical to origin/main. If you see anything under contracts/, test/ or snapshots/ in this diff, something is wrong — there should be nothing.

That is the entire review. The rest of this description explains why a merge this boring needed a hand-written resolution.

The problem

main's aebfa39 (#514) reverted #491 (nested V4_SWAP) and #497 (bounded command input decoding). dev-2.3.0 forked at fb25ff0, before that revert, so it still contains both.

That matters because #502 depends on #491. #502 is +10 lines / −0 in Dispatcher.sol — a guard that sits inside the if (poolManager.isUnlocked()) block #491 created:

if (poolManager.isUnlocked()) {              // <- #491
    if (!NestedUnlock.isPermitted())         // <- #502
        revert NestedExecutionNotPermitted();
    ...
}

Remove #491 and there is no branch for #502 to guard. They are one unit: #491 added the feature and carried audit finding M-01; #502 fixes M-01 by replacing chain-state inference with an explicit opt-in. main took the other remedy for M-01 — delete the feature outright — which is correct for main, since main tracks the deployed 2.1.2 router. 2.3.0 takes the keep-and-gate path.

Why a plain merge is unsafe

A default merge re-applies the revert. Nine files conflict:

contracts/base/Dispatcher.sol
lib/v4-periphery
snapshots/UniversalRouterTest.json
test/foundry-tests/V4SwapWithinUnlock.t.sol          (modify/delete)
test/integration-tests/gas-tests/__snapshots__/*.snap  (5 files)

Those are the visible half. The dangerous half is silent: git cleanly auto-merges contracts/libraries/Commands.sol and contracts/modules/Payments.sol, deleting command 0x0f (UNWRAP_WETH_EXACT / unwrapWETH9Exact) with no conflict marker. Resolving only the nine conflicts and committing would ship that loss invisibly.

Worse, resolving the nine "sensibly" — taking main's side, since main is the release branch — yields #491: 0, #502: 0, #497: 0. All three gone, including the audited work.

Resolution applied

keep dev-2.3.0:  contracts/  test/  snapshots/  README.md  lib/v4-periphery
take main:       deploy-addresses/  script/

Deliberately not git revert aebfa39 — that would restore #491 and #497 but not #502, which was authored on top of them.

Verification

check result
contracts/, test/, snapshots/, README.md, submodule pin vs dev-2.3.0 (8b8595a) byte-identical
incoming files 34, all deploy-addresses/ (24) or script/ (10)
each incoming file vs origin/main (b67f125) byte-identical
#491 nested branch / #502 guard / #497 0x0f / #497 checkInputLength present (1 / 1 / 1 / 17 sites)
RouterParameters.sol (the struct the new scripts construct) identical on main and dev-2.3.0, so the scripts compile against these contracts

Merge parents: 8b8595a (dev-2.3.0) and b67f125 (main, including #517).

Follow-ups

lib/v4-periphery stays pinned at 0cc6e164. The bump to the restored-#584 periphery stack is a separate commit once Uniswap/v4-periphery#604 lands.

Once this merges, #512's diff stops understating the change. GitHub was diffing #512 against merge-base fb25ff0, which predates the revert, so it rendered as 16 files / +687 −71. What would actually land on main is 59 files / +1854 −239. Anyone auditing #512 before now was reviewing roughly a third of it.

This also makes dev-2.3.0 a true superset of main, so future main→dev merges are conflict-free.

Moving dev-2.3.0 makes #507 stale; the restack (#507 → #508 → #509, with gas snapshots regenerated at each level) follows.

🤖 Generated with Claude Code

AI-Generated Description

TL;DR for the reviewer

This merge changes no contract code. The diff is 34 files: 24 deploy-address records and 10 deploy scripts, all from #516 and #517, each byte-identical to origin/main. If you see anything under contracts/, test/ or snapshots/ in this diff, something is wrong — there should be nothing.
That is the entire review. The rest of this description explains why a merge this boring needed a hand-written resolution.

The problem

main's aebfa39 (#514) reverted #491 (nested V4_SWAP) and #497 (bounded command input decoding). dev-2.3.0 forked at fb25ff0, before that revert, so it still contains both.
That matters because #502 depends on #491. #502 is +10 lines / −0 in Dispatcher.sol — a guard that sits inside the if (poolManager.isUnlocked()) block #491 created:

if (poolManager.isUnlocked()) {              // <- #491
    if (!NestedUnlock.isPermitted())         // <- #502
        revert NestedExecutionNotPermitted();
    ...
}

Remove #491 and there is no branch for #502 to guard. They are one unit: #491 added the feature and carried audit finding M-01; #502 fixes M-01 by replacing chain-state inference with an explicit opt-in. main took the other remedy for M-01 — delete the feature outright — which is correct for main, since main tracks the deployed 2.1.2 router. 2.3.0 takes the keep-and-gate path.

Why a plain merge is unsafe

A default merge re-applies the revert. Nine files conflict:

contracts/base/Dispatcher.sol
lib/v4-periphery
snapshots/UniversalRouterTest.json
test/foundry-tests/V4SwapWithinUnlock.t.sol          (modify/delete)
test/integration-tests/gas-tests/__snapshots__/*.snap  (5 files)

Those are the visible half. The dangerous half is silent: git cleanly auto-merges contracts/libraries/Commands.sol and contracts/modules/Payments.sol, deleting command 0x0f (UNWRAP_WETH_EXACT / unwrapWETH9Exact) with no conflict marker. Resolving only the nine conflicts and committing would ship that loss invisibly.
Worse, resolving the nine "sensibly" — taking main's side, since main is the release branch — yields #491: 0, #502: 0, #497: 0. All three gone, including the audited work.

Resolution applied

keep dev-2.3.0:  contracts/  test/  snapshots/  README.md  lib/v4-periphery
take main:       deploy-addresses/  script/

Deliberately not git revert aebfa39 — that would restore #491 and #497 but not #502, which was authored on top of them.

Changes

  • deploy-addresses/ (24 files): Record UniversalRouterV2_1_2 addresses across all networks, add new chain files (arc.json, megaeth.json, robinhood.json)
  • script/deployParameters/ (10 files): Add deploy scripts for Arc, HyperEVM, MegaETH, Robinhood; update BaseSepolia, Ink, Unichain, UnichainSepolia, Worldchain, Zora with live addresses

Verification

check result
contracts/, test/, snapshots/, README.md, submodule pin vs dev-2.3.0 (8b8595a) byte-identical
incoming files 34, all deploy-addresses/ (24) or script/ (10)
each incoming file vs origin/main (b67f125) byte-identical
#491 nested branch / #502 guard / #497 0x0f / #497 checkInputLength present (1 / 1 / 1 / 17 sites)
RouterParameters.sol (the struct the new scripts construct) identical on main and dev-2.3.0, so the scripts compile against these contracts
Merge parents: 8b8595a (dev-2.3.0) and b67f125 (main, including #517).

Follow-ups

lib/v4-periphery stays pinned at 0cc6e164. The bump to the restored-#584 periphery stack is a separate commit once Uniswap/v4-periphery#604 lands.
Once this merges, #512's diff stops understating the change. GitHub was diffing #512 against merge-base fb25ff0, which predates the revert, so it rendered as 16 files / +687 −71. What would actually land on main is 59 files / +1854 −239. Anyone auditing #512 before now was reviewing roughly a third of it.
This also makes dev-2.3.0 a true superset of main, so future main→dev merges are conflict-free.
Moving dev-2.3.0 makes #507 stale; the restack (#507 → #508 → #509, with gas snapshots regenerated at each level) follows.

dianakocsis and others added 4 commits September 16, 2026 19:20
…514)

* revert: bound router command input decoding (#497)

Reverts d203e7f.

This change was deployed to mainnet at 0x0542093271A31f6FC1DADB232bd59eeb27de780F
but was never intended to ship. Removes the checkInputLength calldata bounds
checks, the BytesLib hardening, the SWEEP uint160 truncation fix, the
UNWRAP_WETH_EXACT command (0x0f), and unwrapWETH9Exact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* revert: allow V4_SWAP within an existing PoolManager unlock (#491)

Reverts 9e9a780.

This change was deployed to mainnet at 0x0542093271A31f6FC1DADB232bd59eeb27de780F
but was never intended to ship. It is the root cause of audit finding M-01:
V4 deltas accrue under the router regardless of which contract called execute(),
so within one foreign unlock, one call can leave debt that a later call settles
from a different msgSender().

Removing the fast path restores the pre-#491 behaviour: a nested V4_SWAP reverts
with AlreadyUnlocked. This supersedes #502, #512 and #513, which existed only to
gate the fast path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: regenerate UniversalRouter bytecode size snapshot

23705 -> 23303 after reverting #491 and #497.

The hardhat gas snapshots under test/integration-tests/gas-tests/__snapshots__/
are deliberately left untouched: they require a mainnet fork (FORK_URL, block
20010000) to regenerate and cannot be derived by hand. They must be regenerated
with 'UPDATE_SNAPSHOT=1 yarn test:hardhat --grep gas' before this merges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: regenerate hardhat gas snapshots after reverting #491 and #497

65 snapshots updated across 5 suites. One snapshot removed:
'UNWRAP_WETH_EXACT partial amount', whose command (0x0f) no longer exists
after the #497 revert.

Regenerated with UPDATE_SNAPSHOT=1 against a mainnet fork at block 20010000.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* build: bump v4-periphery to main with the #584 revert

Moves lib/v4-periphery 07336f2 -> a7af5b3, which is v4-periphery main after
#601 merged (revert of #584, tolerate hook-funded input on exact-output swaps).

#564's exact-output partial-fill revert is retained; the only src/ change
against the previous pin is V4Router.sol (+4/-23).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: regenerate bytecode snapshot after v4-periphery bump

23303 -> 23233. Hardhat gas snapshots unchanged: the #584 revert only removes
hook-funded exact-output paths, which the gas suite does not exercise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: record 2.1.2 deployment addresses

Record the UniversalRouter 2.1.2 addresses deployed across all 24 chains.

Every address was verified onchain before recording: 24,380-byte runtime,
all ten constructor parameters matching the deploy script, immutables
embedded in the deployed bytecode, and source verified (Etherscan V2 where
available, Sourcify otherwise; Tempo on its own Sourcify instance).

Four chains additionally record an UnsupportedProtocolV2_1_2 entry. Their
deploy scripts left `unsupported` unset, so the script deployed a throwaway
revert stub first, which consumed a nonce and shifted the router address.
Recording it explains why those routers do not share an address with the
rest of their nonce group.

arc.json, megaeth.json and robinhood.json are new; these chains had never
been recorded. Their pre-existing routers were read from the SDK and
confirmed onchain before being included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(script): point base-sepolia and unichain-sepolia at live v4 deployments

Both scripts referenced PoolManager and PositionManager addresses that have
no recent onchain activity. A router deployed against them cannot execute
any v4 command.

Confirmed by log activity rather than by documentation, which has been
unreliable here. On Base Sepolia the previous PoolManager
(0xf7F5aB3D) has produced no logs in the last 50k blocks, while
0x05E73354 is actively emitting. The same holds on Unichain Sepolia for
0x9cB26A71 versus 0x00B036B5.

Sepolia needed no change; it was corrected on main in 020e1b7.

These addresses are what UniversalRouter 2.1.2 was actually deployed with
on both chains, so main's scripts now describe what is live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(script): add arc, megaeth and robinhood params; fill in unsupported

Two gaps surfaced while deploying 2.1.2.

Ink, Unichain, Worldchain and Zora left `unsupported` unset, so each deploy
created a fresh UnsupportedProtocol stub before the router. That consumed a
nonce and shifted the router off the address its nonce group would
otherwise share, and left an orphaned stub that nothing references. Filling
in the stubs deployed during 2.1.2 stops the next deploy repeating it. Each
address was confirmed onchain to be a 60-byte contract that reverts with
UnsupportedProtocolError.

Arc, MegaETH and Robinhood had no deploy parameters on main at all; they
were added on release/2.1.x and never ported. Without them a deploy from
main silently skips those chains. Ported with permissionsAdapterFactory
added to match the RouterParameters shape on main.

All 24 chains' parameters now match the constructor arguments of the
routers actually deployed onchain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style: fix forge fmt violation in PermissionedV4 test

Pre-existing on main and unrelated to this branch, but it fails
`yarn lint:check` and so would show a red CI run on any PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Revert "style: fix forge fmt violation in PermissionedV4 test"

This reverts commit 8ec8ca8.

* chore: drop the mislabelled 2.1.1 entry for megaeth

0x47837eb80db5908eabba9105626d9b348bea7b02 was recorded as
UniversalRouterV2_1_1, taken from universal-router-sdk's CHAIN_CONFIGS. It
cannot be 2.1.1: it was deployed 2026-01-30, and tag 2.1.1 was not cut until
2026-05-22.

It is on the 2.1 line — SPOKE_POOL() resolves, which the 2.0 router on that
chain does not — but at 21,738 bytes it is well short of 2.1.1's 24,546,
consistent with a build predating the per-hop slippage work. The 2.1.1 release
notes warn about exactly this: "Do not identify existing 2.1.0 bytecode as
2.1.1." There is no 2.1.0 tag to bytecode-match against, so rather than invent
a label the entry is dropped.

UniversalRouterV2 (0x48fd0352...) is kept: verified on Etherscan as
UniversalRouter, no SPOKE_POOL(), and 19,499 bytes matching the 2.0 routers on
ink, tempo, unichain and zora.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Used for the 2.2.0 deployment at 0x9aFe3C497e19501DB228F28CdBdD29bC98F65DBa on 2026-09-18
(tx 0xd9eda6c912e49c0ce7e15bfd7a6d5dc91de796cc08231aec80df191341244f34), built at tag 2.2.0 (64027f3).
Across SpokePool and PermissionsAdapterFactory wired; reuses the UnsupportedProtocol at 0xEEE3…4BcF.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Brings main's deploy-address records and deploy scripts (#516, #517) onto
the 2.3.0 line without letting main's #514 revert delete the work this
branch is built on.

## Why this needs a deliberate resolution

main's `aebfa39` (#514) reverted #491 (nested V4_SWAP) and #497 (bounded
command input decoding). This branch forked at `fb25ff0`, before that, so it
still contains both -- and #502's nested-unlock opt-in gate is 10 lines that
sit *inside* the `if (poolManager.isUnlocked())` block #491 added. #502
cannot exist without #491.

A plain merge re-applies the revert. Nine files conflict, which is the
visible half. The dangerous half is silent: git cleanly auto-merges
`Commands.sol` and `Payments.sol`, deleting command `0x0f`
(`UNWRAP_WETH_EXACT` / `unwrapWETH9Exact`) with no conflict marker at all.

Resolution:

- `contracts/`, `test/`, `snapshots/`, `README.md`, `lib/v4-periphery`
  -> keep this branch's version
- `deploy-addresses/`, `script/` -> take main's

Note this is deliberately not `git revert aebfa39`. That would restore #491
and #497 but not #502, which was authored against them.

## Verified

- `contracts/`, `test/`, `snapshots/`, `README.md` and the submodule pin are
  **byte-identical to dev-2.3.0 (8b8595a)** -- this merge changes no code
- the 34 incoming files are all under `deploy-addresses/` (24) or `script/`
  (10), and each is **byte-identical to origin/main (b67f125)**
- #491's nested branch, #502's opt-in guard, #497's `checkInputLength` (17
  call sites) and #497's `0x0f` command are all still present
- `RouterParameters.sol`, the struct the new deploy scripts construct, is
  identical on main and this branch, so the scripts compile against these
  contracts

`lib/v4-periphery` stays pinned at `0cc6e164`. The bump to the restored-#584
periphery stack is a separate commit, once Uniswap/v4-periphery#604 lands.

After this, #512's diff stops understating the change: GitHub was diffing
against merge-base `fb25ff0`, which predates the revert, so it showed 16
files / +687 -71 when what would actually land on main is 59 files /
+1854 -239.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dianakocsis
dianakocsis requested a review from a team as a code owner September 22, 2026 21:04
@dianakocsis
dianakocsis merged commit db95b46 into dev-2.3.0 Sep 22, 2026
7 checks passed
@dianakocsis
dianakocsis deleted the chore/merge-main-into-dev-2.3.0 branch September 22, 2026 23:56
dianakocsis added a commit that referenced this pull request Sep 28, 2026
#519 brought main in as a squash merge, so git still saw main's commits
as new, including revert #514. A plain merge would re-apply that revert
and silently delete the #491/#497 work (Payments, BytesLib,
V3ToV4Migrator and their tests) in files where nothing conflicts.

Resolve to the dev-2.3.0 tree and take only what main has that dev-2.3.0
lacks, #520 and #521:

- deploy-addresses/hyperevm.json and its CLAUDE.md entry
- permissionsAdapterFactory for Base, Ink and HyperEVM

lib/v4-periphery stays at dev-2.3.0's 0cc6e16; the bump to dev-ur-2.3.0
happens in #508. With main now an ancestor, later merges of main will
not re-apply #514. Bytecode is unchanged at 23,657.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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