Verify Deployment PR
READ-ONLY audit of an already-executed deployment PR. Never broadcast a transaction,
never edit files, never re-run the live deployment. All on-chain data comes from the
block explorer (via sol2uml/Etherscan) or a read-only RPC.
Step 0 — Prerequisites (fail fast)
- All commands run from
contracts/: cd contracts.
- Require
contracts/.env with PROVIDER_URL and ETHERSCAN_API_KEY:
grep -oE '^(PROVIDER_URL|ETHERSCAN_API_KEY)=' .env. Source it for shell use:
set -a; . ./.env; set +a (so $ETHERSCAN_API_KEY is available to sol2uml).
- Missing
ETHERSCAN_API_KEY → checks 2/3/4 can't run → STOP and report the blocker.
- Require
gh authenticated (gh auth status). If absent, ask the user to paste the
PR body + deployed-address list rather than failing.
- Require
sol2uml on PATH (command -v sol2uml). If absent: npm i -g sol2uml.
- Verify you are on the correct branch. This is the most important prereq: every
check diffs the on-chain deployment against the local code, so a wrong branch (or a
dirty tree) silently produces meaningless results. Match the checkout to the PR head:
gh pr view "$PR" --json headRefName,state,mergeCommit
git rev-parse --abbrev-ref HEAD and git status --porcelain
- Require the current branch to equal the PR's
headRefName, OR — if the PR is already
merged — that the checkout contains its merge commit
(git merge-base --is-ancestor <mergeCommit> HEAD).
- On mismatch or a dirty working tree → STOP. Tell the user to
gh pr checkout <PR>
(or git checkout <headRefName> / git stash) and re-run. Do not verify against the
wrong code.
Step 1 — Gather PR context
PR=$1. gh pr view "$PR" --json title,body,files,baseRefName,headRefName,state,url.
- Find the changed Foundry deploy script(s):
git diff --name-only origin/master...HEAD -- 'contracts/scripts/deploy/**/*.s.sol'
(or read the PR files). Ignore 000_Example.s.sol. The folder under
scripts/deploy/<network>/ gives the network (mainnet, base, hyperevm,
…) — use it for sol2uml --network <net> and map it to the chain deployment file
(build/deployments-1.json, -8453.json, -999.json, etc.).
- From each script's
_execute(), enumerate every contract creation (new <Contract>(args))
and every _recordDeployment("<KEY>", address(...), type(<Contract>).name) call.
Resolve <KEY> → address from the matching entry in the chain deployment JSON's
contracts array (.name, .implementation). The JSON field is named
implementation even for proxies and non-implementation contracts. If a newly
recorded key is absent from the committed JSON, mark checks 1/3/4 ❌.
- The script identifier is the string passed to
AbstractDeployScript("<ID>").
Resolve its proposalId, tsDeployment, and tsGovernance from the matching entry
in the chain deployment JSON's executions array. Build expected governance actions
from _buildGovernanceProposal() calls to
govProposal.action(target, "signature(types)", abi.encode(args)).
If proposalId is 0/absent while governance is expected, mark check 5 ⚠️
("no proposalId recorded — resolve from on-chain Governor events or ask the deployer").
- Working set:
[{key, contractType, address, constructorArgs, proposalId, network, deployScript}].
Step 2 — Run the checks
Checks 2/3/4 are per-address and independent — fan them out with parallel Task
sub-agents (one per deployed contract) when there are several; otherwise run inline.
Each check yields: status (✅ pass / ⚠️ needs-human / ❌ fail), one-line evidence, a
details block, and a confidence (High/Med/Low). Absence of evidence is ⚠️/❌, never ✅.
1 — All deployed contracts listed in the PR description
- Compare the Step-1 deployed name+address set against the PR body.
- ✅ every deployed contract (name and address) appears; ❌ a deployed contract is
missing; ⚠️ names present but addresses missing/ambiguous.
2 — Verified on-chain code (and dependencies) matches the codebase
- For each deployed implementation address, from
contracts/:
sol2uml diff <address> .,node_modules --network <net> --apiKey "$ETHERSCAN_API_KEY"
This downloads the explorer-verified source for the address and diffs it (and its
dependencies) against the local checkout.
- ✅ sol2uml reports no differences (every file identical); ❌ any file differs — quote
the differing files/lines. For
*Proxy addresses run the same diff against the proxy
source; expect the standard proxy to match.
- Confidence High when the diff is clean/empty.
3 — Constructor arguments are correct
- Read the constructor expression (
new <Contract>(args)) corresponding to each
_recordDeployment in _execute() and resolve constants/addresses used by it.
- Compare those arguments to the on-chain "Constructor Arguments" on the explorer
(Etherscan contract page, or decode the tail of the creation tx input). Foundry's
chain deployment JSON does not store constructor args or transaction hashes, so do
not treat it as evidence for this check. They must match positionally.
- ✅ all args match; ❌ any positional mismatch (show Foundry-script value vs on-chain).
Proxies typically have no constructor args — note
[].
4 — The initialize / interaction tx matches the deploy script
- If
_execute() performs a post-deploy call — typically a proxy
initialize(...)/_initialize(...) — locate the corresponding on-chain
tx (explorer tx list for the proxy address, or the proxy's deployment receipt) and
confirm the arguments match the Foundry script.
- ✅ the initialize tx args match the script; ⚠️ the script has no init/interaction call
(nothing to check); ❌ args differ (show the diff).
5 — Governance proposal matches the deploy script
- Read the on-chain proposal directly via ethers — do NOT use
pnpm ops proposal --id,
it parses the id as a float and overflows on real (77-digit) proposalIds. Use the
GovernorSix (addresses.mainnet.GovernorSix) ABI with the id as a BigNumber:
g.state(id) and g.getActions(id) -> (targets, values, signatures, calldatas)
(calldatas are selector-stripped — the signature is a separate string).
- Build expected actions from
_buildGovernanceProposal(): resolve every target through
constants or resolver.resolve("<KEY>") and the chain deployment JSON, keep the
signature passed to govProposal.action, and evaluate the corresponding abi.encode
arguments to compare against each on-chain calldata.
- ✅ identical (same count, targets, signatures, calldatas); ❌ any divergence (show it).
Report the proposal
state: Executed for a fully-executed deploy; Pending/Active/
Queued is normal when reviewing before execution — flag it but it is not a failure.
6 — Smoke tests after fork execution — SKIPPED (per project decision). Mark N/A.
Step 3 — Synthesize
Verdict = VERIFIED only if checks 2,3,4,5 are ✅ (check 1 may be ⚠️ if the sole gap
is documentation; check 4 may be ⚠️ if there is genuinely no init/interaction call).
Any ❌ in 2–5, or an unresolved ⚠️, → BLOCKERS FOUND. Emit the report:
# Deployment PR Verification — #<PR> (<title>)
Verified against: <branch>@<short-sha>
Foundry script(s): <list> | Network: <net> | Proposal: <proposalId>
Verdict: <VERIFIED | BLOCKERS FOUND>
- [<✅|⚠️|❌>] 1. All deployed contracts listed in PR description — <evidence> (conf)
- [<✅|⚠️|❌>] 2. Verified code (+deps) matches codebase (sol2uml diff) — <evidence> (conf)
- [<✅|⚠️|❌>] 3. Constructor args correct — <evidence> (conf)
- [<✅|⚠️|❌>] 4. Initialize/interaction tx matches deploy script — <evidence> (conf)
- [<✅|⚠️|❌>] 5. Governance proposal matches deploy script — <evidence> (conf)
- [⏭️] 6. Smoke tests — skipped (N/A)
## Details
### 2. sol2uml diff <per-address: address, clean/diff, differing files>
### 3. Constructor args <per-address: Foundry-script args vs on-chain args>
### 4. Initialize tx <tx hash, decoded args vs Foundry-script args>
### 5. Proposal diff <on-chain getActions vs script actions, or "identical">
## Human still owes (manual)
- Off-chain Safe/multisig follow-ups noted in the Foundry script.
- That the PR's stated intent matches the on-chain effect (judgment).
- Anything marked ⚠️ above.
Notes / common false positives
- For
sol2uml diff, run from contracts/ and source .env first so $ETHERSCAN_API_KEY
is set; use --network matching the deploy folder (base→base, hyperevm→hyperevm, etc.).
- A proxy address will not match an implementation's source — diff a
*Proxy against
the proxy contract, not the impl.
- A clean
sol2uml diff (no file differences) is the pass signal for check 2; treat any
reported file difference as ❌ pending human review, not a warning.
- If
proposalId is 0/absent, do not fabricate one — mark check 5 ⚠️ and ask the deployer.
deployments/<network>/*.json remains a Talos compatibility registry, not the source
of truth for Foundry deployment execution. Use build/deployments-<chainId>.json for
Foundry-recorded names, addresses, and execution metadata.
- If a helper command errors/rate-limits, retry once, then mark that check ⚠️ "tool
error" with the stderr tail and continue the others. Never silently pass.
Do NOT
- Never send transactions, never re-run the deployment, never edit files. Explorer +
read-only RPC only.
- Never declare VERIFIED while any of checks 2–5 is ❌ or an unresolved ⚠️.