security(phase1b): Chain 138 admin transfer-then-revoke Forge scripts + runbook #1

Open
nsatoshi wants to merge 1 commits from devin/phase1b-chain138-admin-rotation-1776543648 into main
Owner

Summary

Part of the sequenced cleanup tracked in https://gitea.d-bis.org/d-bis/proxmox/issues/1. Phase 1b — Chain 138 admin transfer-then-revoke Forge scripts. Scaffolding only; no broadcast, no secret values committed.

The Chain 138 deployer EOA 0x4A666F96fC8764181194447A7dFdb7d471b301C8 was committed to d-bis/proxmox in plaintext and must be rotated off every contract it controls. Because the key is an EOA (not a multisig), rotation is transfer-then-revoke, not revoke-first — per docs/runbooks/MULTI_CHAIN_EXECUTION_KEY_ROTATION.md.

What this PR adds

File Purpose
script/rotation/RotateChain138Admin.s.sol Three Forge scripts: RotateStage1 (OLD signs, grants NEW), RotateStage2 (NEW signs, revokes OLD), VerifyChain138RotationComplete (read-only end-state check). Targets cUSDT, cUSDC, DODOPMMIntegration (plus POOL_MANAGER_ROLE + SWAP_OPERATOR_ROLE on DODOPMMIntegration).
script/rotation/README.md Index + follow-up table for the other chains (mainnet, Cronos, Polygon, Base, OP, BSC, AVAX, Arbitrum, Wemix).
scripts/rotation/chain138-rotation-runbook.md Operator runbook — simulate/broadcast forge script sequences, Stage-1/Stage-2 verification (cast call), rollback path (only possible between stages), per-chain template for follow-up PRs.

Design

  • Simulation by default. forge script ... -vvv simulates against the RPC and prints calldata; broadcasting requires explicit --broadcast --slow, and the runbook gates that behind per-tx operator approval.
  • Preflight asserts. Stage 1 aborts if OLD no longer holds all roles or if NEW already holds any role; Stage 2 aborts if Stage 1 didn't land. Cuts off the obvious "ran it twice by mistake" failure mode.
  • Signer identity checks. Each stage requires vm.addr(PRIVATE_KEY) to match the expected signer (Stage 1: OLD deployer; Stage 2: NEW admin). Prevents signing with the wrong key.
  • Canonical addresses as defaults. cUSDT / cUSDC / DODOPMMIntegration default to the values in docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md §5; overridable via env for test environments. OLD_DEPLOYER_ADDRESS defaults to 0x4A66…01C8.
  • Verifier reverts loudly. VerifyChain138RotationComplete reverts with a specific message if any contract still names OLD as owner/admin. Exit code 0 = rotation complete.

What this PR does NOT do

  • Does NOT broadcast anything. forge script simulates by default; --broadcast is a separate operator decision per the runbook.
  • Does NOT cover the other chains (mainnet / Cronos / Polygon / Base / OP / BSC / AVAX / Arbitrum / Wemix). Each of those needs its own enumeration + script and lands in a follow-up PR. Template in script/rotation/README.md.
  • Does NOT touch the PMM pool contracts on Chain 138. Their owner() / _OWNER_() reverted during Phase 1b discovery, suggesting they may be immutable DODO-style pools without a rotatable admin. A follow-up probes their actual role model with the source.
  • Does NOT rotate the MINTER_ROLE on CompliantFiatToken derivatives (cXAUC / cXAUT etc.) — discovery shows the deployer holds DEFAULT_ADMIN_ROLE on those, and MINTER_ROLE is delegated elsewhere. Follow-up once the actual minter grants are enumerated on-chain.
  • Does NOT include any secret values. The runbook refers to PRIVATE_KEY as an env var the operator supplies locally.

Review & Testing Checklist for Human

Risk: yellow — scripts do nothing until --broadcast is added, but when they do run they change on-chain admin state permanently. Stage 2 is irreversible.

  • Read scripts/rotation/chain138-rotation-runbook.md end-to-end; confirm the simulate-before-broadcast + stage-then-revoke flow matches how you actually want this executed.
  • Read script/rotation/RotateChain138Admin.s.sol:
    • Canonical addresses in the DEFAULT_* constants match docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md §5.
    • The preflight require set covers the failure modes you care about.
    • The SWAP_OPERATOR_ROLE / POOL_MANAGER_ROLE grant in Stage 1 is intentional (deployer holds both today per the Phase 1b discovery).
  • Confirm whether PMM pools (0xff8d…0849, 0x6fc6…9D71, 0x9f74…0263) need admin rotation — follow-up PR pending.
  • Decide the NEW_ADMIN_ADDRESS target (Gnosis Safe preferred per runbook §0.2) and who holds the signer.
  • Dry-run locally against Chain 138 (read-only) before the actual rotation window:
    source smom-dbis-138/scripts/load-env.sh
    export NEW_ADMIN_ADDRESS=0x<candidate>
    forge script script/rotation/RotateChain138Admin.s.sol:RotateStage1 \
      --rpc-url "$RPC_URL_138" -vvv
    

