Skip to content

feat: deploy Uniswap to HyperEVM (999) - #164

Merged
david-uniswap merged 2 commits into
mainfrom
david/hyperevm-deploy
Sep 22, 2026
Merged

david-uniswap merged 2 commits into
mainfrom
david/hyperevm-deploy

Conversation

@david-uniswap

@david-uniswap david-uniswap commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

Deploys the full Uniswap stack to HyperEVM (chain 999): v2, v3, v4, quoters, SwapRouter02, UniversalRouter, SwapProxy, FeeOnTransferDetector, FeeCollector. 27 contracts, all verified on hyperevmscan and Sourcify.

Router and FeeCollector: the recorded UniversalRouter is the re-cut 2.2.0 release (tag 2.2.0 @ 64027f3, release, includes the #499 permissioned-sweep fix and v4-periphery #564; #584 was reverted upstream and is not included) at 0x9aFe3C497e19501DB228F28CdBdD29bC98F65DBa, deployed from the universal-router repo with the Across SpokePool and PermissionsAdapterFactory wired. FeeCollector was redeployed pointing at it: 0xf8a6dee153dfe5c43f66564252b6df7885fa6165. The first-cut 2.2.0 router (0x05498c32f8F4825BCD4e5c6325134431B12d492A) and FeeCollector (0xda7030C3A45EdF79421E8e7F8CcF4d7A3Fa4EeA3) that Deploy-all produced on 2026-09-15 are superseded and not recorded (agreed in review); the live UniversalRouter and FeeCollector entries carry a note naming the superseded address (rendered into 999.md once forge-chronicles #7 lands), the README's "Superseded deployments" section lists both, and the router must never be added to a permissioned hook allowlist. The 2.1.2 router deployed 2026-09-17 is kept as UniversalRouter#v2.1.2.

Ownership is governance from block one. Every protocol owner/admin role (v2 feeToSetter, v3 owner, PoolManager owner, both descriptor ProxyAdmins) is the Uniswap Wormhole receiver 0x2d09d0c2f82c59b19b3c65a48ce2c550bf0921f9. The one exception, same as every other chain: FeeCollector is owned by the ops KMS EOA 0xbE84…1b55 because fee sweeps need a hot key. No handover step later.

HyperEVM specifics

  • Deploys need big blocks (30M gas, one per minute); the driver toggles the HyperCore flag on and off
  • Canonical v3 factory address is squatted on this chain, so all addresses are fresh
  • No EIP-7702, so no Calibur
  • Across SpokePool is live and wired into the router

Also in here

  • script/hyperevm/: runbook + driver, HyperCore big-block toggle, both-explorer verify script, and merge_extra_deploys.py for the few contracts forge-chronicles cannot record (unnamed CREATE2 deploys, pre-existing Permit2, routers deployed from the universal-router repo)
  • test/HyperEVMDeploy.t.sol: fork test, ownership + fake pools with swaps on v2/v3/v4 (7/7 against mainnet; not in CI since *.t.sol is skipped in foundry.toml, run via the driver)
  • NFTDescriptorDeployer: reuses the library if it already sits at its CREATE2 address instead of reverting
  • foundry.toml: skips src/pkgs/**/node_modules/**; a stale local yarn install inside the universal-router submodule was what broke local forge builds and made chronicles look unusable

Companion PRs: universal-router #517 (2.2.0 DeployHyperEVM.s.sol, the file the canonical router was deployed from), #518 (same on release/2.1.x for the 2.1.2 router), forge-chronicles #7 (chain-name override for 999, ssh-remote heading fix).

Registry: deployments/json/999.json written by forge-chronicles over the two Deploy-all broadcasts, then merge_extra_deploys.py (idempotent) adds the six entries chronicles cannot see and the PermissionsAdapterFactory constructor arg forge left undecoded; deployments/999.md rendered by chronicles. Two commits: tooling, then the deployment record. Every commitHash cites a commit already on main (chronicles' convention: the code the deploy ran from, never the record commit), so any merge mode is safe.

🤖 Generated with Claude Code

@socket-security

socket-security Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​hyperliquid-python-sdk@​0.24.0100100100100100
Addedpypi/​eth-account@​0.14.0100100100100100

View full report

@socket-security

socket-security Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

Ignoring alerts on:

  • pypi/hyperliquid-python-sdk@0.24.0
  • pypi/hyperliquid-python-sdk@0.24.0
  • pypi/eth-account@0.14.0
  • pypi/eth-account@0.14.0

View full report

Comment thread script/hyperevm/build_registry.py Outdated
if admin:
entry["proxyAdmin"] = admin[0]["address"].lower()
if t["transactionType"] == "CREATE":
entry["initcodeHash"] = cast("keccak", t["transaction"]["input"])[2:]

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.

Three deviations from what forge-chronicles writes, and this file gets copied into the briefcase repo on merge. initcodeHash is only recorded for CREATE txs, chronicles records it for CREATE2 too (5042.json PoolManager has one). For CREATE2 through the factory the input is salt plus initcode, so hash input[32:]. For the two proxies the hash covers the proxy creation input, chronicles hashes the implementation tx (extractor.js:203), so the NonfungibleTokenPositionDescriptor hash means something different than in every other registry. The Permit2 entry has no deploymentTxn or timestamp, which renders as "Invalid Date" in the generated markdown. V4SmokeTest reads .latest.Permit2.address, so keep the entry, in the 10143.json shape: deploymentTxn 0xb53a183abfae0bd1348b73e6505371d95be836bc6c6f5249b7d63f539eb2260b, timestamp 1739894100000.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed all three. One tweak on the CREATE2 hash: chronicles hashes the whole tx input, salt included, so I matched that. Proxies now hash the impl tx. Permit2 has the tx/timestamp you gave, verified onchain.

Comment thread deployments/index.md Outdated
| 4326 | Chain 4326 | [View Deployment](./4326.md) |
| 4663 | Chain 4663 | [View Deployment](./4663.md) |
| 5042 | Chain 5042 | [View Deployment](./5042.md) |
| 999 | HyperEVM Mainnet | [View Deployment](./999.md) |

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.

This links to deployments/999.md, which is not in the PR. No CI generates markdown for real chains, only the Tenderly workflow runs chronicles. The other chains committed theirs by hand. It builds from the JSON locally with node lib/forge-chronicles -c 999 --skip-json -e https://hyperevmscan.io. Two things first: chains.json in lib/forge-chronicles names 999 "Wanchain Testnet" and sorts it after 480, and the Permit2 entry renders as Invalid Date until the registry point is fixed.

@david-uniswap david-uniswap Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Generated with your command. Two things chronicles gets wrong (the heading with an ssh remote, and chainid.network still calling 999 Wanchain) are fixed at the source in forge-chronicles#7; until that merges the driver patches them by hand.

Comment thread script/hyperevm/README.md Outdated

### If the broadcast halts midway

Do not restart from scratch. Mark each landed contract `deploy:false` + `address` in

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.

Marking UniswapV3Factory deploy:false with an address skips the two calls that follow it in Deploy-all, enableFeeAmount(100, 1) and setOwner. If the run halts between the factory creation and setOwner, the deployer stays owner and the recovery run does not repair it. Worth a line here. Also every protocol returns early on its protocol-level flag (view-quoter-v3, mixed-quoter, universal-router, swap-proxy), not only swap-router-contracts, so the same caveat applies to those.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, added. Also noted that the protocol-level early return applies to view-quoter, mixed-quoter, UR and swap-proxy.

Comment thread script/hyperevm/hyperevm.sh Outdated

bigblocks-on|bigblocks-off|bigblocks-status)
MODE=${STEP#bigblocks-}
if [[ $MODE == status ]]; then PRIVATE_KEY=0x$(openssl rand -hex 32) $PY script/hyperevm/toggle_big_blocks.py status $HLFLAG;

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.

toggle_big_blocks.py derives the address from PRIVATE_KEY, so status with a random key queries a random address and always prints NONE. Pass the deployer address for status, or use the real key here too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yep, that was always going to print NONE. Status now takes the deployer address, no key needed.



def sh(*a, **k):
return subprocess.run(a, capture_output=True, text=True, env=ENV, **k).stdout.strip()

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.

sh() drops the exit code. If BriefcaseHashes fails to compile, ref is empty, the loop runs over nothing and the script prints ALL REPRODUCIBLE with exit 0. Same for individual hashes missing from ref. This is the pre-deploy gate, so use check=True and assert ref covers FQ. The README says 24/24, FQ has 23 entries, and the v3 NonfungibleTokenPositionDescriptor implementation is not covered because of the library link.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: hard-fails on compile errors or missing hashes. Count is 23 briefcase deployers plus the descriptor impl with the NFTDescriptor placeholder linked before hashing, SwapProxy is frozen and excluded.

Comment thread script/hyperevm/verify.sh Outdated
v FeeCollector $(a FeeCollector) src/pkgs/util-contracts/src/FeeCollector.sol:FeeCollector 0.8.19 200 paris no "$(enc 'constructor(address,address,address,address)' $FC_OWNER $UR $PERMIT2 $USDC)"

# ---- briefcase-pinned sources: UR + PermissionsAdapterFactory only reproduce at the briefcase-era submodule pins
pin() { local sm=$1 ref=$2; local t=$(git ls-tree $ref $sm | awk '{print $3}'); (cd $sm && git checkout -q $t && git submodule update --init --recursive -q); echo "$sm -> ${t:0:8}"; }

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.

pin ends with echo, so a failed checkout returns 0, and the script runs without errexit, so verification continues on whatever revision is checked out. No trap either, so an interruption between line 90 and 95 leaves both submodules on the briefcase-era commits. verify_swapproxy.sh has the same shape, it exits 0 after six failed polls or a Sourcify none.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added errexit, a trap that restores both pins, and real exit codes in both scripts.

Comment thread script/hyperevm/README.md Outdated
| canonical | SwapProxy, ReservesLens | frozen / via-ir | Etherscan links by bytecode match once verified on any chain |

Fallbacks: indexer lag (wait 60s, rerun with the contract name filter); `--guess-constructor-args` if an arg
is wrong; Sourcify partial match means metadata drift, re-run reproducibility check for that contract.

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.

foundry.toml sets bytecode_hash = none, so there is no metadata hash to match and Sourcify can only return a partial match. Right now 25 of 26 are partial, SwapProxy is the only exact one because it was compiled elsewhere. Partial is the expected result here, not a drift signal.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Right, bytecode_hash = none means partial is the ceiling. Reworded.

Comment thread script/hyperevm/build_registry.py Outdated
# drop contracts the task file did not deploy (e.g. ReservesLens already at its canonical address on 998)
order = [n for n in ORDER if not (n == "ReservesLens" and not P["v4"]["contracts"]["ReservesLens"]["deploy"])]
receipts = {r["transactionHash"]: r for r in b["receipts"]}
if len(creates) != len(order):

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.

With the NFTDescriptorDeployer change, a run where the library already exists (998, the case that motivated it) has no NFTDescriptor create tx and this count check fails. Only ReservesLens has an exclusion. The recovery flow in the README produces a shorter broadcast too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Now derives the expected list from every deploy:true flag and drops NFTDescriptor when no create landed at its address, so 998-style and recovery runs work.

@@ -0,0 +1,706 @@
{

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.

The other deployed chains do not keep task-pending.json after the deploy, only the timestamped copy. Deploy-all reads this file by chain id, so its presence reads as a pending 999 deploy. A rerun fails at the PoolManager CREATE2 collision in simulation, so nothing lands, but hyperevm.sh, verify.sh and build_registry.py all read task-pending.json, so removing it means pointing them at task-1789499742.json.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Removed, scripts default to the newest timestamped task file.

/// same address on every chain. NFTDescriptor has no constructor args, so if code already exists there
/// (e.g. a prior deploy on the same chain) it is byte-identical and is reused instead of reverting with
/// CreateCollision. Observed on HyperEVM testnet (998).
address private constant CREATE2_FACTORY = 0x4e59b44847b379578588920cA78FbF26c0B4956C;

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.

The lookup is pinned to 0x4e59, the fallback is a raw CREATE2 from the caller. In forge test, or with --create2-deployer set to another address, the two disagree: an existing canonical library gets reused although this deploy would not have created it there, and repeated calls still collide at the real address. Fine for the script path this repo uses, the comment should say the reuse only holds under the default factory.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated to say it only holds under the default factory.

@david-uniswap

Copy link
Copy Markdown
Collaborator Author

@SocketSecurity ignore-all
Both are the official upstream packages (hyperliquid-python-sdk, eth-account), used only by the local big-block toggle script. Nothing in CI or onchain imports them.

@david-uniswap
david-uniswap marked this pull request as ready for review September 18, 2026 21:29
Comment thread script/hyperevm/README.md
UniswapInterfaceMulticall and PositionManager show fork labels (Huskey/Claw) there, byte-identical code.

### If the broadcast halts midway

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.

This paragraph is unchanged. The reply on the earlier thread says the setOwner caveat was added, but it is not in the diff. Marking UniswapV3Factory deploy:false with an address skips enableFeeAmount(100, 1) and setOwner, so a halt between the factory creation and setOwner leaves the deployer as owner and the recovery run does not repair it. Same for the protocol-level early return on view-quoter-v3, mixed-quoter, universal-router and swap-proxy.

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.

Still unchanged on 8e6d061, the paragraph is now at line 108.

@david-uniswap david-uniswap Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You're right, my edit didn't land and I said it had. Fixed now, sorry about that.

@david-uniswap david-uniswap Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in the latest commit.

Comment thread script/hyperevm/extra-deploys.json Outdated
@@ -0,0 +1,86 @@
{
"_comment": "Contracts on chain 999 that the registry records outside the original Deploy-all broadcast, plus Deploy-all outputs to drop. latest.UniversalRouter is the re-cut 2.2.0 release (tag 2.2.0 @ 64027f3, includes the #499 sweep fix, v4-periphery a7af5b34) deployed 2026-09-18 from Uniswap/universal-router script/deployParameters/DeployHyperEVM.s.sol. The 2.1.2 router deployed 2026-09-17 stays as UniversalRouter#v2.1.2. Deploy-all's first-cut 2.2.0 router (0x0549...492A) and its FeeCollector (0xda70...EeA3) are superseded and not recorded; the FeeCollector recorded is the 2026-09-18 redeploy wired to the new router. Permit2 is the pre-existing canonical instance.",

@gretzke gretzke Sep 18, 2026 •

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.

script/deployParameters/DeployHyperEVM.s.sol does not exist at 64027f3 (tag 2.2.0), and I could not find it on any branch or PR of universal-router. The parameters for the canonical router on 999 are then only recorded here. Worth committing that file to universal-router so the deploy is reproducible from the source repo.

@david-uniswap david-uniswap Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed, committed it upstream as two draft PRs (main and the 2.1.x branch).

Comment thread script/hyperevm/hyperevm.sh Outdated
else PRIVATE_KEY=$(cast wallet private-key --account $ACCOUNT) $PY script/hyperevm/toggle_big_blocks.py $MODE $HLFLAG; fi;;

dry-run) # Deploy-all reads task-pending.json; the committed record is the timestamped copy, so stage it
cp "$(ls -t script/deploy/tasks/$CHAIN/task-*.json | head -1)" script/deploy/tasks/$CHAIN/task-pending.json

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.

task-*.json also matches task-pending.json. On a second dry-run the newest file is the pending copy itself, cp fails on a self-copy and set -e aborts. build_registry has the same glob and sorts by name, where task-pending.json sorts after every task-17…json, so it picks the pending file whenever one is present. Exclude task-pending.json from the glob in all three places.

@david-uniswap david-uniswap Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, the glob now skips the pending file everywhere.

Comment thread script/hyperevm/build_registry.py Outdated
e = {"address": addr if x.get("preexisting") else addr.lower(), "proxy": False, "deploymentTxn": tx, "initcodeHash": h}
latest[x["name"]] = {**e, "timestamp": ts, "commitHash": commit, **({"note": "pre-existing canonical deployment; not deployed by this repo"} if x.get("preexisting") else {"note": x["run"]})}
if x.get("preexisting"):
run1[x["name"]] = {**e, "input": x["input"]}

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.

Permit2 is folded into the Deploy-all history entry, so the markdown shows it under the September run with this repo's commit although it was deployed in February 2025 by someone else. Keep it in latest and drop it from history, or give it its own entry with its real timestamp.

@david-uniswap david-uniswap Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Permit2 now has its own history entry with its real date and no repo commit.

Comment thread script/hyperevm/extra-deploys.json Outdated
}
}
],
"skip": ["UniversalRouter", "FeeCollector"]

@david-uniswap david-uniswap Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Per our chat, dropped both again. The README keeps the never-allowlist warning for the first-cut router address.

@linear-code

linear-code Bot commented Sep 21, 2026

Copy link
Copy Markdown

ECO-966

@david-uniswap
david-uniswap force-pushed the david/hyperevm-deploy branch 6 times, most recently from bbcaa9e to 8828135 Compare September 21, 2026 18:01
Comment thread deployments/json/999.json Outdated
}
}
},
"NonfungibleTokenPositionDescriptor": {

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.

NonfungibleTokenPositionDescriptor and SwapProxy were created in the 2026-09-15 run (broadcast 1789501501114, e34ba78) but sit here in the 2026-09-18 FeeCollector redeploy entry, and their latest.commitHash is 58a1e67 instead of e34ba78. merge_extra_deploys.py line 77 takes the first history entry whose commitHash is on main. History is newest first, and once a previous merge has rewritten the FeeCollector run to 58a1e67, that entry wins. Select the original run explicitly (broadcast timestamp, or the oldest entry) and regenerate this file.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Right, newest-first bit me. The original run is now selected by its broadcast timestamp (pinned in extra-deploys.json) and both entries sit under e34ba78 again.

Comment thread script/hyperevm/merge_extra_deploys.py Outdated
txj = json.loads(sh("cast", "tx", tx, "--json", "--rpc-url", rpc))
data = txj["input"]
salt, initcode = "0x" + data[2:66], "0x" + data[66:]
created = sh("cast", "create2", "--deployer", txj["to"], "--salt", salt, "--init-code", initcode).split()[-1].lower()

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.

cast create2 prints address and salt on one line (1.8.4 prints "

\t"), so split()[-1] is the salt and the SwapProxy check fails with "created 0xd003..., not 0x0000000085e1...". The version that produced the committed file must have printed the address last. Compute the address in Python (keccak of 0xff, deployer, salt, keccak(initcode)) or take the first token, so the script does not depend on the cast output format.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, my cast version prints the address alone. Now computed from keccak(0xff, deployer, salt, keccak(initcode)) with no dependence on cast's output format.

Comment thread script/hyperevm/hyperevm.sh Outdated
# commitHash cites the main commit this branch is based on (chronicles convention; squash-safe). Override with COMMIT=.
for b in $(ls broadcast/Deploy-all.s.sol/$CHAIN/run-1*.json | sort); do
cp "$b" broadcast/Deploy-all.s.sol/$CHAIN/run-latest.json
node lib/forge-chronicles Deploy-all.s.sol -c $CHAIN -e https://hyperevmscan.io --force

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.

Rerunning $H registry on the committed file does not reproduce it. The chronicles call has no -r $RPC, and on a replay PositionDescriptor is already in latest as a proxy while the broadcast carries its implementation address, so extractor.js line 323 aborts with "No RPC URL provided". With the RPC added, chronicles dedupes against history contents, and drop_addresses removed the first-cut FeeCollector from history, so every replay re-adds it under a new tag before the merge drops it again, and history grows each run. Since this loop already replays every broadcast, deleting deployments/json/999.json at the start of the step and rebuilding from scratch fixes both, and removes the ordering dependency behind the misfiled entries.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed, the step now deletes the JSON first and rebuilds from every broadcast. A replay on the committed file is byte-identical.

latest["commitHash"] = target["commitHash"]
else:
if not x.get("nocommit"):
latest["commitHash"] = commit

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.

After this PR merges, git merge-base HEAD origin/main is HEAD itself, so a later rerun rewrites the router and UnsupportedProtocol entries from 58a1e67 to whatever main is at that moment although nothing was redeployed. If the convention is "the main commit the deploy was based on", record it once per run in extra-deploys.json instead of deriving it at merge time.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yep, derived at merge time was wrong. The base commit is pinned in extra-deploys.json now.

Comment thread script/hyperevm/hyperevm.sh Outdated
echo "deployer occurrences in constructor args (expect 0): $(jq -r '.transactions[] | select(.transactionType!="CALL") | .transaction.input' broadcast/Deploy-all.s.sol/$CHAIN/dry-run/run-latest.json | grep -oic ${DEPLOYER#0x})";;

deploy) # Deploy-all reads task-pending.json and, with rename:true, archives it as task-<ts>.json afterwards
[ -f script/deploy/tasks/$CHAIN/task-pending.json ] || { echo "no task-pending.json: write the exact task you intend to deploy first (make_feecollector_task.py, or copy a task-1*.json), never guess"; exit 1; }

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.

make_feecollector_task.py is deleted in this PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Removed.

Comment thread deployments/json/999.json
"deploymentTxn": "0xbc5218850f9c611b5835ed396f8f619e77510d7b2bf8550f4d7866c4f3eee5cb",
"initcodeHash": "cc8a477ccda73a74197ccac91cfe41adf7fa0d91131a84fc8c196f30fc387baf",
"timestamp": 1789678080000,
"commitHash": "58a1e67",

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.

Merged entries carry block timestamps in latest (UnsupportedProtocol 1789678080000) but their history run carries 1789678140000. Chronicles gives latest and history the same run timestamp, so the markdown shows two dates for one run. Minor.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed, merged entries use the run timestamp in latest like chronicles does.

david-uniswap and others added 2 commits September 21, 2026 14:15
Tooling for a dual-block chain (HyperEVM, 999) in script/hyperevm/: step driver, HyperCore big-block toggle,
explorer verification for hyperevmscan + Sourcify, and merge_extra_deploys.py, which adds to the
forge-chronicles registry the contracts chronicles cannot see (unnamed CREATE2 deploys, pre-existing Permit2,
routers deployed from the universal-router repo).

test/HyperEVMDeploy.t.sol: fork test asserting governance ownership (Wormhole receiver), deployer owns nothing,
and fake-token pools with swaps on v2/v3/v4 through the routers and UniversalRouter. Runs via hyperevm.sh test
(*.t.sol is skipped in foundry.toml).

NFTDescriptorDeployer reuses the library if it already sits at its CREATE2 address (collision seen on 998);
pragma >=0.8.0 for address.code.

foundry.toml skips src/pkgs/**/node_modules/** so a local yarn install inside a submodule no longer breaks
forge builds or forge-chronicles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Full stack on HyperEVM with governance ownership from block one: every owner/admin role is the Uniswap
Wormhole receiver 0x2d09d0c2f82c59b19b3c65a48ce2c550bf0921f9; FeeCollector is owned by the ops KMS EOA.

UniversalRouter is the re-cut 2.2.0 (universal-router tag 2.2.0 @ 64027f3, #499 sweep fix, v4-periphery
#564 in / #584 reverted) at 0x9aFe3C497e19501DB228F28CdBdD29bC98F65DBa, deployed from the universal-router
repo with the Across SpokePool and PermissionsAdapterFactory wired; FeeCollector redeployed pointing at it.
The 2.1.2 router (0xeD27...d6bF) is recorded as UniversalRouter#v2.1.2. Deploy-all's first-cut 2.2.0 router
and FeeCollector are superseded and not recorded (agreed in review); never allowlist that router in a
permissioned hook.

All 27 recorded contracts verified on hyperevmscan and Sourcify. Registry written by forge-chronicles plus
merge_extra_deploys.py; deployments/999.md rendered from it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
runs = {}
for x in cfg["extra"]:
addr, tx = x["address"], x["deploymentTxn"]
rc = json.loads(sh("cast", "receipt", tx, "--json", "--rpc-url", rpc))

@gretzke gretzke Sep 21, 2026 •

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.

Fixing in a follow-up PR: switching to the field accessors (cast receipt contractAddress / blockNumber, cast tx to / input), no --json left.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Switched to the field accessors (cast receipt contractAddress / blockNumber, cast tx to / input), no --json left.

forge build >/dev/null || exit 1
# never lose the committed registry if a step fails: work on a backup and restore on any error
cp "$J" "$J.bak" 2>/dev/null || true
trap '[ -f "$J.bak" ] && { echo "registry step failed, restoring $J"; mv "$J.bak" "$J"; }; cp broadcast/Deploy-all.s.sol/'$CHAIN'/run-1*.json /dev/null 2>&1' ERR

@gretzke gretzke Sep 21, 2026 •

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.

You're right, that cp was broken. Follow-up PR makes the trap copy the last file of the sorted list to run-latest.json explicitly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You're right, that cp was broken. The trap now copies the last file of the sorted list to run-latest.json explicitly.

@david-uniswap
david-uniswap merged commit a677c0d into main Sep 22, 2026
4 checks passed
@david-uniswap
david-uniswap deleted the david/hyperevm-deploy branch September 22, 2026 20:38
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.

2 participants