Skip to content

ops: solidity-ibc-eureka v2 → v3 upgrade + SP1 v6.1 (Ethereum mainnet) - #20

Merged
gjermundgaraba merged 68 commits into
mainfrom
operations/2026-06-18-upgrade-v2-to-v3
Jun 26, 2026
Merged

gjermundgaraba merged 68 commits into
mainfrom
operations/2026-06-18-upgrade-v2-to-v3

Conversation

@gjermundgaraba

@gjermundgaraba gjermundgaraba commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

What this is

The Ethereum mainnet (chain 1) operation that upgrades the solidity-ibc-eureka deployment from v2 → v3 and migrates its Cosmos light clients to SP1 v6.1. The branch carries the deploy/upgrade scripts, verification tooling, the updated on-chain deployment state, and the cutover runbook for the operation.

What the full upgrade does

1. Access-control model: per-contract AccessControl → a shared AccessManager.
v3 retires the v2 per-contract roles in favour of one OpenZeppelin AccessManager (0x3fa3f45a…), whose ADMIN is a TimelockController (0xb3999B2D…) with a 72-hour delay. Every privileged action now routes through the timelock behind the governance Safe.

2. Core contract upgrades — in a mandatory order.
ICS20Transfer → ICS26Router → Escrow beacon → IBCERC20 beacon. The ICS20Transfer and ICS26Router upgrades each call initializeV2(accessManager) to move onto the new auth model; the order matters because ICS26Router.initializeV2 deletes the v2 admin records that ICS20Transfer.initializeV2 still reads. After the beacon upgrade, each escrow proxy takes a one-time initializeV2().

3. SP1 v6.1 light-client migration.
The Tendermint light clients move to SP1 v6.1 (sp1-programs v2.0.0): a fresh SP1ICS07Tendermint is deployed per client and ICS26Router.migrateClient(...) is run inside the same timelock window (migration is now AccessManager-controlled, not a per-client role). Migrated clients: cosmoshub-0 (Cosmos Hub) and ledger-mainnet-1; client-4 is dropped (its escrow is still initialized). The Groth16 gateway routes to the real v6.1.0 verifier, vkeys are taken from the published release, and the prover/relayer is cut to that same build in lockstep with the on-chain migration.

4. Rate limiting + ICS27 GMP.
RATE_LIMITER_ROLE (manager-wide in v3) is granted to the rate-limiter holder (0x4b46ea82…) on both migrated escrows. A new ICS27 GMP (general message-passing) IBC app is deployed (0xbebd14A6…) and registered on the router under the gmpport port.

How it's applied

Everything that must take effect at cutover is scheduled in one timelock window and then executed as a single atomic Safe MultiSend, so the chain never settles in a mixed v2/v3 state:

  1. 8 timelock schedule operations proposed via the 4-of-7 governance Safe — the 4 core upgrades, the 2 client migrations, and the 2 rate-limiter grants.
  2. 72-hour timelock delay.
  3. 1 atomic execute (MultiSend) that applies all 8 at once.
  4. Post-cutover: initialize the escrows, register ICS27 GMP, verify the on-chain end-state, then roll the relayer onto the new build.

What's in the diff

  • New v3 deploy/upgrade Solidity scripts (DeployV3AccessManager, DeployV3Core, implementation deploys) and the SP1 v6.1 light-client deployer + vkey tooling.
  • Shadow-fork rehearsal and on-chain verification/validation scripts (role discovery/validation, trust-root checks, vkey/verifier checks).
  • Updated deployments/mainnet/1.json (v3 AccessManager, ICS27 GMP, new implementations, migrated-client state) plus the broadcast artifacts.
  • Build/recipe updates (just recipes, Foundry config) and supporting tests.
  • The v2→v3 cutover runbooks.

gjermundgaraba and others added 30 commits May 29, 2026 15:46
Entire-Checkpoint: 7f21cc17edda
Entire-Checkpoint: 66327a5fe0dc
- vm.writeJson does not resolve bracket-notation paths, so the with-sp1
  rehearsal wrote junk top-level keys instead of updating the migrated
  light-client implementations in the shadow deployment JSON. Use dot
  notation like DeploySP1ICS07Tendermint already does.
- Fund the ID customizer on the fork; vm.deal only covers simulation, so
  the addIBCApp broadcast failed on mainnet forks where the account
  holds no ETH.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: ad70b2393387
- Shadow rehearsal: assert SP1 deployment/migration counts unconditionally
  so a core-only run proves no client was silently migrated
