Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 65 additions & 3 deletions .github/workflows/manual-broadcast.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ on:
- 20260706-deploy-tokens-ethereum
- 20260807-deploy-missing-tokens
- 20260729-deploy-governance-timelock
- 20260817-deploy-orchestrator
network:
description: 'Network to broadcast against (default: base)'
required: true
Expand Down Expand Up @@ -77,9 +78,9 @@ jobs:
# network too (per-chain nonces cannot collide across networks), but a
# MULTI-CHAIN script broadcasts on every chain regardless of the
# selected network, so all its dispatches must share ONE group — list
# each multi-chain script in the ternary below.
# each multi-chain script in the expression below.
concurrency:
group: manual-broadcast-${{ inputs.script }}-${{ inputs.script == '20260729-deploy-governance-timelock' && 'all-chains' || inputs.network }}
group: manual-broadcast-${{ inputs.script }}-${{ (inputs.script == '20260729-deploy-governance-timelock' || inputs.script == '20260817-deploy-orchestrator') && 'all-chains' || inputs.network }}
cancel-in-progress: false
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand Down Expand Up @@ -110,7 +111,7 @@ jobs:
# broadcast to HyperEVM inside a single run regardless of which
# network was dispatched, so they always take the legacy path.
LEGACY_ARGS=()
if [[ "${NETWORK}" == "hyperevm" || "${SCRIPT}" == "20260729-deploy-governance-timelock" ]]; then
if [[ "${NETWORK}" == "hyperevm" || "${SCRIPT}" == "20260729-deploy-governance-timelock" || "${SCRIPT}" == "20260817-deploy-orchestrator" ]]; then
LEGACY_ARGS+=(--legacy)
fi
nix develop --command forge script "script/${SCRIPT}.s.sol" \
Expand Down Expand Up @@ -189,3 +190,64 @@ jobs:
echo "Etherscan: no ${KEY_VAR} configured, skipping ${NET}"
fi
done
# Same per-chain manifest-driven verification as the timelock step
# above, for the orchestrator singletons. The manifest carries the
# contract path per entry (two contracts land per chain), and both
# contracts have parameterless constructors, so no constructor-args
# guessing (and no RPC) is needed on the Etherscan leg.
- name: Verify orchestrator deployments
if: ${{ inputs.script == '20260817-deploy-orchestrator' }}
env:
# Etherscan-family keys, per chain. Absent or free-tier keys simply
# skip that chain's Etherscan leg; Sourcify still runs.
CI_DEPLOY_BASE_ETHERSCAN_API_KEY: ${{ secrets.EXPLORER_VERIFICATION_KEY || secrets.CI_DEPLOY_BASE_ETHERSCAN_API_KEY || vars.CI_DEPLOY_BASE_ETHERSCAN_API_KEY || '' }}
CI_DEPLOY_ETHEREUM_ETHERSCAN_API_KEY: ${{ secrets.EXPLORER_VERIFICATION_KEY || secrets.CI_DEPLOY_ETHEREUM_ETHERSCAN_API_KEY || vars.CI_DEPLOY_ETHEREUM_ETHERSCAN_API_KEY || '' }}
CI_DEPLOY_HYPEREVM_ETHERSCAN_API_KEY: ${{ secrets.EXPLORER_VERIFICATION_KEY || secrets.CI_DEPLOY_HYPEREVM_ETHERSCAN_API_KEY || vars.CI_DEPLOY_HYPEREVM_ETHERSCAN_API_KEY || '' }}
# foundry resolves EVERY `[etherscan]` entry's env var up front, even
# when `--verifier sourcify` is used, and errors on any that is
# undefined. Declaring the rest (empty when the secret is unset) keeps
# config resolution from failing on a chain this deploy never touches.
CI_DEPLOY_ARBITRUM_ETHERSCAN_API_KEY: ${{ secrets.EXPLORER_VERIFICATION_KEY || secrets.CI_DEPLOY_ARBITRUM_ETHERSCAN_API_KEY || vars.CI_DEPLOY_ARBITRUM_ETHERSCAN_API_KEY || '' }}
CI_DEPLOY_BASE_SEPOLIA_ETHERSCAN_API_KEY: ${{ secrets.EXPLORER_VERIFICATION_KEY || secrets.CI_DEPLOY_BASE_SEPOLIA_ETHERSCAN_API_KEY || vars.CI_DEPLOY_BASE_SEPOLIA_ETHERSCAN_API_KEY || '' }}
CI_DEPLOY_FLARE_ETHERSCAN_API_KEY: ${{ secrets.EXPLORER_VERIFICATION_KEY || secrets.CI_DEPLOY_FLARE_ETHERSCAN_API_KEY || vars.CI_DEPLOY_FLARE_ETHERSCAN_API_KEY || '' }}
CI_DEPLOY_POLYGON_ETHERSCAN_API_KEY: ${{ secrets.EXPLORER_VERIFICATION_KEY || secrets.CI_DEPLOY_POLYGON_ETHERSCAN_API_KEY || vars.CI_DEPLOY_POLYGON_ETHERSCAN_API_KEY || '' }}
run: |
MANIFEST=out/20260817-orchestrator-deployments.json
jq -c '.[]' "${MANIFEST}" | while read -r entry; do
CHAIN=$(echo "${entry}" | jq -r '.chainId')
NET=$(echo "${entry}" | jq -r '.network')
ADDR=$(echo "${entry}" | jq -r '.address')
CONTRACT=$(echo "${entry}" | jq -r '.contract')