Next

Once the NEW_ADMIN_ADDRESS decision is made, operator executes §2 and §3 of the runbook with per-tx approval. Follow-up PRs stage the same pattern for the other chains.

Tracking: https://gitea.d-bis.org/d-bis/proxmox/issues/1.

## Summary Part of the sequenced cleanup tracked in <https://gitea.d-bis.org/d-bis/proxmox/issues/1>. **Phase 1b — Chain 138 admin transfer-then-revoke Forge scripts.** Scaffolding only; no broadcast, no secret values committed. The Chain 138 deployer EOA `0x4A666F96fC8764181194447A7dFdb7d471b301C8` was committed to `d-bis/proxmox` in plaintext and must be rotated off every contract it controls. Because the key is an EOA (not a multisig), rotation is **transfer-then-revoke**, not revoke-first — per `docs/runbooks/MULTI_CHAIN_EXECUTION_KEY_ROTATION.md`. ### What this PR adds | File | Purpose | |---|---| | `script/rotation/RotateChain138Admin.s.sol` | Three Forge scripts: `RotateStage1` (OLD signs, grants NEW), `RotateStage2` (NEW signs, revokes OLD), `VerifyChain138RotationComplete` (read-only end-state check). Targets cUSDT, cUSDC, DODOPMMIntegration (plus `POOL_MANAGER_ROLE` + `SWAP_OPERATOR_ROLE` on DODOPMMIntegration). | | `script/rotation/README.md` | Index + follow-up table for the other chains (mainnet, Cronos, Polygon, Base, OP, BSC, AVAX, Arbitrum, Wemix). | | `scripts/rotation/chain138-rotation-runbook.md` | Operator runbook — simulate/broadcast `forge script` sequences, Stage-1/Stage-2 verification (`cast call`), rollback path (only possible between stages), per-chain template for follow-up PRs. | ### Design - **Simulation by default.** `forge script ... -vvv` simulates against the RPC and prints calldata; broadcasting requires explicit `--broadcast --slow`, and the runbook gates that behind per-tx operator approval. - **Preflight asserts.** Stage 1 aborts if OLD no longer holds all roles or if NEW already holds any role; Stage 2 aborts if Stage 1 didn't land. Cuts off the obvious "ran it twice by mistake" failure mode. - **Signer identity checks.** Each stage requires `vm.addr(PRIVATE_KEY)` to match the expected signer (Stage 1: OLD deployer; Stage 2: NEW admin). Prevents signing with the wrong key. - **Canonical addresses as defaults.** cUSDT / cUSDC / DODOPMMIntegration default to the values in `docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md` §5; overridable via env for test environments. `OLD_DEPLOYER_ADDRESS` defaults to `0x4A66…01C8`. - **Verifier reverts loudly.** `VerifyChain138RotationComplete` reverts with a specific message if any contract still names OLD as owner/admin. Exit code 0 = rotation complete. ### What this PR does NOT do - Does NOT broadcast anything. `forge script` simulates by default; `--broadcast` is a separate operator decision per the runbook. - Does NOT cover the other chains (mainnet / Cronos / Polygon / Base / OP / BSC / AVAX / Arbitrum / Wemix). Each of those needs its own enumeration + script and lands in a follow-up PR. Template in `script/rotation/README.md`. - Does NOT touch the PMM pool contracts on Chain 138. Their `owner()` / `_OWNER_()` reverted during Phase 1b discovery, suggesting they may be immutable DODO-style pools without a rotatable admin. A follow-up probes their actual role model with the source. - Does NOT rotate the `MINTER_ROLE` on CompliantFiatToken derivatives (cXAUC / cXAUT etc.) — discovery shows the deployer holds `DEFAULT_ADMIN_ROLE` on those, and `MINTER_ROLE` is delegated elsewhere. Follow-up once the actual minter grants are enumerated on-chain. - Does NOT include any secret values. The runbook refers to `PRIVATE_KEY` as an env var the operator supplies locally. ## Review & Testing Checklist for Human Risk: **yellow** — scripts do nothing until `--broadcast` is added, but when they do run they change on-chain admin state permanently. Stage 2 is irreversible. - [ ] Read `scripts/rotation/chain138-rotation-runbook.md` end-to-end; confirm the simulate-before-broadcast + stage-then-revoke flow matches how you actually want this executed. - [ ] Read `script/rotation/RotateChain138Admin.s.sol`: - [ ] Canonical addresses in the `DEFAULT_*` constants match `docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md` §5. - [ ] The preflight `require` set covers the failure modes you care about. - [ ] The `SWAP_OPERATOR_ROLE` / `POOL_MANAGER_ROLE` grant in Stage 1 is intentional (deployer holds both today per the Phase 1b discovery). - [ ] Confirm whether PMM pools (`0xff8d…0849`, `0x6fc6…9D71`, `0x9f74…0263`) need admin rotation — follow-up PR pending. - [ ] Decide the `NEW_ADMIN_ADDRESS` target (Gnosis Safe preferred per runbook §0.2) and who holds the signer. - [ ] Dry-run locally against Chain 138 (read-only) before the actual rotation window: ```bash source smom-dbis-138/scripts/load-env.sh export NEW_ADMIN_ADDRESS=0x<candidate> forge script script/rotation/RotateChain138Admin.s.sol:RotateStage1 \ --rpc-url "$RPC_URL_138" -vvv ``` ### Next Once the `NEW_ADMIN_ADDRESS` decision is made, operator executes §2 and §3 of the runbook with per-tx approval. Follow-up PRs stage the same pattern for the other chains. Tracking: <https://gitea.d-bis.org/d-bis/proxmox/issues/1>.
nsatoshi added 1 commit 2026-04-18 20:25:01 +00:00
security(phase1b): Chain 138 admin transfer-then-revoke Forge scripts + runbook
Some checks failed
CI/CD Pipeline / Solidity Contracts (pull_request) Failing after 53s
CI/CD Pipeline / Security Scanning (pull_request) Successful in 2m32s
CI/CD Pipeline / Lint and Format (pull_request) Failing after 19s
CI/CD Pipeline / Terraform Validation (pull_request) Failing after 11s
CI/CD Pipeline / Kubernetes Validation (pull_request) Successful in 15s
Validation / validate-genesis (pull_request) Successful in 16s
Validation / validate-terraform (pull_request) Failing after 15s
Validation / validate-kubernetes (pull_request) Failing after 2s
Validation / validate-smart-contracts (pull_request) Failing after 3s
Validation / validate-security (pull_request) Failing after 2m1s
Validation / validate-documentation (pull_request) Failing after 6s
1344219fd3
Part of the sequenced cleanup tracked in
d-bis/proxmox#1.