- Require broadcast sender to hold ADMIN_ROLE in role-management scripts,
  with a pointer to the timelock-* recipes when the admin is a timelock
- Bounds-check GRANT_ROLE/REVOKE_ROLE before casting to uint64
- Rename GrantPortCustomizerRole to GrantIdCustomizerRole
- Delete unreferenced DeployProxiedICS26Router/DeployProxiedICS20Transfer
- VerifyDeployment: actionable error when ICS27GMP is not yet registered
- Runbook: document red verify CI window, RATE_LIMITER_ROLE scope change
- Uniform deployment path building and forge fmt across touched scripts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 6ed894de6256
Execute the v2->v3 upgrade (core proxy/beacon upgrades + SP1 client
migrations) as one atomic transaction via a Safe MultiSend
(MultiSendCallOnly delegatecall through the timelock), eliminating the
mixed v2/v3 window the sequential per-operation execute created. This is
now the only v3 execute path.

- safe.just: add execute-v3-upgrade-multisend (builds the MultiSend
  payload + signer safeTxHash); consolidate the SafeTxStruct hashing into
  one operation-aware _get_safe_message_hash.
- test/SafeMultiSendV3Upgrade.t.sol: self-contained test with a real Safe
  + MultiSendCallOnly + TimelockController proving atomic all-or-nothing
  execution and predecessor ordering.
- package.json / remappings.txt / foundry.toml: add safe-smart-account
  v1.4.1 (dev) with a via_ir=false compile profile scoped to the Safe
  contracts.
- scripts/shadow-v2-to-v3-timelock-rehearsal.sh + shadow.just: rehearsal
  drives the real Safe via execTransaction + MultiSend; sequential
  execute path removed.
- runbooks/upgrade-v2-to-v3.md: document the atomic execute as the single
  upgrade path.
- deployments/testnet/11155111.json: refresh hub-testnet-0 +
  ledger-testnet-1 trusted state from the proof-api for the SP1 migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: da20a4fe7f69
…ire up pause recipes

- T37: use OpenZeppelin SafeCast.toUint64 for GRANT_ROLE / REVOKE_ROLE, dropping
  the manual bounds-check and the unsafe-typecast lint suppression.
- T36: rename the fresh-deploy helpers to a symmetric
  _deployICS26Stack / _deployICS20Stack / _deployICS27Stack trio; rename the shared
  base deployer to _deployICS27GmpContracts to avoid the name/overload clash.
- pause: populate pause.just with ops-pause-transfers / ops-unpause-transfers (via
  the ops-script helper), import it in the justfile, and point runbooks/pause.md at
  the real command instead of the non-existent `just pause-deployment`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 539ea7645d05
Entire-Checkpoint: e4485fe8a821
Operations are normally branched from main, but during the v3 transition the
upgrade tooling/runbook can't be on main until mainnet is upgraded (CI gate).
Add an optional `base` arg (default main) so the operation can be cut from the
v3-upgrade branch instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: a391af2fc4f1
… MultiSend

- safe.just / scripts/safe-propose.sh: propose timelock schedules to the Safe Transaction
  Service (auto-queued nonce, owner-signed); propose-schedule wraps the schedule-v3-* recipes.
- safe.just execute-timelock-multisend: pack any list of already-scheduled timelock ops into
  one atomic Safe MultiSend (operation=1). execute-v3-upgrade-multisend now delegates to it with
  the v3 upgrade as the default set + an EXTRA_TIMELOCK_OPS hook, so rate-limiter/role grants can
  be folded into a single timelock round on mainnet.
- sp1.just / scripts/check-relayer-vkeys.sh: verify the running proof-api serves the vkeys
  recorded in the deployment JSON; decode_create_client.py now also emits vkeys/verifier.
- runbook: propose-via-CLI flow, relayer-vkey gate, rate-limiter snapshot how-to, one-round folding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: c4a3b1bf0856
…1 clients

Live Sepolia (11155111) deploy state from the v2->v3 operation: .accessManager + .ics27Gmp, the
four v3 implementations, and the new SP1ICS07Tendermint clients (hub-testnet-0, ledger-testnet-1)
with the v6.1 vkeys/verifier and freshly fetched trusted state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 30a9d5c965a0
…ehearsal

