Chapter 10 · Deploy and release gate
Deploy and release gate
The Arc wiring sequence, the manifest, code-hash pinning and the canary.
One Solidity function, HookarcWiring._wire, deploys and connects every contract. The local script, the Arc script and the fork test all call it, so what was tested is what was broadcast. The deploy writes a manifest with addresses and code hashes; a canary then exercises the live system and flips a flag; the web app refuses to sign anything until both agree with the chain. The deployer runs the scripts from their own terminal with their own key; nothing in the repository or the tooling ever holds it.
The wiring sequence
- 01QuoteRegistry, LPLocker, CreatorFeeNFT, LaunchFactoryPlain deploys. The registry and factory take the owner; the locker and NFT keep their deployer for a one-shot setter.
- 02Hook address miningA Uniswap v4 hook encodes its permissions in its address bits. HookMiner.find searches CREATE2 salts until the address carries exactly beforeInitialize, beforeAddLiquidity, beforeRemoveLiquidity, beforeSwap, afterSwap and both swap-return-delta flags. The hook is then deployed with that salt through the deterministic CREATE2 deployer at 0x4e59b44847b379578588920cA78FbF26c0B4956C (present on Arc) and the script asserts the address matches.
- 03One-shot wiringlocker.setFactory, nft.setMinter, factory.setHook. Each can be called once, ever.
- 04Router and burnerThe Router is deployed against the hook and marked trusted (setTrustedRouter). The FlywheelBurner is deployed and set as the USDC fee sink.
- 05Quotesregistry.add for USDC (0x3600…, USDC / USD feed) and cirBTC (0x171A…, BTC / USD feed) from ArcAddresses, each with a 90,000 s heartbeat; the registry reads and stores 6 and 8 decimals. cirBTC gets the treasury as its fee sink.
- 06Manifestcontracts/deployments/arc.json: chain id 5042, deploy block, every address, the owner and treasury, and keccak256 of the runtime bytecode of the hook, router and factory.
Running it on Arc
Both commands run from contracts/ in the deployer's own shell, with DEPLOYER set to the deployer address and PRIVATE_KEY to its key. The deploy profile compiles with the EIP-170 size settings; --slow sends one transaction per block, which the canary needs.
export FOUNDRY_PROFILE=deploy
forge script script/DeployArc.s.sol:DeployArc \
--rpc-url https://rpc.mainnet.arc.io \
--sender $DEPLOYER --private-key $PRIVATE_KEY \
--broadcast --slow
forge script script/Canary.s.sol:Canary \
--rpc-url https://rpc.mainnet.arc.io \
--sender $DEPLOYER --private-key $PRIVATE_KEY \
--broadcast --slow
deploy-arc.sh prints the same two commands with the environment filled in. Always pass --sender alongside --private-key: without it forge simulates value-carrying calls from an empty account and the creation fee step fails in simulation. The deployer wallet needs native USDC for gas plus 1 USDC for the canary's creation fee; at Arc's 20 to 100 gwei base fee the whole deploy plus canary costs on the order of one to two dollars.
The canary
Canary.s.sol reads the manifest and, with the deployer key, launches a USDC-quoted pool with all four blocks on, buys a few USDC worth, approves and sells the tokens back, claims the creator fees and sweeps the protocol fees to the burner. Each step is its own transaction (--slow), so the launch lock is exercised for real: the buy happens in the block after the launch. On success it writes the pool id, token, amounts and block into the manifest and sets canaryPassed: true. The canary pool stays visible on Discover as $CANARY / USDC.
The web write gate
useGate() runs in the app shell and before every write:
- A manifest must exist and its hook address must not be zero.
canaryPassedmust be true.- For chain 5042, the bytecode at the hook, router and factory addresses is fetched and its keccak256 compared with the manifest. Any mismatch closes writes with a reason shown in the masthead.
Local anvil (chain 31337) skips the hash check because every run redeploys.
Environments
| Local | Arc mainnet | |
|---|---|---|
| Chain | anvil 31337 | Arc 5042 |
| Script | DeployLocal.s.sol with mock feeds and 6- and 8-decimal mock quotes | DeployArc.s.sol, then Canary.s.sol |
| Manifest | contracts/deployments/local.json | contracts/deployments/arc.json |
| Web | copy to web/deployments/manifest.json | same |
| Indexer | indexer/.env.local from the manifest | same, PONDER_START_BLOCK = deployBlock |
Until arc.json is copied in, the app ships with a zero manifest: it builds and reads, but the gate keeps writes closed.
contracts/script/HookarcWiring.sol_wire()contracts/script/ArcAddresses.solchain id, PoolManager, USDC, cirBTC and the feedscontracts/script/DeployArc.s.solArc deploy + quote registration + manifestcontracts/script/Canary.s.solthe post-deploy canarycontracts/test/fork/DeployWiringFork.t.solthe same wiring against the live PoolManagerweb/lib/gate.tsuseGate()