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
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>
318 lines
13 KiB
Solidity
318 lines
13 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import {Script, console} from "forge-std/Script.sol";
|
|
|
|
/**
|
|
* @title Rotate Chain 138 admin/owner roles away from compromised deployer EOA
|
|
* @notice Transfer-then-revoke rotation for:
|
|
* - CompliantUSDT (Ownable owner + AccessControl DEFAULT_ADMIN_ROLE)
|
|
* - CompliantUSDC (Ownable owner + AccessControl DEFAULT_ADMIN_ROLE)
|
|
* - DODOPMMIntegration (AccessControl DEFAULT_ADMIN_ROLE +
|
|
* POOL_MANAGER_ROLE + SWAP_OPERATOR_ROLE)
|
|
*
|
|
* The scripts here DO NOT broadcast by default. They simulate via
|
|
* `forge script` and print the calldata + expected state transitions.
|
|
* Broadcasting requires explicit `--broadcast` plus per-tx operator
|
|
* approval (see scripts/rotation/chain138-rotation-runbook.md).
|
|
*
|
|
* Two scripts are exposed:
|
|
* - RotateStage1 — signed by the OLD (compromised) key. Transfers
|
|
* Ownable ownership + grants AccessControl admin to NEW_ADMIN.
|
|
* Does NOT revoke old admin yet (revocation happens in stage 2
|
|
* once the new admin has confirmed it can act).
|
|
* - RotateStage2 — signed by the NEW key. Revokes DEFAULT_ADMIN_ROLE
|
|
* (and other roles) from the OLD deployer.
|
|
*
|
|
* @dev Canonical addresses come from docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md
|
|
* and ADDRESS_MATRIX_AND_STATUS.md — DO NOT change them here without
|
|
* updating both docs.
|
|
*/
|
|
|
|
interface IOwnable {
|
|
function owner() external view returns (address);
|
|
function transferOwnership(address newOwner) external;
|
|
}
|
|
|
|
interface IAccessControl {
|
|
function hasRole(bytes32 role, address account) external view returns (bool);
|
|
function grantRole(bytes32 role, address account) external;
|
|
function revokeRole(bytes32 role, address account) external;
|
|
function renounceRole(bytes32 role, address account) external;
|
|
function DEFAULT_ADMIN_ROLE() external view returns (bytes32);
|
|
}
|
|
|
|
abstract contract Chain138RotationBase is Script {
|
|
// ---- Canonical Chain 138 addresses (rules/chain138-tokens-and-pmm.mdc) ----
|
|
address internal constant DEFAULT_CUSDT =
|
|
0x93E66202A11B1772E55407B32B44e5Cd8eda7f22;
|
|
address internal constant DEFAULT_CUSDC =
|
|
0xf22258f57794CC8E06237084b353Ab30fFfa640b;
|
|
address internal constant DEFAULT_DODO_PMM_INTEGRATION =
|
|
0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d;
|
|
address internal constant DEFAULT_OLD_DEPLOYER =
|
|
0x4A666F96fC8764181194447A7dFdb7d471b301C8;
|
|
|
|
// Roles on DODOPMMIntegration (see contracts/dex/DODOPMMIntegration.sol)
|
|
bytes32 internal constant DEFAULT_ADMIN_ROLE = 0x00;
|
|
bytes32 internal constant POOL_MANAGER_ROLE =
|
|
keccak256("POOL_MANAGER_ROLE");
|
|
bytes32 internal constant SWAP_OPERATOR_ROLE =
|
|
keccak256("SWAP_OPERATOR_ROLE");
|
|
|
|
struct Targets {
|
|
address cUSDT;
|
|
address cUSDC;
|
|
address dodoPmmIntegration;
|
|
address oldDeployer;
|
|
address newAdmin;
|
|
}
|
|
|
|
function _loadTargets() internal view returns (Targets memory t) {
|
|
t.cUSDT = vm.envOr("CUSDT_ADDRESS", DEFAULT_CUSDT);
|
|
t.cUSDC = vm.envOr("CUSDC_ADDRESS", DEFAULT_CUSDC);
|
|
t.dodoPmmIntegration = vm.envOr(
|
|
"DODO_PMM_INTEGRATION_ADDRESS", DEFAULT_DODO_PMM_INTEGRATION
|
|
);
|
|
t.oldDeployer = vm.envOr("OLD_DEPLOYER_ADDRESS", DEFAULT_OLD_DEPLOYER);
|
|
t.newAdmin = vm.envAddress("NEW_ADMIN_ADDRESS");
|
|
require(t.newAdmin != address(0), "NEW_ADMIN_ADDRESS not set");
|
|
require(t.newAdmin != t.oldDeployer, "NEW_ADMIN must differ from OLD");
|
|
}
|
|
|
|
function _dumpTargets(Targets memory t, string memory label) internal pure {
|
|
console.log("== Chain 138 rotation targets (%s) ==", label);
|
|
console.log(" cUSDT :", t.cUSDT);
|
|
console.log(" cUSDC :", t.cUSDC);
|
|
console.log(" DODOPMMIntegration :", t.dodoPmmIntegration);
|
|
console.log(" OLD deployer :", t.oldDeployer);
|
|
console.log(" NEW admin :", t.newAdmin);
|
|
}
|
|
|
|
function _requireOwner(
|
|
address contractAddr,
|
|
address expectedOwner,
|
|
string memory label
|
|
) internal view {
|
|
address cur = IOwnable(contractAddr).owner();
|
|
require(
|
|
cur == expectedOwner,
|
|
string.concat(
|
|
label, ": current owner != expected (aborting rotation)"
|
|
)
|
|
);
|
|
}
|
|
|
|
function _requireHasRole(
|
|
address contractAddr,
|
|
bytes32 role,
|
|
address account,
|
|
string memory label
|
|
) internal view {
|
|
require(
|
|
IAccessControl(contractAddr).hasRole(role, account),
|
|
string.concat(label, ": account does not hold role (aborting)")
|
|
);
|
|
}
|
|
|
|
function _requireLacksRole(
|
|
address contractAddr,
|
|
bytes32 role,
|
|
address account,
|
|
string memory label
|
|
) internal view {
|
|
require(
|
|
!IAccessControl(contractAddr).hasRole(role, account),
|
|
string.concat(label, ": account unexpectedly holds role")
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @title RotateStage1 — signed by OLD deployer
|
|
* @notice Transfers Ownable + grants DEFAULT_ADMIN_ROLE to NEW_ADMIN.
|
|
* Does NOT revoke from OLD (that is Stage 2, signed by NEW).
|
|
*
|
|
* Required env:
|
|
* PRIVATE_KEY — private key of the OLD deployer (compromised,
|
|
* will be rotated off all other chains too)
|
|
* NEW_ADMIN_ADDRESS — new admin EOA or multisig
|
|
*
|
|
* Optional env (defaults to canonical Chain 138 addresses):
|
|
* CUSDT_ADDRESS
|
|
* CUSDC_ADDRESS
|
|
* DODO_PMM_INTEGRATION_ADDRESS
|
|
* OLD_DEPLOYER_ADDRESS
|
|
*
|
|
* Usage:
|
|
* # simulate (default; safe):
|
|
* forge script script/rotation/RotateChain138Admin.s.sol:RotateStage1 \
|
|
* --rpc-url "$RPC_URL_138" -vvv
|
|
* # broadcast (requires per-tx operator approval):
|
|
* forge script script/rotation/RotateChain138Admin.s.sol:RotateStage1 \
|
|
* --rpc-url "$RPC_URL_138" --broadcast --slow -vvv
|
|
*/
|
|
contract RotateStage1 is Chain138RotationBase {
|
|
function run() external {
|
|
Targets memory t = _loadTargets();
|
|
_dumpTargets(t, "stage 1 - OLD signs");
|
|
|
|
// Preflight — abort if on-chain state does not match pre-rotation.
|
|
_requireOwner(t.cUSDT, t.oldDeployer, "cUSDT");
|
|
_requireOwner(t.cUSDC, t.oldDeployer, "cUSDC");
|
|
_requireHasRole(t.cUSDT, DEFAULT_ADMIN_ROLE, t.oldDeployer, "cUSDT ADMIN");
|
|
_requireHasRole(t.cUSDC, DEFAULT_ADMIN_ROLE, t.oldDeployer, "cUSDC ADMIN");
|
|
_requireHasRole(
|
|
t.dodoPmmIntegration, DEFAULT_ADMIN_ROLE, t.oldDeployer, "DODO ADMIN"
|
|
);
|
|
_requireLacksRole(t.cUSDT, DEFAULT_ADMIN_ROLE, t.newAdmin, "cUSDT new-admin");
|
|
_requireLacksRole(t.cUSDC, DEFAULT_ADMIN_ROLE, t.newAdmin, "cUSDC new-admin");
|
|
_requireLacksRole(
|
|
t.dodoPmmIntegration, DEFAULT_ADMIN_ROLE, t.newAdmin, "DODO new-admin"
|
|
);
|
|
|
|
uint256 pk = vm.envUint("PRIVATE_KEY");
|
|
address signer = vm.addr(pk);
|
|
require(
|
|
signer == t.oldDeployer,
|
|
"PRIVATE_KEY does not match OLD_DEPLOYER_ADDRESS (stage 1 must be signed by OLD)"
|
|
);
|
|
|
|
vm.startBroadcast(pk);
|
|
|
|
// --- cUSDT ---
|
|
IOwnable(t.cUSDT).transferOwnership(t.newAdmin);
|
|
IAccessControl(t.cUSDT).grantRole(DEFAULT_ADMIN_ROLE, t.newAdmin);
|
|
console.log("cUSDT: ownership transferred + DEFAULT_ADMIN_ROLE granted to NEW");
|
|
|
|
// --- cUSDC ---
|
|
IOwnable(t.cUSDC).transferOwnership(t.newAdmin);
|
|
IAccessControl(t.cUSDC).grantRole(DEFAULT_ADMIN_ROLE, t.newAdmin);
|
|
console.log("cUSDC: ownership transferred + DEFAULT_ADMIN_ROLE granted to NEW");
|
|
|
|
// --- DODOPMMIntegration (AccessControl only — no Ownable) ---
|
|
IAccessControl(t.dodoPmmIntegration).grantRole(DEFAULT_ADMIN_ROLE, t.newAdmin);
|
|
IAccessControl(t.dodoPmmIntegration).grantRole(POOL_MANAGER_ROLE, t.newAdmin);
|
|
IAccessControl(t.dodoPmmIntegration).grantRole(SWAP_OPERATOR_ROLE, t.newAdmin);
|
|
console.log(
|
|
"DODOPMMIntegration: DEFAULT_ADMIN + POOL_MANAGER + SWAP_OPERATOR granted to NEW"
|
|
);
|
|
|
|
vm.stopBroadcast();
|
|
|
|
console.log("\n== Stage 1 complete ==");
|
|
console.log("Next: NEW admin runs RotateStage2 to revoke OLD.");
|
|
console.log("Verify with VerifyChain138RotationComplete (read-only).");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @title RotateStage2 — signed by NEW admin
|
|
* @notice Revokes DEFAULT_ADMIN_ROLE (and DODO pool/swap roles) from OLD
|
|
* deployer on cUSDT, cUSDC, DODOPMMIntegration. Must only run after
|
|
* Stage 1 has completed and the NEW admin has confirmed it can
|
|
* successfully call an admin-gated function (e.g. pause/unpause on a
|
|
* test-only token, or `setReserveSystem` on DODOPMMIntegration).
|
|
*
|
|
* Required env:
|
|
* PRIVATE_KEY — private key of the NEW admin
|
|
* NEW_ADMIN_ADDRESS — must match the key above; used for sanity
|
|
*
|
|
* Optional env: same as Stage 1.
|
|
*/
|
|
contract RotateStage2 is Chain138RotationBase {
|
|
function run() external {
|
|
Targets memory t = _loadTargets();
|
|
_dumpTargets(t, "stage 2 - NEW signs");
|
|
|
|
// Preflight — new admin must already hold the roles; old admin still has them.
|
|
_requireHasRole(t.cUSDT, DEFAULT_ADMIN_ROLE, t.newAdmin, "cUSDT new-admin");
|
|
_requireHasRole(t.cUSDC, DEFAULT_ADMIN_ROLE, t.newAdmin, "cUSDC new-admin");
|
|
_requireHasRole(
|
|
t.dodoPmmIntegration, DEFAULT_ADMIN_ROLE, t.newAdmin, "DODO new-admin"
|
|
);
|
|
_requireHasRole(t.cUSDT, DEFAULT_ADMIN_ROLE, t.oldDeployer, "cUSDT OLD");
|
|
_requireHasRole(t.cUSDC, DEFAULT_ADMIN_ROLE, t.oldDeployer, "cUSDC OLD");
|
|
_requireHasRole(
|
|
t.dodoPmmIntegration, DEFAULT_ADMIN_ROLE, t.oldDeployer, "DODO OLD"
|
|
);
|
|
require(
|
|
IOwnable(t.cUSDT).owner() == t.newAdmin, "cUSDT owner != NEW (stage 1 missed?)"
|
|
);
|
|
require(
|
|
IOwnable(t.cUSDC).owner() == t.newAdmin, "cUSDC owner != NEW (stage 1 missed?)"
|
|
);
|
|
|
|
uint256 pk = vm.envUint("PRIVATE_KEY");
|
|
address signer = vm.addr(pk);
|
|
require(
|
|
signer == t.newAdmin,
|
|
"PRIVATE_KEY does not match NEW_ADMIN_ADDRESS (stage 2 must be signed by NEW)"
|
|
);
|
|
|
|
vm.startBroadcast(pk);
|
|
|
|
// --- cUSDT ---
|
|
IAccessControl(t.cUSDT).revokeRole(DEFAULT_ADMIN_ROLE, t.oldDeployer);
|
|
console.log("cUSDT: DEFAULT_ADMIN_ROLE revoked from OLD");
|
|
|
|
// --- cUSDC ---
|
|
IAccessControl(t.cUSDC).revokeRole(DEFAULT_ADMIN_ROLE, t.oldDeployer);
|
|
console.log("cUSDC: DEFAULT_ADMIN_ROLE revoked from OLD");
|
|
|
|
// --- DODOPMMIntegration ---
|
|
IAccessControl(t.dodoPmmIntegration).revokeRole(SWAP_OPERATOR_ROLE, t.oldDeployer);
|
|
IAccessControl(t.dodoPmmIntegration).revokeRole(POOL_MANAGER_ROLE, t.oldDeployer);
|
|
IAccessControl(t.dodoPmmIntegration).revokeRole(DEFAULT_ADMIN_ROLE, t.oldDeployer);
|
|
console.log(
|
|
"DODOPMMIntegration: DEFAULT_ADMIN + POOL_MANAGER + SWAP_OPERATOR revoked from OLD"
|
|
);
|
|
|
|
vm.stopBroadcast();
|
|
|
|
console.log("\n== Stage 2 complete ==");
|
|
console.log("Run VerifyChain138RotationComplete to confirm end state.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @title VerifyChain138RotationComplete — read-only end-state check
|
|
* @notice Fails (reverts) if any contract still lists OLD as owner or admin.
|
|
* Safe to run without a funded signer; no broadcast.
|
|
*/
|
|
contract VerifyChain138RotationComplete is Chain138RotationBase {
|
|
function run() external view {
|
|
Targets memory t = _loadTargets();
|
|
_dumpTargets(t, "verification");
|
|
|
|
// Ownable
|
|
require(
|
|
IOwnable(t.cUSDT).owner() == t.newAdmin, "cUSDT owner != NEW"
|
|
);
|
|
require(
|
|
IOwnable(t.cUSDC).owner() == t.newAdmin, "cUSDC owner != NEW"
|
|
);
|
|
|
|
// AccessControl — NEW holds admin, OLD does not
|
|
_requireHasRole(t.cUSDT, DEFAULT_ADMIN_ROLE, t.newAdmin, "cUSDT new-admin");
|
|
_requireHasRole(t.cUSDC, DEFAULT_ADMIN_ROLE, t.newAdmin, "cUSDC new-admin");
|
|
_requireHasRole(
|
|
t.dodoPmmIntegration, DEFAULT_ADMIN_ROLE, t.newAdmin, "DODO new-admin"
|
|
);
|
|
|
|
_requireLacksRole(t.cUSDT, DEFAULT_ADMIN_ROLE, t.oldDeployer, "cUSDT OLD");
|
|
_requireLacksRole(t.cUSDC, DEFAULT_ADMIN_ROLE, t.oldDeployer, "cUSDC OLD");
|
|
_requireLacksRole(
|
|
t.dodoPmmIntegration, DEFAULT_ADMIN_ROLE, t.oldDeployer, "DODO OLD"
|
|
);
|
|
_requireLacksRole(
|
|
t.dodoPmmIntegration, POOL_MANAGER_ROLE, t.oldDeployer, "DODO POOL_MANAGER OLD"
|
|
);
|
|
_requireLacksRole(
|
|
t.dodoPmmIntegration, SWAP_OPERATOR_ROLE, t.oldDeployer, "DODO SWAP_OPERATOR OLD"
|
|
);
|
|
|
|
console.log("\nPASS: rotation complete. OLD deployer has no admin authority on target contracts.");
|
|
}
|
|
}
|