Adds opt-in (REHEARSE_RATE_LIMITER_GRANT=1, default off) coverage of the one-round folding
path the v3 atomic execute now supports: schedules a representative rate-limiter grant
(AccessManager.multicall([setTargetFunctionRole, grantRole]), byte-identical to
GrantRateLimiterRole.sol), folds its execute into the atomic Safe MultiSend via
EXTRA_TIMELOCK_OPS (the production raw-0x mechanism), and asserts post-execute that
getTargetFunctionRole + hasRole reflect the grant — with an absent pre-check so the
assertion can't trivially pass. Core rehearsal is byte-for-byte unchanged when off.

Runbook: document REHEARSE_RATE_LIMITER_GRANT=1 to rehearse the fold before a mainnet run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 5a147e0802d2
…ete)

Final on-chain step of the v2->v3 + SP1 v6.1 operation: addIBCApp("gmpport",
ICS27GMP) on the upgraded ICS26Router, sent by the ID customizer. After this,
verify-deployment passes all sections (ICS26/ICS20/ICS27, escrows, both SP1
clients) and check-sp1-verifier confirms v6.1 routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 9ad39fcc680c
From the live testnet run: register-ics27-gmp must be sent from an
.accessManagerRoles.idCustomizers account (a Ledger here), not the deployer key.
Add a Signing note (PRIVATE_KEY vs --ledger/SENDER/MNEMONIC_INDEX, blind signing)
and spell out the role-gated sender in step 8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 8996768b1b43
Full testnet v2->v3 + SP1 v6.1 execution record: addresses, tx hashes, timeline,
tooling, verification status, findings, and mainnet TODOs. Temporary working note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: dbe968349ee4
…ady)

Adds an independent, deployment-JSON-driven on-chain validator and a
post-upgrade AccessManager role-testing plan, executed green on Sepolia.

