Skip to content

feat(universal-router-sdk): support Universal Router v2.3.0 3-parameter UNWRAP_WETH - #639

Draft
claude[bot] wants to merge 2 commits into
mainfrom
claude/unwrap-weth-v2-3-0
Draft

feat(universal-router-sdk): support Universal Router v2.3.0 3-parameter UNWRAP_WETH#639
claude[bot] wants to merge 2 commits into
mainfrom
claude/unwrap-weth-v2-3-0

Conversation

@claude

@claude claude Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Requested by Alice Henshaw · Slack thread

Draft until Uniswap/universal-router#489 merges and the v2.3.0 routers deploy — the final version string and per-chain addresses are TBD (see Follow Ups).

Description

SDK support for the Universal Router UNWRAP_WETH (0x0c) breaking change in Uniswap/universal-router#489, exposed as a new opt-in router version tier so the SDK API stays backward compatible.

Before / After (the on-chain behavior change)

  • Before (routers <= v2.2.0): UNWRAP_WETH encodes (recipient, amountMinimum) and always unwraps the router's entire WETH balance, reverting if that balance is below amountMinimum. There is no way to unwrap only part of the balance.
  • After (routers >= v2.3.0): UNWRAP_WETH encodes (recipient, amount, minAmount) and unwraps exactly amount, leaving the rest as WETH in the router. CONTRACT_BALANCE (2**255, already exported by the SDK) is the only unwrap-the-full-balance sentinel; 0 now means "unwrap nothing", not "unwrap all with no minimum". Legacy-equivalent migration: UNWRAP_WETH(recipient, x)UNWRAP_WETH(recipient, CONTRACT_BALANCE, x).

How

