Skip to content

feat(universal-router-sdk): add SwapProxy support for no-Permit2 approve+swap flow#518

Draft
Ayoakala wants to merge 2 commits intomainfrom
ayo/proxy-UR
Draft

feat(universal-router-sdk): add SwapProxy support for no-Permit2 approve+swap flow#518
Ayoakala wants to merge 2 commits intomainfrom
ayo/proxy-UR

Conversation

@Ayoakala
Copy link
Contributor

@Ayoakala Ayoakala commented Feb 25, 2026

PR Scope

Please title your PR according to the following types and scopes following conventional commits:

  • fix(SDK name): will trigger a patch version
  • chore(<type>): will not trigger any release and should be used for internal repo changes
  • <type>(public): will trigger a patch version for non-code changes (e.g. README changes)
  • feat(SDK name): will trigger a minor version
  • feat(breaking): will trigger a major version for a breaking change

Description

[Summary of the change, motivation, and context]

How Has This Been Tested?

[e.g. Manually, E2E tests, unit tests, Storybook]

Are there any breaking changes?

[e.g. Type definitions, API definitions]

If there are breaking changes, please ensure you bump the major version Bump the major version (by using the title feat(breaking): ...), post a notice in #eng-sdks, and explicitly notify all Uniswap Labs consumers of the SDK.

(Optional) Feedback Focus