scripts/validate-v3-roles.py
  - Verifies (A) every (target,selector)->role, (B) role membership exactly
    matches the JSON (reconstructed from RoleGranted/RoleRevoked events, so it
    catches missing AND stray holders + non-zero execution delays), (C) all
    proxies + escrows point authority() at the AccessManager, (D) reports the
    escrow setRateLimit wiring (upstream TODO #559). Trusts no deploy artifact.
  - Guards against running pre-cutover (zero accessManager). Sepolia: 32/32.

runbooks/post-upgrade-role-testing.md
  - Tiered method (Simulation / Proven-in-prod / Live round-trip / Fork-only),
    per-role gate-simulation matrix, live grant/revoke + pause/unpause +
    rate-limiter procedures, revert-selector reference, sign-off checklist.
  - Testnet execution record (2026-06-16): all 8 roles confirmed executable,
    all non-holders rejected, deployment restored to its exact baseline.
  - Mainnet adaptation: documents the on-chain reality that mainnet cannot be
    driven autonomously (4-of-7 Safe 0x7B96CD54, 72 h timelock 0xb3999B2D,
    AccessManager not yet deployed) and the adapted post-cutover scope.

runbooks/upgrade-v2-to-v3.md
  - Adds step 13 pointing at the role validator + testing runbook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 6e7cb0a9278f
Answers "how do we find all the role grants for the mainnet round": the
bootstrap only grants what's in the deployment JSON, but the live v2 model is
richer, so we need to enumerate ground truth and reconcile.

scripts/discover-v2-roles.py
  - For each live v2 contract (ICS26Router, ICS20Transfer, each Escrow): reads the
    full RoleGranted/RoleRevoked history (Etherscan logs API, no 50k-block cap),
    reconstructs current holders, confirms each with hasRole, self-discovers role
    names via on-chain getters, and classifies per-client LIGHT_CLIENT_MIGRATOR
    roles via their grant tx. Reconciles against deployments/<env>/<chain>.json +
    the v3 role model, flagging holders that would be dropped, rate limiters to
    re-grant, and roles with no v3 equivalent. Read-only.

Live mainnet (chain 1, pre-upgrade) findings, now documented:
  - The 6 bootstrap-migrated roles MATCH the JSON exactly.
  - RATE_LIMITER has real holders (0x4b46ea82, 0x64259f72 on the cosmoshub-0 and
    ledger-mainnet-1 escrows) NOT in the JSON -> must be re-granted (step 10);
    unlike testnet, this is not a no-op.
  - TOKEN_OPERATOR + 11 per-client LIGHT_CLIENT_MIGRATOR roles have no v3
    equivalent (dropped by design; governance-held only) -- confirm intended.
  - DEFAULT_ADMIN is the timelock -> becomes v3 ADMIN.

runbooks: document the discovery step + findings table in
post-upgrade-role-testing.md (Mainnet adaptation); point the v2->v3 step-7
snapshot and step-10 re-grant at the tool with the concrete mainnet holders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 8c35d7d98884
…tomERC20

The "dropped — confirm intended" note undersold the v3 story. TOKEN_OPERATOR_ROLE
in v2 only gated grant/revokeMetadataCustomizerRole on IBCERC20 (which in turn
gated setMetadata) — i.e. it controlled who may relabel IBCERC20 token metadata.
v3 removes mutable IBCERC20 metadata entirely; the comparable capability is the
custom-ERC20 flow (setCustomERC20 under ERC20_CUSTOMIZER, which IS migrated to the
same 0x4b46ea82 holder). Only in-place relabel of an auto-deployed token is gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 51b12eb3f290
Replaces the earlier hedge ("confirm it survives / re-register") with the verified
conclusion. Metadata customization WAS used on mainnet (5 of 13 auto-deployed
tokens carry custom name/symbol/decimals — Cosmos Hub ATOM, SEDA, Nillion, two
hub-testnet ATOMs) and it survives the v2->v3 IBCERC20 beacon upgrade unchanged
(identical IBCERC20_STORAGE_SLOT + struct; proxies are never re-initialized; v3
just freezes it) — no action needed. Adds the enumeration caveat that
setCustomERC20 registers external tokens with no event, so the registered-denom
set is 13 beacon + 6 external = 19, not 13. Kept proportionate (informational).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 23916e2df34f
…iters

discover-v2-roles.py: etherscan_logs() previously treated any non-list result
as end-of-logs, so an Etherscan throttle (result is a string, e.g. "Max rate
limit reached") silently returned a partial/empty set while still printing
"JSON matches live v2 holders" — which could drop the RATE_LIMITER re-grant set.
Now distinguishes a legitimate "No records found" from a throttle/error, retries
with backoff, and exits non-zero rather than under-reporting holders.

validate-v3-roles.py: RATE_LIMITER(5) now reads an optional
.accessManagerRoles.rateLimiters array so the post-step-10 mainnet run (where
role 5 is non-empty) matches instead of flagging live holders as UNEXPECTED;
absent key -> empty -> pre-step-10/testnet behavior unchanged (still 32/32).
Also corrects the docstring: the (target,selector)->role table is hand-encoded
and cross-checked against IBCRolesLib + the v3 deploy wiring, not pulled from
compiled methodIdentifiers as the old comment claimed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: c83777179889
- New runbooks/operations/2026-06-15-upgrade-v2-to-v3/RECORD.md: durable home
  for the testnet execution record (addresses, tx hashes, Safe nonces, SP1 v6.1
  details), the confirmed mainnet pre-upgrade state, the authoritative mainnet
  grant-set/token audit, findings, and a mainnet-execution placeholder — so the
  TMP_WHAT_HAS_BEEN_DONE.md content survives its removal before merge.
- RUNBOOK.md in the operation folder -> thin pointer to the canonical procedure
  + RECORD (it was a stale point-in-time copy); justfile new-operation gets a
  root-cause note so future operations don't re-diverge.
- README: runbooks index + operations-log pointer + the discover/validate scripts.
- upgrade-escrow.md / upgrade-ibcerc20.md: replace the non-existent
  `decode-*-upgrade` signer-verification recipe with the real `get_safe_hashes`
  (correct arity) and fix the duplicated step numbering.
- upgrade-v2-to-v3.md: mainnet JSON-is-pre-v6.1 note + verifier trio; --version
  before --write; SRC_CHAIN-is-a-module-id warning; 4-of-7 hardware delegatecall
  proposal path; client_id<->schedule reconciliation; Ledger-Live path caveat;
  rate-limiter snapshot points to RECORD.
- post-upgrade-role-testing.md: judge mainnet runs by `0 failed` (count scales
  with escrow count: testnet 2 -> 32, mainnet 3 -> ~33); role-5 non-empty
  post-step-10; mainnet validate prereqs (AccessManager + ics27Gmp populated,
  explicit FROM_BLOCK); repoint the TMP link to RECORD.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 7722e74b9116
bun >=1.2 (repo uses 1.3.14) resolves from the text bun.lock (solidity-v3.0.1 /
04b9767) and ignores the legacy binary bun.lockb, which still pinned v2.0.0.
Drop the dead lockfile so nothing can resolve the old pin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: cc66b71ce41e
- safe-propose.sh: add a Ledger signing branch (LEDGER=1/--ledger,
  MNEMONIC_INDEX, MNEMONIC_DERIVATION_PATH) that blind-signs the EIP-712
  safeTxHash; takes precedence over PRIVATE_KEY and is inherited by the
  propose-schedule/safe-propose recipes through the env. PRIVATE_KEY path
  is byte-for-byte unchanged.
- safe.just (execute-timelock-multisend): before packing, verify every
  sub-op is a pending TimelockController operation on-chain
  (isOperationPending) so a mistyped/unscheduled client id or a
  byte-mismatched folded grant aborts the build instead of silently
  producing a shorter-but-valid MultiSend. Uses Pending (not Ready) so it
  still passes while previewing during the delay window; skipped with a
  warning if no RPC is configured.
- RevokeRole.sol: refuse REVOKE_ROLE=0 (ADMIN) unless ALLOW_REVOKE_ADMIN=true,
  so a fat-fingered admin revoke cannot brick governance.

Entire-Checkpoint: 7c790b340c9a
Record the resolved pre-mainnet decisions and the now-implemented tooling:
- client-4 (08-wasm-301) DROPPED (no prod relayer module); migrate set =
  cosmoshub-0 + ledger-mainnet-1.
- sp1-programs final v2.0.0 cut at the rc.2 commit -> vkeys byte-identical.
- SRC_CHAIN module ids pinned from prod relayer config: cosmoshub-0<-cosmoshub-4,
  ledger-mainnet-1<-ledger-mainnet-1; proof-api via localhost.
- ID/ERC20 customizer 0x4b46ea82 is a 2-of-5 Safe (verified on-chain), so
  step 8 (addIBCApp) is a Safe CALL tx, not a Ledger broadcast (corrects the
  prior step-8 note).
- governance-Safe proposer signs with a Ledger at MNEMONIC_INDEX=1.
- document the ledger proposer branch and the timelock-pending / admin-revoke
  guards landed in the companion commit.

Entire-Checkpoint: 718da83517b7
….md)

Continues from TMP_WHAT_HAS_BEEN_DONE.md (testnet) at its "What remains for
mainnet" handoff: confirmed mainnet pre-upgrade state, the locked decisions
(client-4 dropped, sp1-programs v2.0.0 at the rc.2 commit, SRC_CHAIN ids,
2-of-5 customizer Safe, Ledger proposer), the mainnet-specific tooling and
hardening, the green mainnet-fork rehearsal, the single-round cutover plan,
and the remaining checklist. Temp working note, to be removed before merge.

Entire-Checkpoint: 4c84c00513e8
gjermundgaraba and others added 22 commits June 18, 2026 09:59
The timelock rehearsal registered ICS27GMP with an impersonated
single-sender forge broadcast, so the production mainnet step-8 path — a
2-of-5 customizer-Safe execTransaction (addIBCApp is gated by
ID_CUSTOMIZER_ROLE, held by the 2-of-5 Safe 0x4b46ea82…) — was never
exercised.

Add a generic safe_exec_tx() helper (the same approveHash +
prevalidated-signature machinery execute_atomic already uses, generalized
over to/data/operation) and a register_ics27() dispatcher: if the
customizer has code (a Safe), drive addIBCApp via the real execTransaction
CALL; if it's an EOA (testnet Ledger), keep the single-sender broadcast.
Asserts getIBCApp(gmpport) == the ICS27 proxy afterwards. execute_atomic
is left untouched (additive only).

Verified end-to-end on a mainnet fork (2026-06-18): the 2-of-5 customizer
Safe execTransaction lands, getIBCApp(gmpport) == the ICS27 proxy, and
VerifyDeployment passes. Record the result in RECORD #8 and the runsheet
T-minus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 8dac2e162e6c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: de80ce427ecd
Phase A executed on mainnet (chain 1, 2026-06-18) and verified on-chain:
- deployments/mainnet/1.json: v3 AccessManager + ICS27GMP, 4 v3 impls, and
  SP1 v6.1 clients (cosmoshub-0, ledger-mainnet-1) with fresh trusted state,
  v6.1 vkeys, and gateway verifier.
- Rate-limiter re-grant set: drop 0x64259f72 (decision #9) -> 2 grants, an
  8-sub-call atomic fold; --expect-subcalls 8. Propagated across RECORD,
  CUTOVER-RUNSHEET, SIGNER-CHECKLIST, FINAL-READINESS-REPORT, upgrade-v2-to-v3,
  post-upgrade-role-testing, signer-verify.sh, and the shadow rehearsal.
- Phase-A Foundry broadcast artifacts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: a2d4807b6141
The owner-only check (getOwners) hard-failed for the mainnet gov-Safe proposer
0x5622612b, which is a registered tx-service DELEGATE (label 'Gjermund Proposer',
delegator 0x64ACC525), not one of the 7 owners. Testnet never hit this (its 1-of-2
Safe proposer was an owner). Now: a non-owner sender is allowed iff it is a
registered delegate of the Safe (queried from /api/v2/delegates), failing closed if
the lookup can't be made. Delegates may propose but never count toward threshold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 807903c54660
…onces 18-25)

All 8 safeTxHashes independently confirmed via the gov Safe getTransactionHash and
present in the tx-service queue. Includes the 2 rate-limiter grant execute blobs as the
Phase-C EXTRA_TIMELOCK_OPS (holder 0x4b46ea82; 0x64259f72 dropped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 115e0e7c2bb8
Replaces the ‹N›..‹N+7› placeholders with the actual proposed nonces and their
on-chain-confirmed safeTxHashes. Step-7 execute pinned to nonce 26 (conditional) and
the addIBCApp row left for Phase D.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 0ceacd40859e
…creen

Adversarial multi-persona + consistency review (gov/Ledger, WSL, customizer,
attacker, consistency) surfaced 8 blockers + 13 should-fix; all folded in as
one-line imperatives, then the doc rewritten 190->89 lines so the per-tx
ceremony (DO-NOT-SIGN box + steps 1-5) fits one screen and setup/manual mode
move to "do once".

Security-relevant fixes:
- manual mode: --expect MUST come from the table, never the Safe UI (a UI hash
  always self-matches -> proves nothing).
- duplicate-nonce: sign only on a single PASS AND exit 0; a STOP beside a PASS
  still means stop (delegates can now propose, so same-nonce collisions exist).
- stamp the frozen script's sha256 (7158537e...5bb0); add a second-source
  cross-check; Ledger blind-signing; full WSL path (CRLF/file/Ctrl+Shift+V);
  network-pinned Safe links; reject-on-device is safe.

Verified: sha256 == scripts/signer-verify.sh; all 8 safeTxHashes byte-identical
to COORDINATOR-HASH-TABLE.md; script run live against nonces 18-25 -> PASS/exit-0
with matching hashes. Frozen script untouched. Phase C/D rows remain placeholders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 465623492588
Signers only validate what they sign in the current round, so SIGNER-CHECKLIST.md
is now the schedule round only — governance signers, the 8 schedules (nonces
18-25), one Safe hardcoded into the command. Dropped the two-signer framing, the
execute (DELEGATECALL / --expect-subcalls 8), and the customizer step.

The execute and customizer signer wording moves to SIGNER-CHECKLIST-LATER-ROUNDS.md
(marked NOT for distribution), reusing the schedule-round setup + procedure, ready
to hand out as its own one-round doc when each round opens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 7af96037461b
…efs)

Rename runbooks/operations/2026-06-15-upgrade-v2-to-v3/ -> 2026-06-18-... and
update every operation-identifier reference (path links + "Branch:" labels) in
README.md, justfile, upgrade-v2-to-v3.md, post-upgrade-role-testing.md, and the
operation's own RECORD / readiness docs. Historical event dates (06-16/06-17
rehearsals, decisions, on-chain snapshots) are left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 54f689428862
…e + signer)