Follows the exact pattern the SDK already uses for the v2.1.1 minHopPriceX36 swap-command extension:

  • Version tierUniversalRouterVersion.V2_3_0 = '2.3.0' + isAtLeastV2_3_0() in universal-router-sdk/src/utils/constants.ts, mirroring isAtLeastV2_1_1.
  • ABI override tablePAYMENTS_COMMANDS_V2_3_0 in routerCommands.ts with the 3-param UNWRAP_WETH ABI ['address','uint256','uint256'], plus a new exported getCommandDefinition(type, urVersion) that layers overrides newest-tier-first over COMMAND_DEFINITION. Used by createCommand for encoding; CommandParser.parseCalldata spreads the same table for decoding. defaultAbiCoder's arity check doubles as an encode-time guard against un-migrated call sites.
  • Internal call sites — all UNWRAP_WETH encoders now pass urVersion through and encode [recipient, CONTRACT_BALANCE, amountMin] on >= 2.3.0 (preserving legacy full-balance semantics), or the old 2-param shape otherwise:
    • entities/actions/uniswap.ts (4 sites: input-unwrap ingress, native-output settlement, exact-output refund, mixed-route WETH→ETH transition) via a shared unwrapWethParams helper
    • utils/encodeSwapStep.ts (the encodeSwaps step encoder)
    • entities/actions/unwrapWETH.tsUnwrapWETH gains an optional urVersion constructor param; since it knows the exact amount it pulled via Permit2, on >= 2.3.0 it encodes a true exact unwrap [ROUTER_AS_RECIPIENT, amount, amount] rather than CONTRACT_BALANCE
  • New capability surface — the UNWRAP_WETH SwapStep gains an optional exact amount?: BigNumberish (defaults to CONTRACT_BALANCE when encoding for >= 2.3.0). validateEncodeSwaps rejects amount on pre-2.3.0 versions (UNWRAP_WETH_AMOUNT_UNSUPPORTED_BEFORE_V2_3_0) instead of silently dropping it.
  • v4-sdk — one-line URVersion.V2_3_0 enum addition so toV4URVersion doesn't throw when encoding V4_SWAP at the new version (the two enums resolve by shared string values). v4-sdk must release first / in the same batch.
  • Changesets — minor for @uniswap/universal-router-sdk and @uniswap/v4-sdk (the new ABI is gated behind explicit urVersion opt-in, so the SDK API is backward compatible). Note for integrators: calldata built with urVersion >= 2.3.0 must only be sent to v2.3.0+ deployments, and 2-param calldata reverts on v2.3.0 routers.
  • Deployment addresses — intentionally no CHAIN_CONFIGS entries for V2_3_0 yet: routerConfigs is an optional-per-version map, so UNIVERSAL_ROUTER_ADDRESS(V2_3_0, chainId) throws "not deployed" (the repo's existing convention for versions absent on a chain, e.g. V1_2 on Linea). Entries slot in with no code change once the routers deploy.

How Has This Been Tested?

Unit tests (hardhat/mocha), all passing locally — 383 tests across test/unit + test/utils:

  • test/utils/routerCommands.test.ts (new): getCommandDefinition tier layering (base / 2.1.1 / 2.3.0) and createCommand arity guards in both directions
  • test/utils/commandParser.test.ts: 3-param decode at 2.3.0 (exact amount and CONTRACT_BALANCE sentinel), legacy 2-param decode at 2.2.0, and 2.1.1 swap overrides still layering at 2.3.0
  • test/unit/encodeSwaps.test.ts: encodeSwapStep 2-param vs 3-param encoding with amount ?? CONTRACT_BALANCE defaulting and exact-amount passthrough; end-to-end SwapRouter.encodeSwaps at 2.3.0; validateEncodeSwaps version gate
  • test/unit/toV4URVersion.test.ts: existing enum-sync contract test now covers V2_3_0 automatically
  • v4-sdk: full bun test suite passing (240 pass / 2 skip)

Not run in this environment: test:forge (no forge binary) and the fork-based test/uniswapTrades.test.ts (requires FORK_URL). Those forge/interop fixtures pin pre-2.3.0 router deployments and are intentionally untouched; 2.3.0 fixture variants need a deployed/forkable v2.3.0 router.

Are there any breaking changes?

No SDK API breaking changes — the 3-param encoding only activates behind an explicit urVersion >= 2.3.0 opt-in; all defaults and existing signatures are unchanged (the UnwrapWETH constructor gains a trailing optional param). The on-chain command ABI is breaking between router versions, which is exactly what the version gate isolates: do not send >= 2.3.0 calldata to older routers or vice versa.

(Optional) Feedback Focus

  • The UnwrapWETH entity intentionally encodes an exact unwrap of its known pulled amount on >= 2.3.0 (safer than CONTRACT_BALANCE there); the UniswapTrade sites keep CONTRACT_BALANCE for exact parity with legacy behavior. Sanity-check that split.
  • Naming/placement of PAYMENTS_COMMANDS_V2_3_0 and getCommandDefinition.

(Optional) Follow Ups

🤖 Generated with Claude Code

https://claude.ai/code/session_014JpEsbYe1BuXBVri6anYBi


Generated by Claude Code

claude added 2 commits July 13, 2026 18:21
…er UNWRAP_WETH

From Universal Router v2.3.0 (Uniswap/universal-router#489) UNWRAP_WETH
encodes (recipient, amount, minAmount) instead of (recipient, amountMinimum):
amount is the exact amount of WETH to unwrap and CONTRACT_BALANCE (2^255) is
the only unwrap-the-full-balance sentinel (0 now unwraps nothing).

- Add UniversalRouterVersion.V2_3_0 and isAtLeastV2_3_0 (no CHAIN_CONFIGS
  entries yet; addresses pending deployment)
- Add PAYMENTS_COMMANDS_V2_3_0 ABI override table and a layered
  getCommandDefinition used by both createCommand and CommandParser
- Migrate internal UNWRAP_WETH call sites (UniswapTrade, addMixedSwap,
  encodeSwapStep, UnwrapWETH entity) to the 3-param encoding when
  urVersion >= 2.3.0, preserving legacy full-balance semantics via
  CONTRACT_BALANCE (UnwrapWETH entity uses its known exact pulled amount)
- Add optional exact `amount` to the UNWRAP_WETH swap step, gated in
  validateEncodeSwaps to urVersion >= 2.3.0
- v4-sdk: add URVersion.V2_3_0 so toV4URVersion resolves the new version
- Tests for both version tiers plus changesets (minor)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JpEsbYe1BuXBVri6anYBi
Resolves conflict in universal-router-sdk validateEncodeSwaps.ts between
main's direct-transfers recipient validation (#638) and this branch's
v2.3.0 UNWRAP_WETH amount support: UNWRAP_WETH now uses the
direct-transfers-aware checkRecipient from main while keeping the
UNWRAP_WETH_AMOUNT_UNSUPPORTED_BEFORE_V2_3_0 invariant; imports keep
both MAX_UINT160 and isAtLeastV2_3_0.
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