Skip to content

feat(swapproxy): deterministic CREATE2 deployment#151

Open
david-uniswap wants to merge 12 commits into
mainfrom
david/swapproxy-create2
Open

feat(swapproxy): deterministic CREATE2 deployment#151
david-uniswap wants to merge 12 commits into
mainfrom
david/swapproxy-create2

Conversation

@david-uniswap

@david-uniswap david-uniswap commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Migrates SwapProxy from the fragile nonce-0 CREATE scheme to a deterministic CREATE2 deployment, so any funded key can deploy it to the same address on every chain (no privileged 0xb259… EOA at nonce 0 required).

Deterministic address (deployed on all 13 chains): 0x0000000085E102724e78eCd2F45DC9cA239Affad

  • factory: 0x4e59b44847b379578588920cA78FbF26c0B4956C (Arachnid; present on all 13 chains incl. Arc, MegaETH, Robinhood)
  • salt: 0xd00319ff7795e528cc1ccd28bd6e08e46a42130d69ab4992d656773ba5fb323c (vanity-mined for a 4-leading-zero-byte address, matching Permit2 / CaliburEntry)
  • initcode hash: 0x342d83f4d7400dd603e2a6829db9cb032e7f62a2dda94746f2acd4e7e881bb2f

Status

Deployed on all 13 chains; each holds byte-identical 1005-byte runtime (keccak 0x24a2…7aaf). Source-verified on each block explorer (pending your review of the spot-checks):

  • Etherscan-family (auto similar-match off the canonical bytecode): ethereum, optimism, base, arbitrum, celo, unichain, unichain-sepolia, sepolia
  • Blockscout: soneium, zora, megaeth, robinhood, arc

Per-chain tx hashes + explorers are recorded in .swapproxy-deploy/create2.json and the registry.

Source-of-truth model (pin the contract, not the UR repo)

The CREATE2 address depends only on {factory, salt, initcode}. SwapProxy's compile closure is 5 files and it references the router only through the IUniversalRouter interface, embedding zero router bytecode — so Universal Router implementation upgrades never move this address.

We deploy the frozen canonical initcode (.swapproxy-deploy/canonical-initcode.hex, the mainnet-verified 1005-byte build) rather than recompiling from the universal-router submodule. This also sidesteps the repo's universal-router profile (optimizer_runs=1, bytecode_hash=none) which produces a divergent 964-byte build. Full rationale in .swapproxy-deploy/README.md.

What's in this PR

  • script/DeploySwapProxyCreate2.s.sol — idempotent, self-checking CREATE2 deploy script.
  • .swapproxy-deploy/create2.json — pinned factory/salt/initcodeHash/address/build/closure + all 13 deployments (chain → txHash → explorer → verified).
  • .swapproxy-deploy/README.md — source-of-truth model + deploy/verify instructions.
  • .swapproxy-deploy/canonical-initcode.hex — frozen 1031-byte initcode.
  • foundry.toml — fs read permission for .swapproxy-deploy/.
  • deployments/json/{chainId}.json (×13) — standard SwapProxy registry entry ({address, proxy, deploymentTxn, timestamp, commitHash}), matching the schema used by Permit2 / CaliburEntry. .md files left for CI regeneration.

Notes / open items

  • New address. The legacy 0x02e5…b2a9 (nonce-0 CREATE) cannot be reproduced via CREATE2. SwapProxy is immutable + ownerless, so legacy instances stay on-chain and consumers migrate.
  • The registry entries supersede the legacy 0x02e5 tracked in PR feat(deployments): track SwapProxy across 13 chains (incl. Arc 5042 + Robinhood 4663) #150/robinhood chain deployments #137 (main has no SwapProxy entries yet, so this is additive; whichever merges second reconciles old→new).
  • Chain quirks captured in create2.json notes: MegaETH needed --gas-limit ~13M (estimates ~10.8M for the create), Arc needed --legacy, Robinhood deployed via cast mktx → sequencer.

🤖 Generated with Claude Code

david-uniswap and others added 3 commits June 3, 2026 11:03
Adds the source-of-truth + tooling to deploy SwapProxy to one deterministic
address on every chain via the canonical Arachnid CREATE2 factory, so any
funded key can deploy it (no privileged nonce-0 EOA required).