Trim this branch to the upgrade code + the current-round signer artifacts. Remove
the operation's working/narrative docs (they live on gjermund/v2-to-v3-later-rounds):
  RECORD, READINESS-REVIEW, FINAL-READINESS-REPORT, REVIEW-RESPONSE, RUNBOOK,
  CUTOVER-RUNSHEET, and SIGNER-CHECKLIST-LATER-ROUNDS.

All code stays (solidity, broadcasts, deployments incl. mainnet/1.json, tests, CI,
foundry, just recipes, env example), as do the repo runbooks and README. Fixed the
two user-facing links the removals broke: SIGNER-CHECKLIST footer (dropped the
CUTOVER-RUNSHEET pointer) and README operations-log (repointed RECORD -> SIGNER-CHECKLIST).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 1caabae8e099
…oads from the table

signer-verify.sh no longer calls the Safe transaction service (the fetch was only
convenience and added curl/jq/API-key deps while contributing nothing to trust —
the anchor was always the published hash + the Ledger). It is now a pure offline
generator: given a nonce it reads that tx's canonical payload from
COORDINATOR-HASH-TABLE.md, recomputes the EIP-712 hashes, decodes the action, and
prints the exact card a signer must confirm in the Safe UI + Ledger. Needs only
`cast` — no network, no jq.