[Specific parts of this PR you'd like feedback on, or that reviewers should pay closer attention to]

(Optional) Follow Ups

[Things that weren't addressed in this PR, ways you plan to build on this work, or other ways this work could be extended]


✨ Claude-Generated Content

Description

Adds SwapProxy support to universal-router-sdk, enabling a no-Permit2 approve+swap flow where the proxy pulls ERC20 tokens directly from the user into the Universal Router before executing swap commands.

Changes

New Options

  • Added useProxy and chainId options to SwapOptions type in sdks/universal-router-sdk/src/entities/actions/uniswap.ts
  • useProxy: boolean - when true, encodes calldata for SwapProxy instead of Universal Router
  • chainId: number - required in proxy mode to resolve the correct UR address

SwapRouter Updates

  • Added PROXY_INTERFACE with the SwapProxy execute(address router, address token, uint256 amount, bytes commands, bytes[] inputs, uint256 deadline) function signature
  • Added encodeProxyPlan() private method to encode calldata targeting the SwapProxy contract
  • Modified swapCallParameters() to detect proxy mode and route to proxy encoding
  • Added validation: proxy mode requires ERC20 input (no native), explicit chainId, explicit recipient, and disallows Permit2 permits

UniswapTrade Updates

  • Updated constructor to set payerIsUser = false in proxy mode (proxy transfers tokens to UR)
  • Added validation requiring explicit recipient address (SENDER_AS_RECIPIENT resolves to proxy)
  • Skipped PERMIT2_TRANSFER_FROM command when using proxy with WETH unwrap (proxy already transferred tokens)

Constants Updates

  • Added swapProxy field to ChainConfig type
  • Added placeholder SwapProxy addresses for 8 chains (mainnet, sepolia, polygon, optimism, arbitrum, base, BNB, worldchain)
  • Exported SWAP_PROXY_ADDRESS(chainId) helper function

Tests

  • Added comprehensive unit tests in sdks/universal-router-sdk/test/unit/swapProxy.test.ts covering:
    • UniswapTrade payerIsUser behavior
    • Recipient validation
    • Proxy calldata encoding for V3 and V4 trades
    • Input validation (native input, missing chainId, permit conflict)
    • Slippage handling
    • UR version selection
    • Non-proxy fallback behavior

How Has This Been Tested?

Unit tests added covering proxy mode encoding, validation, and edge cases.

Are there any breaking changes?

No - the useProxy parameter is optional and defaults to false, maintaining backward compatibility with existing code.

Follow Ups

  • Update placeholder SwapProxy addresses (0x0000...0000) after SWAP-2046 deployment

@Ayoakala Ayoakala requested a review from a team as a code owner February 25, 2026 22:17
@Ayoakala Ayoakala marked this pull request as draft February 25, 2026 22:17
@github-actions
Copy link
Contributor

github-actions bot commented Feb 25, 2026

🤖 Claude Code Review

Review complete

Summary

This PR adds SwapProxy support to the universal-router-sdk, enabling a no-Permit2 approve+swap flow for ERC20 tokens. The implementation adds new options (useProxy, chainId) to SwapOptions, exports a new SWAP_PROXY_ADDRESS constant, and encodes calldata targeting a SwapProxy contract.

Review Findings

Critical Issues

1. Potential type mismatch: URVersion vs UniversalRouterVersion

In encodeProxyPlan (swapRouter.ts:376):

const urVersion = (options.urVersion as string as UniversalRouterVersion) ?? UniversalRouterVersion.V2_0

The options.urVersion is typed as URVersion (from @uniswap/v4-sdk), but it's being cast to UniversalRouterVersion. These appear to be different enums. If the string values don't align exactly, this could silently pass the wrong version to UNIVERSAL_ROUTER_ADDRESS() and resolve to an incorrect router address. This could cause transactions to fail or target the wrong contract.

2. Missing chainId validation for proxy mode

The SWAP_PROXY_ADDRESS function is exported but never called when encoding proxy transactions. If a caller passes a chainId for a chain that doesn't have swapProxy configured, the transaction will encode successfully but target an invalid address (since UNIVERSAL_ROUTER_ADDRESS is called but SWAP_PROXY_ADDRESS is not verified).

3. Placeholder addresses (0x0...0) for SwapProxy

All swapProxy entries in constants.ts are set to 0x0000000000000000000000000000000000000000 with TODO comments. While the PR title indicates deployment is pending (SWAP-2046), using the zero address could lead to transactions that:

  • Send funds to the burn address if someone mistakenly uses useProxy: true before real addresses are deployed
  • The SWAP_PROXY_ADDRESS function will return 0x0...0 without throwing, since it only checks if the property is undefined/falsy, not if it's the zero address

Minor Issues

4. Hardcoded 30-minute default deadline

In encodeProxyPlan (swapRouter.ts:380-382):

const deadline = options.deadlineOrPreviousBlockhash
  ? BigNumber.from(options.deadlineOrPreviousBlockhash)
  : BigNumber.from(Math.floor(Date.now() / 1000) + 1800) // 30 min default

Using Date.now() in library code is generally discouraged as it makes the output non-deterministic. The regular encodePlan doesn't have this default - it passes undefined if no deadline is provided. Consider requiring a deadline when useProxy is true, or documenting this behavior clearly.

5. Inconsistent recipient requirement enforcement

The UniswapTrade constructor validates that recipient is provided and not SENDER_AS_RECIPIENT when useProxy is true. However, swapCallParameters doesn't duplicate this check before calling encodeProxyPlan. If someone constructs a UniswapTrade manually and calls encoding methods directly, they could bypass this validation. The constructor validation is appropriate, but consider adding an invariant in swapCallParameters for defense-in-depth.

Positive Notes

  • Clear error messages that explain why SwapProxy requires explicit recipients
  • Proper validation that native currency input isn't supported with proxy mode
  • The payerIsUser = false logic is correctly set for proxy mode since the proxy transfers tokens

Recommendations

  1. Add a check for zero address in SWAP_PROXY_ADDRESS function to prevent silent failures
  2. Consider adding tests for the new proxy flow
  3. Verify that URVersion and UniversalRouterVersion enum values align, or add explicit mapping

💡 Want a fresh review? Add a comment containing @request-claude-review to trigger a new review at any time.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

📋 Review verdict: REQUEST_CHANGES

👆 The main review comment above is the source of truth for this PR review. It is automatically updated on each review cycle, so always refer to it for the most current feedback.

This formal review submission is for the verdict only. 3 inline comment(s) are attached below.

private static encodeProxyPlan(planner: RoutePlanner, trade: UniswapTrade, options: SwapOptions): MethodParameters {
const { commands, inputs } = planner

const urVersion = (options.urVersion as string as UniversalRouterVersion) ?? UniversalRouterVersion.V2_0
Copy link
Contributor

Choose a reason for hiding this comment

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

Casting URVersion to UniversalRouterVersion via string could fail silently if the enum values don't match. Consider explicit mapping or type assertion with validation.


export const SWAP_PROXY_ADDRESS = (chainId: number): string => {
if (!(chainId in CHAIN_CONFIGS)) throw new Error(`SwapProxy not deployed on chain ${chainId}`)
const proxy = CHAIN_CONFIGS[chainId].swapProxy
Copy link
Contributor

Choose a reason for hiding this comment

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

This will return 0x0000000000000000000000000000000000000000 for configured chains, which could cause funds loss. Consider adding a check:

if (proxy === '0x0000000000000000000000000000000000000000') throw new Error(`SwapProxy not yet deployed on chain ${chainId}`)
Suggested change
const proxy = CHAIN_CONFIGS[chainId].swapProxy
export const SWAP_PROXY_ADDRESS = (chainId: number): string => {
if (!(chainId in CHAIN_CONFIGS)) throw new Error(`SwapProxy not deployed on chain ${chainId}`)
const proxy = CHAIN_CONFIGS[chainId].swapProxy
if (!proxy) throw new Error(`SwapProxy not configured for chain ${chainId}`)
if (proxy === '0x0000000000000000000000000000000000000000') throw new Error(`SwapProxy not yet deployed on chain ${chainId}`)
return proxy
}

const inputAmount = BigNumber.from(trade.trade.maximumAmountIn(options.slippageTolerance).quotient.toString())
const deadline = options.deadlineOrPreviousBlockhash
? BigNumber.from(options.deadlineOrPreviousBlockhash)
: BigNumber.from(Math.floor(Date.now() / 1000) + 1800) // 30 min default
Copy link
Contributor

Choose a reason for hiding this comment

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

Using Date.now() makes output non-deterministic. The regular flow doesn't have a default deadline. Consider requiring deadline when useProxy: true to maintain consistency.

@github-actions github-actions bot changed the title feat(swap-router): add support for SwapProxy feat(universal-router-sdk): add SwapProxy support for no-Permit2 approve+swap flow Feb 27, 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.

1 participant