- script/DeploySwapProxyCreate2.s.sol: idempotent, self-checking deploy script
  that ships the FROZEN canonical initcode (not a local recompile) via
  CREATE2_FACTORY with salt 0. Reverts on initcode-hash or address mismatch.
- .swapproxy-deploy/create2.json: pinned {factory, salt, initcodeHash, address,
  build settings, compile closure}. Deployments map filled in a follow-up commit.
- .swapproxy-deploy/README.md: pin-the-contract rationale (SwapProxy embeds zero
  UR router bytecode, so UR upgrades never move the address) + deploy/verify.
- .swapproxy-deploy/canonical-initcode.hex: frozen 1031-byte initcode (matches
  the mainnet-verified 1005-byte runtime).
- foundry.toml: grant fs read for .swapproxy-deploy/ so the script reads initcode.

Deterministic address: 0x18F2e4BE6c4E266a8150605867D81a484E77708a
(verified via cast create2 and manual keccak; empty on-chain; Base dry-run OK).

This is a NEW address; the legacy 0x02e5...b2a9 (nonce-0 CREATE) cannot be
reproduced via CREATE2. SwapProxy is immutable/ownerless, so legacy instances
remain and consumers migrate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mines a salt that lands SwapProxy on a 4-leading-zero-byte address, matching
the Uniswap singleton convention (Permit2 0x000000000022..., CaliburEntry
0x000000009b1d...). The Arachnid factory uses the salt raw (no msg.sender
mixing), so the address is identical on every chain from any deployer key.

- salt:    0xd00319ff7795e528cc1ccd28bd6e08e46a42130d69ab4992d656773ba5fb323c
- address: 0x0000000085E102724e78eCd2F45DC9cA239Affad

Verified via cast create2 + manual keccak; empty on mainnet and Base.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Populates the deployments map in create2.json with all 13 chains. Each is
live at 0x0000000085E102724e78eCd2F45DC9cA239Affad with byte-identical
1005-byte runtime, and source-verified on its block explorer.

Etherscan-family (auto similar-match against the verified canonical bytecode):
ethereum, optimism, base, arbitrum, celo, unichain, unichain-sepolia, sepolia.
Blockscout (standard-json / verified-twin): soneium, zora, megaeth, robinhood, arc.

Chain quirks captured in per-entry notes: MegaETH needed --gas-limit ~13M
(estimates ~10.8M for the create), Arc needed --legacy, Robinhood deployed via
cast mktx -> sequencer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@david-uniswap david-uniswap changed the title [WIP] feat(swapproxy): deterministic CREATE2 deployment feat(swapproxy): deterministic CREATE2 deployment Jun 3, 2026
david-uniswap and others added 2 commits June 3, 2026 12:36
Matches the repo's .prettierrc (JSON override tabWidth 4). Fixes the
'Format non solidity files with prettier' pre-commit hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the standard SwapProxy entry to each deployments/json/{chainId}.json
`latest` block, matching the existing schema used by other CREATE2 / vanity
contracts (Permit2, CaliburEntry): {address, proxy, deploymentTxn, timestamp,
commitHash}. CREATE2 salt/initcode/build stay in .swapproxy-deploy/create2.json
(the registry never stores those).

Address 0x0000000085E102724e78eCd2F45DC9cA239Affad on all 13 chains. Timestamps
are the real per-chain deploy block times. Supersedes the legacy 0x02e5 entry
tracked in PR #150/#137.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@david-uniswap david-uniswap marked this pull request as ready for review June 3, 2026 19:47
@david-uniswap david-uniswap requested review from SocksNFlops, ericneil-sanc and gretzke and removed request for SocksNFlops June 3, 2026 19:47
david-uniswap and others added 2 commits June 3, 2026 14:04
Extends the CREATE2 SwapProxy to the remaining chains where the legacy 0x02e5
instance exists, reaching parity (22 chains total). All deployed from 0xb259…aC87
at 0x0000000085E102724e78eCd2F45DC9cA239Affad, byte-identical 1005-byte runtime.