- COORDINATOR-HASH-TABLE.md gains a machine-readable per-nonce payloads block
  (nonce|safe|to|operation|safeTxHash|data); every data recomputes to the published
  safeTxHash (verified, and those match the on-chain getTransactionHash).
- New signer command is just `signer-verify.sh <nonce>`; manual mode (paste row
  fields FROM THE TABLE) remains as a fallback. Closes the manual-mode tautology
  (payload no longer sourced from the UI).
- SIGNER-CHECKLIST.md reworked for the offline flow; publishes sha256 for BOTH the
  script (b3f66119…) and the table (59bcf738…) — the table now drives verification,
  so its integrity matters as much as the script's.

Verified: all 8 nonces (18-25) generate PASS offline with matching hashes; tampered
data or tampered published hash -> REJECT; unknown nonce -> no row; default
table discovery (co-located files) works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 57670459e7d7
…rop the download ceremony

Signers check out the repo on the branch the coordinator names; that branch IS the
trust anchor (git verifies file contents), so the per-file sha256 / second-source /
"download both files" ceremony is gone. The substantive protection remains the
card-vs-Safe-UI-vs-Ledger comparison.

- signer-verify.sh auto-discovers the operation's COORDINATOR-HASH-TABLE.md under the
  repo (<repo>/runbooks/operations/*/), so the command is just
  `bash scripts/signer-verify.sh <nonce>` from the repo root. --table still overrides.
- Tolerate CRLF in the table (Windows `git checkout`), and add .gitattributes
  (`*.sh` + the table = eol=lf) so scripts stay runnable when checked out on Windows/WSL.
- SIGNER-CHECKLIST.md setup collapses to: check out the named branch, install `cast`,
  enable Ledger blind-signing. No downloads, no sha256, no jq.

Verified: all 8 nonces PASS via repo auto-discovery (from root and a subdir); tampered
payload -> REJECT; no .gitattributes renormalization churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6d823ff3c001
Per direction, the signer doc says one thing: how to VALIDATE the transactions
they're about to approve — nothing about how to sign, enabling Ledger blind
signing, device setup, or platform specifics (Mac is the default; others figure
it out). Removed the signing-ceremony mechanics, the Ledger-setup bullet, and the
Windows/WSL notes; setup is now just "check out the named branch + install cast".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3db96e6c1d17
The paragraphs were hard-wrapped at ~100 cols, splitting sentences across source
lines. Unwrap each paragraph / list item / blockquote line to a single line
(tables and code blocks unchanged). Content identical; verifier unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 30b942b87b7c
…nguage

Fill the send-time blanks now that they're decided: clone URL
(git@github.com:cosmos/eureka-ops.git) and branch
(operations/2026-06-18-upgrade-v2-to-v3). Remove all reporting/"trusted channel"/
all-clear wording — signers know to report back; the doc just says how to validate
and to not approve anything that doesn't.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f5edb94858ef
…le table command

Pre-send readiness review found two should-fixes:
- signer-verify.sh didn't know upgradeEscrowTo(0xaaa2c343) / upgradeIBCERC20To(0x06ab20bc),
  so nonces 20/21 (the Escrow + IBCERC20 beacon upgrades) printed "(unknown selector ...)"
  with no impl line — alarming to a signer. Whitelist both; they now show the new beacon
  impl. Decode-only; the safeTxHash already bound the payload (all 8 still PASS, tamper REJECTs).
- COORDINATOR-HASH-TABLE.md carried the pre-offline command
  (`signer-verify.sh 1 <safe> <nonce> --expect <hash>`, now errors); updated to
  `bash scripts/signer-verify.sh <nonce>`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 80d341d1b2c5
- SIGNER-CHECKLIST-EXECUTE.md (new): dedicated cutover-round checklist —
  DELEGATECALL to MultiSendCallOnly, `signer-verify.sh 26 --expect-subcalls 8`,
  sign-only (coordinator executes, only after the delay).
- COORDINATOR-HASH-TABLE.md: nonce-26 payload row + Phase-C section
  (safeTxHash 0xe999dee7ac3a0a003383efb2fa45c9b8105ef8c21ab8322dcd9371173f0a637a).
- SIGNER-CHECKLIST.md: cross-link the execute checklist.

nonce-26 atomic execute independently verified: matches the gov Safe's on-chain
getTransactionHash(26) and a current-state mainnet fork dry-run (warp +72h,
execTransaction through the real Safe succeeds, VerifyDeployment passes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 1fdd0763c477
…le verify cmd

The operator-facing runbook (runbooks/upgrade-v2-to-v3.md, lines 133/241/344)
references these two docs, but they were dropped from this branch in 2242e96 —
leaving broken links on the branch the operator executes from. Restore the
current copies from the archive branch (gjermund/v2-to-v3-later-rounds @ 7a7f67d,
which already carries the Phase-B-executed / Phase-C-built narrative). xref check:
0 dangling relative .md links across the runbook + both restored docs.

Also fix one stale signer-verify invocation in the runsheet appendix
(`~/signer-verify.sh 1 <safe> <nonce> --expect …` -> the current offline form
`scripts/signer-verify.sh <nonce>` / `... 26 --expect-subcalls 8`) now that the
runsheet is operator-facing on this branch.

Note: the step-8a relayer-cut owner (runsheet line 217) is still a run-time
placeholder to be assigned before the execute window.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 589e2d62e7cc
From the live-proposal validation workflow (2026-06-22):
- Execute the nonce-26 bundle via the Safe UI / device Execute button — it
  auto-sorts the collected signatures ascending. A hand-assembled
  `cast send execTransaction` with a non-ascending blob reverts GS026
  (recoverable; does not consume nonce 26). Avoid hand-assembly.
- Collect 4-of-7 on the existing posted object — never re-propose; the
  REQUIRE_READY=1 rebuild is byte-identical and for the ready-assert only.
- Minutes before Execute, re-query all 8 sub-ops (isOperationPending==true &&
  getTimestamp==1782217319) and freeze other gov-Safe proposals: the gov Safe
  holds CANCELLER, so a stray 4-of-7 cancel would atomically void the bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: d450fab78cf5
The proposing Safe was hardcoded to .safe in the deployment JSON (the
governance Safe). Add a --safe <addr> override so the same flow can
propose to another Safe — e.g. the 2-of-5 customizer Safe used to
register ICS27GMP (addIBCApp) in the v2->v3 cutover. Everything
downstream (nonce(), domain separator, owner/delegate check, POST URL)
already flows from this one value, so the override is sufficient.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: e017821645aa
The dry-run simulation output from building the rate-limiter grant
timelock ops folded into the nonce-26 cutover bundle. Tracked to match
the repo's existing broadcast/**/dry-run convention; no secrets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 388778e81697
Record the completed cutover: Phase C atomic execute (gov-Safe nonce 26,
tx 0x65db3718, 2026-06-25 14:58:11 UTC, status 1, gas 527,847) and all of
Phase D (escrow initializeV2 x3, relayer roll to v6.1, ICS27GMP register,
verify-deployment + check-sp1-verifier + validate-v3-roles 35/0), each with
its tx hash and timestamp. Flips status to COMPLETE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: ee299e5781e4
@gjermundgaraba
gjermundgaraba marked this pull request as ready for review June 26, 2026 13:26
@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown

Too many files changed for review. (109 files found, 100 file limit)

gjermundgaraba and others added 2 commits June 26, 2026 16:23
Move PROOF_API_FAILURE_MODE.md out of the repo root into runbooks/ — it is
durable, reusable troubleshooting knowledge (SP1 /dev/shm exhaustion), not a
this-op record, so it belongs alongside the other runbooks. Fix the two inbound
links in the cutover runsheet to the new path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 08c07b4b24a5
These three dry-run artifacts are pure simulations (hash:None, 0 receipts) of a
grant that actually executed via the Safe UI — throwaway, not templates. They
move to the archive/2026-06-18-upgrade-v2-to-v3 branch to keep that operation's
broadcast record complete, and off the lean PR to main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: d76483164ebc
@gjermundgaraba
gjermundgaraba requested a review from srdtrk June 26, 2026 14:30

@srdtrk srdtrk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm :)

@gjermundgaraba
gjermundgaraba merged commit 0d2b103 into main Jun 26, 2026
5 checks passed
@gjermundgaraba
gjermundgaraba deleted the operations/2026-06-18-upgrade-v2-to-v3 branch June 29, 2026 10:12
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