Scaffolding only. No broadcast. No secret values committed.

The Chain 138 deployer EOA 0x4A666F96...01C8 is in proxmox master in
plaintext and must be rotated off as owner/admin on all contracts it
controls. Because the key is an EOA (not a multisig), rotation is
transfer-then-revoke (not revoke-first) per
docs/runbooks/MULTI_CHAIN_EXECUTION_KEY_ROTATION.md.

- script/rotation/RotateChain138Admin.s.sol
  - RotateStage1 (signed by OLD): transferOwnership + grantRole
    DEFAULT_ADMIN_ROLE on cUSDT, cUSDC, DODOPMMIntegration; additionally
    POOL_MANAGER_ROLE + SWAP_OPERATOR_ROLE on DODOPMMIntegration.
    Preflight asserts OLD still holds each role and NEW does not.
  - RotateStage2 (signed by NEW): revokeRole on OLD for each of the
    above. Preflight asserts Stage 1 landed (NEW holds roles, OLD still
    holds roles).
  - VerifyChain138RotationComplete: read-only end-state check; reverts
    with a specific message if any contract still names OLD as owner or
    admin.
  - Default addresses are the canonical Chain 138 cUSDT / cUSDC /
    DODOPMMIntegration from docs/11-references/ADDRESS_MATRIX_AND_STATUS.md
    and .cursor/rules/chain138-tokens-and-pmm.mdc. Overridable via env.

- script/rotation/README.md: index + follow-up PR table for the other
  chains (mainnet, Cronos, Polygon, Base, OP, BSC, AVAX, Arbitrum,
  Wemix).

- scripts/rotation/chain138-rotation-runbook.md: authoritative operator
  runbook with simulate/broadcast cast sequences, Stage-1/Stage-2
  verification steps, rollback path (only possible between stages), and
  a per-chain template for the follow-up PRs.

Forge build passes (one mixed-case-variable lint note on field names
matching the contract names; not an error).

This PR does not cover other chains or execute the rotation — those
are explicit operator steps gated on per-tx approval.

Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
Some checks failed
CI/CD Pipeline / Solidity Contracts (pull_request) Failing after 53s
CI/CD Pipeline / Security Scanning (pull_request) Successful in 2m32s
CI/CD Pipeline / Lint and Format (pull_request) Failing after 19s
CI/CD Pipeline / Terraform Validation (pull_request) Failing after 11s
CI/CD Pipeline / Kubernetes Validation (pull_request) Successful in 15s
Validation / validate-genesis (pull_request) Successful in 16s
Validation / validate-terraform (pull_request) Failing after 15s
Validation / validate-kubernetes (pull_request) Failing after 2s
Validation / validate-smart-contracts (pull_request) Failing after 3s
Validation / validate-security (pull_request) Failing after 2m1s
Validation / validate-documentation (pull_request) Failing after 6s
Checking for merge conflicts…
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin devin/phase1b-chain138-admin-rotation-1776543648:devin/phase1b-chain138-admin-rotation-1776543648
git checkout devin/phase1b-chain138-admin-rotation-1776543648
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: d-bis/smom-dbis-138#1