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.

Deploy, canary and the release gateDeployArc and the fork test run the same _wire() code. The web app opens writes only when the manifest and the chain agree.
web gate (useGate)reads arc.jsonweb/deployments/manifest.jsonyesnoyesnoQuoteRegistryowner = deployerLPLockerdeployer-set factoryCreatorFeeNFTdeployer-set minterLaunchFactoryregistry · lockerHookMiner.find → CREATE2address bits = hook flagsOne-shot wiringlocker.setFactory · nft.setMinter · factory.setHookRouterhook.setTrustedRouter(router)FlywheelBurnersetFeeSink(USDC, burner)registry.add USDC + cirBTCfeed · decimals · 90,000 s heartbeatsetFeeSink(cirBTC, treasury)TREASURY env or deployerManifestaddresses + code hashesCanarylaunchWithBlocks · buy · sell · claimCreatorFees · sweepcanaryPassed = truereceipts written to manifestmanifest deployed?canaryPassed?keccak(code) == manifesthook · router · factory · 5042writes openwrites closed

The wiring sequence

  1. 01
    QuoteRegistry, LPLocker, CreatorFeeNFT, LaunchFactory
    Plain deploys. The registry and factory take the owner; the locker and NFT keep their deployer for a one-shot setter.
  2. 02
    Hook address mining
    A 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.
  3. 03
    One-shot wiring
    locker.setFactory, nft.setMinter, factory.setHook. Each can be called once, ever.
  4. 04
    Router and burner
    The Router is deployed against the hook and marked trusted (setTrustedRouter). The FlywheelBurner is deployed and set as the USDC fee sink.
  5. 05
    Quotes
    registry.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.
  6. 06
    Manifest
    contracts/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:

  1. A manifest must exist and its hook address must not be zero.
  2. canaryPassed must be true.
  3. 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

LocalArc mainnet
Chainanvil 31337Arc 5042
ScriptDeployLocal.s.sol with mock feeds and 6- and 8-decimal mock quotesDeployArc.s.sol, then Canary.s.sol
Manifestcontracts/deployments/local.jsoncontracts/deployments/arc.json
Webcopy to web/deployments/manifest.jsonsame
Indexerindexer/.env.local from the manifestsame, 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.

read the code
  • contracts/script/HookarcWiring.sol _wire()
  • contracts/script/ArcAddresses.sol chain id, PoolManager, USDC, cirBTC and the feeds
  • contracts/script/DeployArc.s.sol Arc deploy + quote registration + manifest
  • contracts/script/Canary.s.sol the post-deploy canary
  • contracts/test/fork/DeployWiringFork.t.sol the same wiring against the live PoolManager
  • web/lib/gate.ts useGate()