New chains: BNB (56), Polygon (137), Monad (143), X Layer (196), Worldchain (480),
Monad Testnet (10143), Avalanche (43114), Linea (59144), Blast (81457).

Verification: 8/9 source-verified on their explorers (Etherscan-family similar-match
for bnb/polygon/worldchain/avalanche/linea/blast/monad-testnet; Monad mainnet submitted
via Etherscan v2 API). X Layer (196) pending — its only explorer is OKLink, which needs
an OKX/OKLink API key.

Adds the 9 to .swapproxy-deploy/create2.json and the standard SwapProxy entry to each
deployments/json/{chainId}.json. 4217 still excluded (unidentified).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploys the Uniswap periphery missing on Ink and records addresses (existing v2/v3 Protofire + v4 Jan-2025 cores adopted as-is, deploy:false):
- PermissionsAdapterFactory 0xd8eeBA7c373d612F24660e45Fa96f5d95B0C9d16
- View Quoter 0x89e5db8b5aa49aa85ac63f691524311aeb649eba
- MixedRouteQuoterV2 0x1f7d7550b1b028f7571e69a784071f0205fd2efa
- UniversalRouter#v2.2 0x28bd21bb4ea4fda370d8d7544992038375d8d456 (Across spokePool 0xeF684C38F94F48775959ECf2012D7E864ffb9dd4)
- Calibur/CaliburEntry 0x000000009b1d0af20d8c6d0a44e162d11f9b8f00
- FeeOnTransferDetector 0x282a3c4d320cc7f0d5eaf56b8029e4b88338f0a3
- FeeCollector 0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7
- ERC7914Detector 0xc470458fc6a7e43471b31e6a2eb2612215a7102e
- SwapProxy 0x0000000085E102724e78eCd2F45DC9cA239Affad (CREATE2, PR #151 flow)

FeeCollector deployed under deployer EOA then transferOwnership'd to the canonical AWS KMS fee owner 0xbE84D31B2eE049DCb1d8E7c798511632b44d1b55 (matches all other fee-collecting chains). All 9 source-verified on explorer.inkonchain.com.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(ink): deploy missing periphery + SwapProxy to Ink (57073)
@linear-code

linear-code Bot commented Jun 9, 2026

Copy link
Copy Markdown

DXDOCS-125

david-uniswap added a commit that referenced this pull request Jun 11, 2026
Standalone off main: the full Ink Uniswap deployment record — v4 core + the periphery we deployed (PermissionsAdapterFactory, view/mixed quoters, UR v2.2, Calibur, FOT/Fee/7914 detectors, SwapProxy) + the adopted Protofire v2/v3 + canonical Permit2 = 25 contracts in deployments/json/57073.json, the regenerated deployments/57073.md, and the Deploy-all broadcast + task records. Cross-checked against PROTO-1474; addresses verified on-chain. SwapProxy's cross-chain create2.json record stays in the SwapProxy PR (#151).

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


Keeps this PR SwapProxy-only. The Ink v4/periphery registry (deployments/json/57073.json), the regenerated 57073.md, the Deploy-all broadcast, and the deploy task were recorded here via the #152 merge; they now live in the standalone Ink registry PR #153 (off main). The SwapProxy cross-chain record for Ink stays in .swapproxy-deploy/create2.json (this PR's domain).

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

@gretzke gretzke left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Deployments look good, however the deployment data is missing in the history section of the deployment json files.
Additionally, in order for us to be able to deploy the SwapProxy contract seamlessly in the future for new chain deployments, the contract should be deployed through the Deploy-all.s.sol script (using the deploy cli) instead of a separate deployment script. I would recommend deleting the .swapproxy-deploy directory and DeploySwapProxyCreate2.sol script and instead integrate the deployment into the script as per CONTRIBUTING.md

david-uniswap and others added 3 commits June 15, 2026 10:11
…istory

SwapProxy was recorded in latest but missing from the history section of
each chain's deployments JSON. Add a newest-first history entry per chain
(22 chains) with address, per-chain deploymentTxn, initcodeHash, and an
empty constructor input, matching the schema used by other contracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…riefcase deployer

Integrate the canonical SwapProxy deployment into Deploy-all.s.sol via the
deploy CLI instead of a standalone script (per CONTRIBUTING.md):

- add SwapProxyDeployer briefcase library that CREATE2-deploys the FROZEN
  canonical initcode via the Arachnid factory and asserts the result equals
  0x0000000085E102724e78eCd2F45DC9cA239Affad (initcode hash 0x342d...bb2f).
  Initcode is intentionally frozen (not regenerated from the UR submodule,
  whose profile yields different bytecode and a different address).
- wire deploySwapProxy() into Deploy-all.s.sol, gated on the contract flag
- register SwapProxy under the universal-router protocol in task_template.json
- add a guard test locking the initcode hash and canonical address
- remove the standalone .swapproxy-deploy/ dir and DeploySwapProxyCreate2.s.sol
  (provenance folded into the deployer NatSpec) and revert the foundry.toml
  fs_permissions entry it required

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the repo's deterministic-deploy pattern (Permit2) as closely as the
canonical address allows:

- make SwapProxy its own single-contract protocol (swap-proxy) in the task
  template, gated and deployed independently like Permit2 (the universal-router
  protocol deploys UR unconditionally, so SwapProxy cannot live under it)
- read the salt from the task file (params.salt) instead of hardcoding it
- do the deterministic-factory call + computeAddress in Deploy-all.s.sol's
  deploySwapProxy(), mirroring deployPermit2(); deployer is now minimal
- guard test locks the frozen initcode hash and canonical address

The initcode stays frozen (cannot match Permit2 here): SwapProxy's canonical
build embeds an IPFS metadata hash (bytecode_hash=ipfs) from the standalone
universal-router repo. This repo builds with bytecode_hash=none, so a recompile
yields identical runtime but a different metadata hash and address (verified:
0x5927... vs canonical 0x342d...). Permit2 reproduces only because its canonical
used bytecode_hash=none. Documented in SwapProxyDeployer.

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

Copy link
Copy Markdown
Collaborator Author

Heads-up: three places this PR doesn't look like the rest of the repo, and why.

1. SwapProxy's bytecode is "frozen" and must not be regenerated.
Normally every contract in src/briefcase/deployers/… has its bytecode auto-generated from source by ./script/util/create_briefcase.sh. SwapProxy's is hardcoded, and the deployer explicitly says "do not regenerate."
Why: SwapProxy has to live at the exact same address on every chain, and that address is locked to its exact bytecode. The canonical build (done in the standalone universal-router repo) bakes in an IPFS metadata fingerprint. This monorepo compiles with that fingerprint turned off (bytecode_hash = none), so recompiling here produces the same runtime logic but a different fingerprint, which changes the bytecode and therefore the address. I verified it: a local recompile gives initcode hash 0x5927… instead of the canonical 0x342d…. So we ship the frozen bytes plus a guard test that fails if they ever change. (Permit2 doesn't hit this only because its canonical build left the fingerprint off, so it regenerates cleanly.)

2. SwapProxy is its own "protocol" in the task file, even though it's just one helper contract.
It sits as a top-level swap-proxy protocol next to permit2, v2, etc., rather than under universal-router, where its source actually lives.
Why: turning on the universal-router protocol always redeploys the Universal Router itself. We want to be able to drop SwapProxy onto a chain (including chains that already have the router) without redeploying anything else. Permit2 is modeled the same way (its own one-contract protocol), so this matches the closest precedent.

3. The deployment-history JSON entries are hand-written, not generated.
Usually forge-chronicles writes the deployments/json/<chainId>.json entries. Here they're edited by hand.
Why: SwapProxy is deployed through the deterministic CREATE2 factory, and chronicles can't attribute a factory deploy to a contract name, so it skips it (same as the Universal Router CREATE2 deploys). We backfilled the 22 existing chains by hand to match the schema; future chains will need the same manual entry.

Everything else follows the Permit2 pattern: salt in the task file, the factory deploy done in Deploy-all.s.sol's deploySwapProxy() (mirroring deployPermit2()), and a minimal deployer.

@david-uniswap david-uniswap requested a review from gretzke June 15, 2026 17:55
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