# Sourcify: no API key, and the only verifier that covers all
# three chains including HyperEVM. Matches on bytecode, so it
# needs neither constructor args nor an RPC endpoint.
echo "Sourcify: verifying ${ADDR} (${CONTRACT}) on chain ${CHAIN}"
nix develop --command forge verify-contract \
--verifier sourcify \
--chain "${CHAIN}" \
--watch \
"${ADDR}" \
"${CONTRACT}"

# Etherscan: what actually surfaces on Basescan/Etherscan, since
# they do NOT ingest Sourcify. Needs a paid v2 key for any chain
# beyond mainnet, so run it only where a key is configured rather
# than failing the whole deploy on a missing or free-tier key.
KEY_VAR="CI_DEPLOY_$(echo "${NET}" | tr '[:lower:]' '[:upper:]')_ETHERSCAN_API_KEY"
if [[ -n "${!KEY_VAR:-}" ]]; then
echo "Etherscan: verifying ${ADDR} (${CONTRACT}) on ${NET}"
# Best-effort: a free-tier key is accepted by the config but
# rejected per-chain at request time, and that must not abort
# the loop before the remaining chains reach Sourcify. Sourcify
# above is the leg that gates this step's success.
nix develop --command forge verify-contract \
--chain "${NET}" \
--watch \
"${ADDR}" \
"${CONTRACT}" \
|| echo "Etherscan: verification failed for ${NET} (paid v2 plan required); Sourcify stands"
else
echo "Etherscan: no ${KEY_VAR} configured, skipping ${NET}"
fi
done
32 changes: 27 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,28 @@

### Deploy scripts

- **New broadcast script `20260817-deploy-orchestrator` deploys the orchestrator
singletons on every production chain.** `ST0xOrchestrator` and
`ST0xOrchestratorBeaconSetDeployer` postdate the audited 0.1.1 set that
`script/DeployProdV4_0_1_1.sol` ships, so they get their own
`manual-broadcast.yaml` entry rather than a suite in the 0.1.1 bootstrap
workflow. One dispatch covers Base, Ethereum and HyperEVM (multi-chain like
`20260729-deploy-governance-timelock`: shared `all-chains` concurrency group,
always `--legacy`), Zoltu-deploying the checked-out ref's CANDIDATE creation
bytecode — implementation first, then the beacon-set deployer, whose
constructor bakes an `UpgradeableBeacon` over the implementation pin.
Idempotent per chain: an already-deployed pin is codehash-asserted and
skipped, and a pin carrying any other code reverts `DeployedCodehashMismatch`
rather than deploying over a ref/deployment mismatch. Post-state asserts both
candidate codehashes, the beacon's implementation wiring, and that the
beacon's owner is the pinned initial owner or the chain's governance timelock.
The orchestrator proxy instance (`deploy(owner)`) is deliberately NOT created
— the vault-logic version lock gates `initialize` on the candidate vault +
receipt beacons being live, so instance creation is sequenced with the
vault-logic rollout. The script writes
`out/20260817-orchestrator-deployments.json`, and a manifest-driven workflow
step verifies both contracts per chain (Sourcify, plus Etherscan where a key
is configured).
- **The per-chain "deploy missing tokens" scripts are merged into one.**
`20260722-deploy-missing-tokens-ethereum` and `-hyperevm` were byte-identical
apart from the chain each hardcoded; both are replaced by
Expand All @@ -93,11 +115,11 @@
reading as "copy everything".
- **The canonical config table is allowed to run ahead of Base.** A row is
authored when a ticker is chosen and Base is pinned when it is deployed, so
the config table leads in that window; only the rows Base carries are read,
so the excess is inert. The genuine error is a config table SHORTER than
Base — a deployed Base row with no name/symbol to deploy under — which
reverts `TokenTableTooShort(configsLength, baseLength)`. Row-for-row key
drift between the two tables still reverts `TokenTableMisaligned`.
the config table leads in that window; only the rows Base carries are read, so
the excess is inert. The genuine error is a config table SHORTER than Base — a
deployed Base row with no name/symbol to deploy under — which reverts
`TokenTableTooShort(configsLength, baseLength)`. Row-for-row key drift between
the two tables still reverts `TokenTableMisaligned`.
- **Three checks the gap-fill scripts had dropped are back, matching
`20260706-deploy-tokens-ethereum`.** Each deployed vault is now read back
before the loop moves on — `AuthoriserNotWired` if it is not routed to the
Expand Down
Loading