diff --git a/.env.example b/.env.example index 6220d3e0..51c8bf51 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,7 @@ WRAPPER_OWNER= # ── Deployment role overrides (all default to ADMIN if not set) ─ PLATFORM_TREASURY= +PAUSER= # PAUSER_ROLE holder on all contracts; use an automation bot address COVER_POOL_FACTORY_CREATOR= CLAIM_OPERATOR= SWAP_MANAGER= diff --git a/AGENTS.md b/AGENTS.md index e92b8bad..a3e0c3d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,7 +130,7 @@ All network deployments extend `Deploy.s.sol`. The base script deploys contracts - Catalysis Core addresses: `NATIVE_WRAPPER`, `ORACLE_PRICE_FEED`, `SLASHING_MANAGER`, `STAKE_MANAGER`, `REWARDS_MANAGER` **Optional role overrides** (default to deployer/admin): -- `ADMIN`, `PLATFORM_TREASURY`, `COVER_POOL_FACTORY_CREATOR`, `SWAP_MANAGER`, `PLATFORM_FEE_BPS` +- `ADMIN`, `PLATFORM_TREASURY`, `PAUSER`, `COVER_POOL_FACTORY_CREATOR`, `SWAP_MANAGER`, `PLATFORM_FEE_BPS` ## Commit & Pull Request Guidelines diff --git a/docs/deployment/Upgrade-PAUSER-ROLE-Mainnet.md b/docs/deployment/Upgrade-PAUSER-ROLE-Mainnet.md new file mode 100644 index 00000000..1496a115 --- /dev/null +++ b/docs/deployment/Upgrade-PAUSER-ROLE-Mainnet.md @@ -0,0 +1,361 @@ +# Mainnet Upgrade Runbook — PAUSER_ROLE + +## Overview + +This runbook upgrades the six Coverage UUPS proxies on Ethereum mainnet to introduce +`PAUSER_ROLE`, separating the pause capability from `DEFAULT_ADMIN_ROLE` so that an +automation bot can pause contracts instantly without going through the multisig. + +**Contracts upgraded:** PolicyManager · ClaimManager · PremiumManager · SpecRegistry · +Swapper · CoverPoolFactory + +**Storage safety:** `PAUSER_ROLE` is a `constant` and does not occupy any storage slot. +All six `StorageLayout` pinned baselines pass unchanged. No reinitializer is required. + +**Exception — ClaimManager:** The base `UpgradeClaimManager` script encodes `initializeV2` +into `upgradeCalldata`. `UpgradeClaimManagerEthereum` overrides this to return empty bytes +because `initializeV2` (`reinitializer(2)`) was already executed on mainnet and would revert +if called again. + +--- + +## Addresses + +| Contract | Proxy | +|----------|-------| +| PolicyManager | `0xfb771BE75365D2a1D32be198e03ce8a2125e2699` | +| ClaimManager | `0x3ceE181C3E78fB9968f0Fb0935d2db0723B9Cb45` | +| PremiumManager | `0xEc7322D6754709d5001B710ec4fB2547a89B3aD1` | +| CoverPoolFactory | `0x4f3DbB70cD85bcb63303FBa8610Cb163aDDA4E66` | +| SpecRegistry | `0x90EfF742958dd4c54ede3ED365375a14077D0A58` | +| Swapper | `0x148Bfe2330cEe4f227addb47736a105DB427dc31` | +| TimelockController | `0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7` | +| Catalysis Master Admin (multisig) | `0xd2d03377Fa96687e9C11380DA9956AcC5F307e2c` | + +**PAUSER_ROLE hash:** `0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a` + +Verify: +```bash +cast keccak "PAUSER_ROLE" +# 0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a +``` + +--- + +## Why the upgrade script alone is not sufficient + +The upgrade script (`UpgradeBase`) only produces a single-call `schedule` / `execute` +for `upgradeToAndCall(newImpl, "")`. After that call executes, `pause()` on every upgraded +proxy immediately requires `PAUSER_ROLE` — which no address holds yet, including the +TimelockController. + +The fix is to issue a single `scheduleBatch` that atomically upgrades each proxy **and** +grants `PAUSER_ROLE` to the designated pauser in the same timelock operation. There is then +no window where the contracts are upgraded but unpaused by anyone. + +--- + +## Prerequisites + +1. **Confirm timelock min delay** — the deploy script defaults to 2 days but verify on-chain: + ```bash + cast call 0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7 "getMinDelay()(uint256)" \ + --rpc-url $ETHEREUM_RPC_URL + ``` + +2. **Choose a pauser address** — an automation bot EOA or dedicated hot-wallet. Set as + `PAUSER` throughout this runbook. + +3. **Deployer EOA** — must have ETH for gas; does NOT need any on-chain role (the script + only deploys implementation contracts, not the proxy upgrade). + +4. **Verify existing implementations** (optional sanity check): + ```bash + cast storage 0xfb771BE75365D2a1D32be198e03ce8a2125e2699 \ + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc \ + --rpc-url $ETHEREUM_RPC_URL + ``` + +--- + +## Step 1 — Deploy the 6 new implementations + +Run each script with `TIMELOCK` set. The script **broadcasts only the implementation +deployment** from the deployer EOA and then prints single-call `schedule` / `execute` +calldata to the console. **Ignore those printed calldatas** — they do not include +`grantRole` and must not be submitted. You will build a combined `scheduleBatch` in Step 3. +Collect the six new implementation addresses from the script output. + +```bash +# Set common env +export ETHEREUM_RPC_URL=... +export TIMELOCK=0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7 +export DEPLOYER_ADDRESS= + +# PolicyManager +POLICY_MANAGER_PROXY=0xfb771BE75365D2a1D32be198e03ce8a2125e2699 \ + forge script script/UpgradePolicyManagerEthereum.s.sol \ + --rpc-url $ETHEREUM_RPC_URL --keystore $KEYSTORE --broadcast + +# ClaimManager (note: upgradeCalldata is empty — initializeV2 already applied) +CLAIM_MANAGER_PROXY=0x3ceE181C3E78fB9968f0Fb0935d2db0723B9Cb45 \ + forge script script/UpgradeClaimManagerEthereum.s.sol \ + --rpc-url $ETHEREUM_RPC_URL --keystore $KEYSTORE --broadcast + +# PremiumManager +PREMIUM_MANAGER_PROXY=0xEc7322D6754709d5001B710ec4fB2547a89B3aD1 \ + forge script script/UpgradePremiumManagerEthereum.s.sol \ + --rpc-url $ETHEREUM_RPC_URL --keystore $KEYSTORE --broadcast + +# CoverPoolFactory +COVER_POOL_FACTORY_PROXY=0x4f3DbB70cD85bcb63303FBa8610Cb163aDDA4E66 \ + forge script script/UpgradeCoverPoolFactoryEthereum.s.sol \ + --rpc-url $ETHEREUM_RPC_URL --keystore $KEYSTORE --broadcast + +# SpecRegistry +SPEC_REGISTRY_PROXY=0x90EfF742958dd4c54ede3ED365375a14077D0A58 \ + forge script script/UpgradeSpecRegistryEthereum.s.sol \ + --rpc-url $ETHEREUM_RPC_URL --keystore $KEYSTORE --broadcast + +# Swapper +SWAPPER_PROXY=0x148Bfe2330cEe4f227addb47736a105DB427dc31 \ + forge script script/UpgradeSwapperEthereum.s.sol \ + --rpc-url $ETHEREUM_RPC_URL --keystore $KEYSTORE --broadcast +``` + +Record the six new implementation addresses from the script output: + +| Contract | New Implementation | +|----------|--------------------| +| PolicyManager | `0x...` | +| ClaimManager | `0x...` | +| PremiumManager | `0x...` | +| CoverPoolFactory | `0x...` | +| SpecRegistry | `0x...` | +| Swapper | `0x...` | + +--- + +## Step 2 — Encode the 12 calldata payloads + +For each proxy, two calls are needed: `upgradeToAndCall` and `grantRole`. Replace +`` with the addresses recorded in Step 1 and `` with the chosen pauser. + +```bash +export PAUSER= +export PAUSER_ROLE=0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a + +# --- PolicyManager --- +PM_UPGRADE=$(cast calldata "upgradeToAndCall(address,bytes)" "0x") +PM_GRANT=$(cast calldata "grantRole(bytes32,address)" $PAUSER_ROLE $PAUSER) + +# --- ClaimManager --- +CM_UPGRADE=$(cast calldata "upgradeToAndCall(address,bytes)" "0x") +CM_GRANT=$(cast calldata "grantRole(bytes32,address)" $PAUSER_ROLE $PAUSER) + +# --- PremiumManager --- +PREMM_UPGRADE=$(cast calldata "upgradeToAndCall(address,bytes)" "0x") +PREMM_GRANT=$(cast calldata "grantRole(bytes32,address)" $PAUSER_ROLE $PAUSER) + +# --- CoverPoolFactory --- +CPF_UPGRADE=$(cast calldata "upgradeToAndCall(address,bytes)" "0x") +CPF_GRANT=$(cast calldata "grantRole(bytes32,address)" $PAUSER_ROLE $PAUSER) + +# --- SpecRegistry --- +SR_UPGRADE=$(cast calldata "upgradeToAndCall(address,bytes)" "0x") +SR_GRANT=$(cast calldata "grantRole(bytes32,address)" $PAUSER_ROLE $PAUSER) + +# --- Swapper --- +SW_UPGRADE=$(cast calldata "upgradeToAndCall(address,bytes)" "0x") +SW_GRANT=$(cast calldata "grantRole(bytes32,address)" $PAUSER_ROLE $PAUSER) +``` + +All `grantRole` payloads are identical across proxies (same role, same recipient) and look +like: +``` +0x2f2ff15d +65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a +000000000000000000000000 +``` + +--- + +## Step 3 — Build and submit `scheduleBatch` via Safe TX Builder + +In Gnosis Safe TX Builder, create a transaction to the TimelockController +(`0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7`) calling `scheduleBatch`. + +**Targets (12 entries, in order):** + +``` +0xfb771BE75365D2a1D32be198e03ce8a2125e2699 ← PolicyManager (upgrade) +0xfb771BE75365D2a1D32be198e03ce8a2125e2699 ← PolicyManager (grantRole) +0x3ceE181C3E78fB9968f0Fb0935d2db0723B9Cb45 ← ClaimManager (upgrade) +0x3ceE181C3E78fB9968f0Fb0935d2db0723B9Cb45 ← ClaimManager (grantRole) +0xEc7322D6754709d5001B710ec4fB2547a89B3aD1 ← PremiumManager (upgrade) +0xEc7322D6754709d5001B710ec4fB2547a89B3aD1 ← PremiumManager (grantRole) +0x4f3DbB70cD85bcb63303FBa8610Cb163aDDA4E66 ← CoverPoolFactory (upgrade) +0x4f3DbB70cD85bcb63303FBa8610Cb163aDDA4E66 ← CoverPoolFactory (grantRole) +0x90EfF742958dd4c54ede3ED365375a14077D0A58 ← SpecRegistry (upgrade) +0x90EfF742958dd4c54ede3ED365375a14077D0A58 ← SpecRegistry (grantRole) +0x148Bfe2330cEe4f227addb47736a105DB427dc31 ← Swapper (upgrade) +0x148Bfe2330cEe4f227addb47736a105DB427dc31 ← Swapper (grantRole) +``` + +**Values:** all `0` + +**Payloads (matching order):** + +``` +$PM_UPGRADE $PM_GRANT +$CM_UPGRADE $CM_GRANT +$PREMM_UPGRADE $PREMM_GRANT +$CPF_UPGRADE $CPF_GRANT +$SR_UPGRADE $SR_GRANT +$SW_UPGRADE $SW_GRANT +``` + +**Predecessor:** `0x0000000000000000000000000000000000000000000000000000000000000000` + +**Salt:** `0x0000000000000000000000000000000000000000000000000000000000000000` + +**Delay:** output of `cast call ... "getMinDelay()(uint256)"` from Prerequisites step 1 + +Collect 5-of-7 signatures and submit. + +Note the **operation ID** from the emitted `CallScheduled` events for use in Step 5. It can +also be computed locally: +```bash +cast keccak $(cast abi-encode \ + "(address[],uint256[],bytes[],bytes32,bytes32)" \ + "[]" "[0,0,0,0,0,0,0,0,0,0,0,0]" "[]" \ + 0x0000000000000000000000000000000000000000000000000000000000000000 \ + 0x0000000000000000000000000000000000000000000000000000000000000000) +``` + +--- + +## Step 4 — Wait for the timelock delay + +No action required. Track the earliest execution time: + +```bash +cast call 0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7 \ + "getTimestamp(bytes32)(uint256)" \ + --rpc-url $ETHEREUM_RPC_URL +``` + +--- + +## Step 5 — Execute the batch + +After the delay has passed, submit a second Safe TX Builder transaction to the +TimelockController calling `executeBatch` with **identical** targets, values, payloads, +predecessor, and salt (no delay argument). + +Verify it has not already been executed: + +```bash +cast call 0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7 \ + "isOperationDone(bytes32)(bool)" \ + --rpc-url $ETHEREUM_RPC_URL +# must return false before submitting +``` + +--- + +## Step 6 — Verify post-upgrade state + +Run all checks against mainnet: + +```bash +# 1. Confirm new implementations are live +cast storage 0xfb771BE75365D2a1D32be198e03ce8a2125e2699 \ + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc \ + --rpc-url $ETHEREUM_RPC_URL +# Repeat for each proxy + +# 2. Confirm PAUSER_ROLE is granted to on all six proxies +for PROXY in \ + 0xfb771BE75365D2a1D32be198e03ce8a2125e2699 \ + 0x3ceE181C3E78fB9968f0Fb0935d2db0723B9Cb45 \ + 0xEc7322D6754709d5001B710ec4fB2547a89B3aD1 \ + 0x4f3DbB70cD85bcb63303FBa8610Cb163aDDA4E66 \ + 0x90EfF742958dd4c54ede3ED365375a14077D0A58 \ + 0x148Bfe2330cEe4f227addb47736a105DB427dc31; do + cast call $PROXY \ + "hasRole(bytes32,address)(bool)" \ + 0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a \ + $PAUSER \ + --rpc-url $ETHEREUM_RPC_URL +done +# All must return true + +# 3. Confirm DEFAULT_ADMIN_ROLE is still on the TimelockController (not disturbed) +cast call 0xfb771BE75365D2a1D32be198e03ce8a2125e2699 \ + "hasRole(bytes32,address)(bool)" \ + 0x0000000000000000000000000000000000000000000000000000000000000000 \ + 0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7 \ + --rpc-url $ETHEREUM_RPC_URL +# Must return true + +# 4. Confirm pauser can pause (simulate — do not broadcast on mainnet without approval) +cast call 0xfb771BE75365D2a1D32be198e03ce8a2125e2699 \ + "pause()" --from $PAUSER --rpc-url $ETHEREUM_RPC_URL +# Must not revert + +# 5. Confirm admin alone cannot pause (expected revert) +cast call 0xfb771BE75365D2a1D32be198e03ce8a2125e2699 \ + "pause()" --from 0xd2d03377Fa96687e9C11380DA9956AcC5F307e2c \ + --rpc-url $ETHEREUM_RPC_URL +# Must revert with AccessControlUnauthorizedAccount +``` + +--- + +## Operational notes post-upgrade + +### Pausing contracts + +Only the `PAUSER_ROLE` holder can call `pause()` or `unpause()`. The TimelockController +does NOT hold `PAUSER_ROLE` after this upgrade. This is intentional: the purpose of the +role is to enable **instant** pausing by an automation bot without any timelock delay. + +If governance ever needs to pause via timelock (e.g. because the bot is compromised), the +path is: (1) schedule `grantRole(PAUSER_ROLE, timelockAddress)` → wait min-delay → execute; +(2) schedule `pause()` → wait min-delay → execute. This takes 2× the timelock delay and is +not suited for emergencies — always keep a trusted PAUSER_ROLE holder active. + +### Swapper.setNativeWrapper + +`setNativeWrapper` requires `whenPaused` in addition to `DEFAULT_ADMIN_ROLE`. After this +upgrade, the workflow to call it is: + +1. `PAUSER` calls `swapper.pause()` +2. `ADMIN` (via timelock) calls `swapper.setNativeWrapper(newWrapper)` +3. `PAUSER` calls `swapper.unpause()` + +### Granting PAUSER_ROLE to additional addresses + +The TimelockController holds `DEFAULT_ADMIN_ROLE` which is the role-admin for `PAUSER_ROLE`. +To grant the role to a new address, schedule a `grantRole(PAUSER_ROLE, newAddress)` call +on the desired proxy through the normal Safe TX Builder → TimelockController flow. + +--- + +## Checklist + +- [ ] Timelock min delay confirmed on-chain +- [ ] Pauser address decided and documented +- [ ] 6 new implementation contracts deployed (addresses recorded above) +- [ ] 12 calldata payloads encoded and verified +- [ ] `scheduleBatch` submitted and signed by 5-of-7 +- [ ] Operation ID recorded +- [ ] Delay elapsed +- [ ] `executeBatch` submitted and signed by 5-of-7 +- [ ] New implementations verified on-chain for all 6 proxies +- [ ] `hasRole(PAUSER_ROLE, pauser)` returns `true` on all 6 proxies +- [ ] `hasRole(DEFAULT_ADMIN_ROLE, timelock)` still returns `true` on all 6 proxies +- [ ] `pause()` simulation from pauser succeeds on all 6 proxies +- [ ] `pause()` simulation from admin alone reverts on all 6 proxies +- [ ] `docs/deployment/Ethereum-Mainnet.md` updated with new implementation addresses diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 22cc683c..dd85d28d 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -47,6 +47,7 @@ import {CreateXDeployer} from "./CreateXDeployer.s.sol"; * - ORACLE_PRICE_FEED: OraclePriceFeed address (required) * - PLATFORM_TREASURY: Platform fee recipient (defaults to admin) * - PLATFORM_FEE_BPS: Platform fee in basis points (defaults to 500 = 5%) + * - PAUSER: PAUSER_ROLE holder on all contracts — use an automation bot address (defaults to admin) * - COVER_POOL_FACTORY_CREATOR: Factory creator role (defaults to admin) * - SWAP_MANAGER: Swap route configuration manager (defaults to admin) * - TIMELOCK_MIN_DELAY: Minimum delay in seconds for TimelockController (defaults to 2 days) @@ -85,6 +86,7 @@ abstract contract Deploy is CreateXDeployer { * @notice Role configuration loaded from environment variables. * @param deployer Public address of the keystore signer, used for broadcasting. * @param admin DEFAULT_ADMIN_ROLE holder for all deployed contracts. + * @param pauser PAUSER_ROLE holder on all deployed contracts (e.g. an automation bot). * @param coverPoolFactoryCreator CREATOR_ROLE holder on CoverPoolFactory. * @param swapManager SWAP_MANAGER_ROLE holder on Swapper. * @param platformTreasury Recipient address for platform fees. @@ -94,6 +96,7 @@ abstract contract Deploy is CreateXDeployer { struct RoleConfig { address deployer; address admin; + address pauser; address coverPoolFactoryCreator; address swapManager; address platformTreasury; @@ -464,7 +467,7 @@ abstract contract Deploy is CreateXDeployer { * @param roles RoleConfig containing deployer address for temporary role revocation. */ function _configureRoles(DeployedContracts memory contracts, RoleConfig memory roles) private { - _grantRoles(contracts); + _grantRoles(contracts, roles); _revokeTemporaryRoles(contracts, roles); } @@ -472,6 +475,7 @@ abstract contract Deploy is CreateXDeployer { * @notice Grants permanent roles to deployed contracts enabling cross-contract interactions. * @dev Role assignments: * - All contracts: DEFAULT_ADMIN_ROLE → TimelockController (governance layer) + * - All contracts: PAUSER_ROLE → roles.pauser (automation bot or admin; see PAUSER env var) * - Swapper: SWAP_EXECUTOR_ROLE → ClaimManager (for collateral conversion) * - PremiumManager: CLAIM_MANAGER_ROLE → ClaimManager (for surplus distribution) * @@ -482,12 +486,12 @@ abstract contract Deploy is CreateXDeployer { * NOTE: distributePremium() on PremiumManager is permissionless but requires the token * to be in the approved set. Initial tokens are added in _deployPremiumContracts(). * @param contracts DeployedContracts containing all proxy addresses. + * @param roles RoleConfig containing pauser and other role addresses. */ - function _grantRoles(DeployedContracts memory contracts) private { + function _grantRoles(DeployedContracts memory contracts, RoleConfig memory roles) private { bytes32 defaultAdminRole = bytes32(0); address tc = contracts.timelockController; - // Grant DEFAULT_ADMIN_ROLE to TimelockController on all contracts IAccessControl(contracts.swapperProxy).grantRole(defaultAdminRole, tc); IAccessControl(contracts.premiumManagerProxy).grantRole(defaultAdminRole, tc); IAccessControl(contracts.claimManagerProxy).grantRole(defaultAdminRole, tc); @@ -495,7 +499,14 @@ abstract contract Deploy is CreateXDeployer { IAccessControl(contracts.coverPoolFactoryProxy).grantRole(defaultAdminRole, tc); IAccessControl(contracts.specRegistryProxy).grantRole(defaultAdminRole, tc); - // Operational roles — direct contract-to-contract, not through timelock + bytes32 pauserRole = Swapper(payable(contracts.swapperProxy)).PAUSER_ROLE(); + IAccessControl(contracts.swapperProxy).grantRole(pauserRole, roles.pauser); + IAccessControl(contracts.premiumManagerProxy).grantRole(pauserRole, roles.pauser); + IAccessControl(contracts.claimManagerProxy).grantRole(pauserRole, roles.pauser); + IAccessControl(contracts.policyManagerProxy).grantRole(pauserRole, roles.pauser); + IAccessControl(contracts.coverPoolFactoryProxy).grantRole(pauserRole, roles.pauser); + IAccessControl(contracts.specRegistryProxy).grantRole(pauserRole, roles.pauser); + IAccessControl(contracts.swapperProxy) .grantRole(Swapper(payable(contracts.swapperProxy)).SWAP_EXECUTOR_ROLE(), contracts.claimManagerProxy); @@ -509,6 +520,11 @@ abstract contract Deploy is CreateXDeployer { * and the admin (multisig) must have their direct roles revoked. The deployer's * SWAP_EXECUTOR_ROLE placeholder is also revoked (ClaimManager now holds it). * DEFAULT_ADMIN_ROLE is revoked last to ensure all other revocations succeed. + * + * PAUSER_ROLE is intentionally NOT revoked from deployer here. When PAUSER env var is set + * to a dedicated address, the deployer never received PAUSER_ROLE so there is nothing to + * revoke. When PAUSER defaults to the deployer (local/test only), retaining the role is + * intentional for convenience. * @param contracts DeployedContracts containing all proxy addresses. * @param roles RoleConfig containing deployer and admin addresses. */ @@ -554,6 +570,7 @@ abstract contract Deploy is CreateXDeployer { console2.log("--- Accounts ---"); console2.log("Deployer:", roles.deployer); console2.log("Admin:", roles.admin); + console2.log("Pauser:", roles.pauser); console2.log("Platform Treasury:", roles.platformTreasury); console2.log(""); @@ -601,6 +618,7 @@ abstract contract Deploy is CreateXDeployer { function _loadRoleConfig(address deployer) private view returns (RoleConfig memory roles) { roles.deployer = deployer; roles.admin = vm.envOr("ADMIN", roles.deployer); + roles.pauser = vm.envOr("PAUSER", roles.admin); roles.coverPoolFactoryCreator = vm.envOr("COVER_POOL_FACTORY_CREATOR", roles.admin); roles.swapManager = vm.envOr("SWAP_MANAGER", roles.admin); roles.platformTreasury = vm.envOr("PLATFORM_TREASURY", roles.admin); diff --git a/script/UpgradeClaimManager.s.sol b/script/UpgradeClaimManager.s.sol index 28b6964f..9b65773b 100644 --- a/script/UpgradeClaimManager.s.sol +++ b/script/UpgradeClaimManager.s.sol @@ -15,6 +15,11 @@ import {UpgradeBase} from "./UpgradeBase.s.sol"; * WARNING: Storage layout checks are skipped. Manually verify layout before * upgrading to avoid corrupting proxy state. * + * POST-UPGRADE MIGRATION: This version introduces PAUSER_ROLE. After the upgrade, + * the TimelockController must call grantRole(PAUSER_ROLE, ) on the proxy. + * Bundle this grantRole call in the same Safe TX Builder batch as the upgrade to + * avoid a window where no one can pause the contract. + * * Environment Variables: * - PRIVATE_KEY: Deployer private key with DEFAULT_ADMIN_ROLE on ClaimManager * - CLAIM_MANAGER_PROXY: Address of the deployed ClaimManager proxy (required) @@ -47,7 +52,7 @@ abstract contract UpgradeClaimManager is UpgradeBase { * @dev Encoded as the data argument to upgradeToAndCall so the proxy atomically configures * the V2 storage fields in the same transaction as the implementation swap. */ - function upgradeCalldata() internal view override returns (bytes memory) { + function upgradeCalldata() internal view virtual override returns (bytes memory) { address oraclePriceFeed = vm.envAddress("ORACLE_PRICE_FEED"); require(oraclePriceFeed != address(0), "ORACLE_PRICE_FEED required"); uint16 toleranceBps = SafeCast.toUint16(vm.envOr("PRICE_DEVIATION_TOLERANCE_BPS", uint256(300))); diff --git a/script/UpgradeClaimManagerEthereum.s.sol b/script/UpgradeClaimManagerEthereum.s.sol new file mode 100644 index 00000000..908b9998 --- /dev/null +++ b/script/UpgradeClaimManagerEthereum.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {UpgradeClaimManager} from "./UpgradeClaimManager.s.sol"; + +/** + * @title UpgradeClaimManagerEthereum + * @notice Upgrades ClaimManager contract on Ethereum mainnet. + * @dev Set TIMELOCK to the TimelockController address so the script deploys the new + * implementation and prints schedule/execute calldata for Safe TX Builder rather + * than broadcasting the upgrade directly. + * + * IMPORTANT: upgradeCalldata() is intentionally overridden to return empty bytes. + * initializeV2 was already executed on the mainnet proxy; calling it again would + * revert because reinitializer(2) enforces single execution. + * + * POST-UPGRADE MIGRATION: Bundle grantRole(PAUSER_ROLE, ) in the same + * scheduleBatch as the upgrade. See docs/deployment/Upgrade-PAUSER-ROLE-Mainnet.md. + * + * Environment Variables: + * - DEPLOYER_ADDRESS: Public address of the keystore signer + * - CLAIM_MANAGER_PROXY: Address of the deployed ClaimManager proxy (required) + * - TIMELOCK: TimelockController address (0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7) + */ +contract UpgradeClaimManagerEthereum is UpgradeClaimManager { + /** + * @inheritdoc UpgradeClaimManager + * @return UpgradeConfig for Ethereum mainnet (chain id 1). + */ + function getUpgradeConfig() internal pure override returns (UpgradeConfig memory) { + return UpgradeConfig({networkName: "Ethereum", chainId: 1}); + } + + /** + * @notice Returns empty calldata — initializeV2 is already applied on mainnet. + * @dev Overrides the base implementation which calls initializeV2; re-executing + * a reinitializer would revert. This upgrade only swaps the implementation. + */ + function upgradeCalldata() internal pure override returns (bytes memory) { + return ""; + } +} diff --git a/script/UpgradeCoverPoolFactory.s.sol b/script/UpgradeCoverPoolFactory.s.sol index 7532f20b..92c6bc0d 100644 --- a/script/UpgradeCoverPoolFactory.s.sol +++ b/script/UpgradeCoverPoolFactory.s.sol @@ -11,6 +11,11 @@ import {UpgradeBase} from "./UpgradeBase.s.sol"; * WARNING: Storage layout checks are skipped. Manually verify layout before * upgrading to avoid corrupting proxy state. * + * POST-UPGRADE MIGRATION: This version introduces PAUSER_ROLE. After the upgrade, + * the TimelockController must call grantRole(PAUSER_ROLE, ) on the proxy. + * Bundle this grantRole call in the same Safe TX Builder batch as the upgrade to + * avoid a window where no one can pause the contract. + * * Environment Variables: * - PRIVATE_KEY: Deployer private key with DEFAULT_ADMIN_ROLE on CoverPoolFactory * - COVER_POOL_FACTORY_PROXY: Address of the deployed CoverPoolFactory proxy (required) diff --git a/script/UpgradePolicyManager.s.sol b/script/UpgradePolicyManager.s.sol index 7e803078..65f2ac09 100644 --- a/script/UpgradePolicyManager.s.sol +++ b/script/UpgradePolicyManager.s.sol @@ -11,6 +11,11 @@ import {UpgradeBase} from "./UpgradeBase.s.sol"; * WARNING: Storage layout checks are skipped. Manually verify layout before * upgrading to avoid corrupting proxy state. * + * POST-UPGRADE MIGRATION: This version introduces PAUSER_ROLE. After the upgrade, + * the TimelockController must call grantRole(PAUSER_ROLE, ) on the proxy. + * Bundle this grantRole call in the same Safe TX Builder batch as the upgrade to + * avoid a window where no one can pause the contract. + * * Environment Variables: * - PRIVATE_KEY: Deployer private key with DEFAULT_ADMIN_ROLE on PolicyManager * - POLICY_MANAGER_PROXY: Address of the deployed PolicyManager proxy (required) diff --git a/script/UpgradePolicyManagerEthereum.s.sol b/script/UpgradePolicyManagerEthereum.s.sol new file mode 100644 index 00000000..4bf1f403 --- /dev/null +++ b/script/UpgradePolicyManagerEthereum.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {UpgradePolicyManager} from "./UpgradePolicyManager.s.sol"; + +/** + * @title UpgradePolicyManagerEthereum + * @notice Upgrades PolicyManager contract on Ethereum mainnet. + * @dev Set TIMELOCK to the TimelockController address so the script deploys the new + * implementation and prints schedule/execute calldata for Safe TX Builder rather + * than broadcasting the upgrade directly. + * + * POST-UPGRADE MIGRATION: Bundle grantRole(PAUSER_ROLE, ) in the same + * scheduleBatch as the upgrade. See docs/deployment/Upgrade-PAUSER-ROLE-Mainnet.md. + * + * Environment Variables: + * - DEPLOYER_ADDRESS: Public address of the keystore signer + * - POLICY_MANAGER_PROXY: Address of the deployed PolicyManager proxy (required) + * - TIMELOCK: TimelockController address (0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7) + */ +contract UpgradePolicyManagerEthereum is UpgradePolicyManager { + /** + * @inheritdoc UpgradePolicyManager + * @return UpgradeConfig for Ethereum mainnet (chain id 1). + */ + function getUpgradeConfig() internal pure override returns (UpgradeConfig memory) { + return UpgradeConfig({networkName: "Ethereum", chainId: 1}); + } +} diff --git a/script/UpgradePremiumManager.s.sol b/script/UpgradePremiumManager.s.sol index 30ed488b..d0064f9c 100644 --- a/script/UpgradePremiumManager.s.sol +++ b/script/UpgradePremiumManager.s.sol @@ -11,6 +11,11 @@ import {UpgradeBase} from "./UpgradeBase.s.sol"; * WARNING: Storage layout checks are skipped. Manually verify layout before * upgrading to avoid corrupting proxy state. * + * POST-UPGRADE MIGRATION: This version introduces PAUSER_ROLE. After the upgrade, + * the TimelockController must call grantRole(PAUSER_ROLE, ) on the proxy. + * Bundle this grantRole call in the same Safe TX Builder batch as the upgrade to + * avoid a window where no one can pause the contract. + * * Environment Variables: * - PRIVATE_KEY: Deployer private key with DEFAULT_ADMIN_ROLE on PremiumManager * - PREMIUM_MANAGER_PROXY: Address of the deployed PremiumManager proxy (required) diff --git a/script/UpgradePremiumManagerEthereum.s.sol b/script/UpgradePremiumManagerEthereum.s.sol new file mode 100644 index 00000000..13eaab46 --- /dev/null +++ b/script/UpgradePremiumManagerEthereum.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {UpgradePremiumManager} from "./UpgradePremiumManager.s.sol"; + +/** + * @title UpgradePremiumManagerEthereum + * @notice Upgrades PremiumManager contract on Ethereum mainnet. + * @dev Set TIMELOCK to the TimelockController address so the script deploys the new + * implementation and prints schedule/execute calldata for Safe TX Builder rather + * than broadcasting the upgrade directly. + * + * POST-UPGRADE MIGRATION: Bundle grantRole(PAUSER_ROLE, ) in the same + * scheduleBatch as the upgrade. See docs/deployment/Upgrade-PAUSER-ROLE-Mainnet.md. + * + * Environment Variables: + * - DEPLOYER_ADDRESS: Public address of the keystore signer + * - PREMIUM_MANAGER_PROXY: Address of the deployed PremiumManager proxy (required) + * - TIMELOCK: TimelockController address (0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7) + */ +contract UpgradePremiumManagerEthereum is UpgradePremiumManager { + /** + * @inheritdoc UpgradePremiumManager + * @return UpgradeConfig for Ethereum mainnet (chain id 1). + */ + function getUpgradeConfig() internal pure override returns (UpgradeConfig memory) { + return UpgradeConfig({networkName: "Ethereum", chainId: 1}); + } +} diff --git a/script/UpgradeSpecRegistry.s.sol b/script/UpgradeSpecRegistry.s.sol index b386d413..b55bfe5b 100644 --- a/script/UpgradeSpecRegistry.s.sol +++ b/script/UpgradeSpecRegistry.s.sol @@ -12,6 +12,11 @@ import {UpgradeBase} from "./UpgradeBase.s.sol"; * upgrading to avoid corrupting proxy state. The new `_approvedSpecs` mapping * is appended after `_specs` — confirm via `out/SpecRegistry.storageLayout.json`. * + * POST-UPGRADE MIGRATION: This version introduces PAUSER_ROLE. After the upgrade, + * the TimelockController must call grantRole(PAUSER_ROLE, ) on the proxy. + * Bundle this grantRole call in the same Safe TX Builder batch as the upgrade to + * avoid a window where no one can pause the contract. + * * After upgrading, the admin must call `approveSpec(specAddress)` for each trusted * ISpec implementation before curators can register them in new policy bindings. * diff --git a/script/UpgradeSpecRegistryEthereum.s.sol b/script/UpgradeSpecRegistryEthereum.s.sol new file mode 100644 index 00000000..902f82a3 --- /dev/null +++ b/script/UpgradeSpecRegistryEthereum.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {UpgradeSpecRegistry} from "./UpgradeSpecRegistry.s.sol"; + +/** + * @title UpgradeSpecRegistryEthereum + * @notice Upgrades SpecRegistry contract on Ethereum mainnet. + * @dev Set TIMELOCK to the TimelockController address so the script deploys the new + * implementation and prints schedule/execute calldata for Safe TX Builder rather + * than broadcasting the upgrade directly. + * + * POST-UPGRADE MIGRATION: Bundle grantRole(PAUSER_ROLE, ) in the same + * scheduleBatch as the upgrade. See docs/deployment/Upgrade-PAUSER-ROLE-Mainnet.md. + * + * Environment Variables: + * - DEPLOYER_ADDRESS: Public address of the keystore signer + * - SPEC_REGISTRY_PROXY: Address of the deployed SpecRegistry proxy (required) + * - TIMELOCK: TimelockController address (0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7) + */ +contract UpgradeSpecRegistryEthereum is UpgradeSpecRegistry { + /** + * @inheritdoc UpgradeSpecRegistry + * @return UpgradeConfig for Ethereum mainnet (chain id 1). + */ + function getUpgradeConfig() internal pure override returns (UpgradeConfig memory) { + return UpgradeConfig({networkName: "Ethereum", chainId: 1}); + } +} diff --git a/script/UpgradeSwapper.s.sol b/script/UpgradeSwapper.s.sol index be4e90b8..0983dd43 100644 --- a/script/UpgradeSwapper.s.sol +++ b/script/UpgradeSwapper.s.sol @@ -11,6 +11,13 @@ import {UpgradeBase} from "./UpgradeBase.s.sol"; * WARNING: Storage layout checks are skipped. Manually verify layout before * upgrading to avoid corrupting proxy state. * + * POST-UPGRADE MIGRATION: This version introduces PAUSER_ROLE. After the upgrade, + * the TimelockController must call grantRole(PAUSER_ROLE, ) on the proxy. + * Bundle this grantRole call in the same Safe TX Builder batch as the upgrade to + * avoid a window where no one can pause the contract. + * Note: setNativeWrapper() requires whenPaused — the PAUSER_ROLE holder must pause + * before admin can call it; and unpause afterwards. + * * Environment Variables: * - PRIVATE_KEY: Deployer private key with DEFAULT_ADMIN_ROLE on Swapper * - SWAPPER_PROXY: Address of the deployed Swapper proxy (required) diff --git a/script/UpgradeSwapperEthereum.s.sol b/script/UpgradeSwapperEthereum.s.sol new file mode 100644 index 00000000..ca698f5b --- /dev/null +++ b/script/UpgradeSwapperEthereum.s.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {UpgradeSwapper} from "./UpgradeSwapper.s.sol"; + +/** + * @title UpgradeSwapperEthereum + * @notice Upgrades Swapper contract on Ethereum mainnet. + * @dev Set TIMELOCK to the TimelockController address so the script deploys the new + * implementation and prints schedule/execute calldata for Safe TX Builder rather + * than broadcasting the upgrade directly. + * + * POST-UPGRADE MIGRATION: Bundle grantRole(PAUSER_ROLE, ) in the same + * scheduleBatch as the upgrade. See docs/deployment/Upgrade-PAUSER-ROLE-Mainnet.md. + * + * NOTE: After this upgrade setNativeWrapper() requires the contract to be paused + * first. The PAUSER_ROLE holder must pause before admin calls setNativeWrapper, + * then unpause afterwards. + * + * Environment Variables: + * - DEPLOYER_ADDRESS: Public address of the keystore signer + * - SWAPPER_PROXY: Address of the deployed Swapper proxy (required) + * - TIMELOCK: TimelockController address (0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7) + */ +contract UpgradeSwapperEthereum is UpgradeSwapper { + /** + * @inheritdoc UpgradeSwapper + * @return UpgradeConfig for Ethereum mainnet (chain id 1). + */ + function getUpgradeConfig() internal pure override returns (UpgradeConfig memory) { + return UpgradeConfig({networkName: "Ethereum", chainId: 1}); + } +} diff --git a/src/ClaimManager.sol b/src/ClaimManager.sol index 41278ee0..3e2f53e2 100644 --- a/src/ClaimManager.sol +++ b/src/ClaimManager.sol @@ -44,6 +44,8 @@ contract ClaimManager is using Address for address payable; using SafeERC20 for IERC20; + bytes32 public constant override PAUSER_ROLE = keccak256("PAUSER_ROLE"); + /// @dev BPS denominator used in slippage inflation formulas. Must not change. uint16 private constant _MAX_SLIPPAGE_BPS = 10_000; /// @dev Upper bound for the admin-settable slippage parameter. @@ -134,14 +136,14 @@ contract ClaimManager is /** * @inheritdoc IClaimManager */ - function pause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function pause() external override onlyRole(PAUSER_ROLE) { _pause(); } /** * @inheritdoc IClaimManager */ - function unpause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function unpause() external override onlyRole(PAUSER_ROLE) { _unpause(); } diff --git a/src/CoverPoolFactory.sol b/src/CoverPoolFactory.sol index 2f8d91fa..27ab2917 100644 --- a/src/CoverPoolFactory.sol +++ b/src/CoverPoolFactory.sol @@ -26,6 +26,7 @@ contract CoverPoolFactory is using EnumerableSet for EnumerableSet.AddressSet; bytes32 public constant override CREATOR_ROLE = keccak256("CREATOR_ROLE"); + bytes32 public constant override PAUSER_ROLE = keccak256("PAUSER_ROLE"); address public override coverPoolImplementation; address public override policyManager; @@ -75,14 +76,14 @@ contract CoverPoolFactory is /** * @inheritdoc ICoverPoolFactory */ - function pause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function pause() external override onlyRole(PAUSER_ROLE) { _pause(); } /** * @inheritdoc ICoverPoolFactory */ - function unpause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function unpause() external override onlyRole(PAUSER_ROLE) { _unpause(); } diff --git a/src/PolicyManager.sol b/src/PolicyManager.sol index e7448a98..6d0e0335 100644 --- a/src/PolicyManager.sol +++ b/src/PolicyManager.sol @@ -31,6 +31,8 @@ contract PolicyManager is ReentrancyGuard, IPolicyManager { + bytes32 public constant override PAUSER_ROLE = keccak256("PAUSER_ROLE"); + address public override stakeManager; address public override claimManager; address public override coverPoolFactory; @@ -95,14 +97,14 @@ contract PolicyManager is /** * @inheritdoc IPolicyManager */ - function pause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function pause() external override onlyRole(PAUSER_ROLE) { _pause(); } /** * @inheritdoc IPolicyManager */ - function unpause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function unpause() external override onlyRole(PAUSER_ROLE) { _unpause(); } diff --git a/src/PremiumManager.sol b/src/PremiumManager.sol index 3a85a52b..0082c278 100644 --- a/src/PremiumManager.sol +++ b/src/PremiumManager.sol @@ -31,6 +31,7 @@ contract PremiumManager is using EnumerableSet for EnumerableSet.AddressSet; bytes32 public constant CLAIM_MANAGER_ROLE = keccak256("CLAIM_MANAGER_ROLE"); + bytes32 public constant override PAUSER_ROLE = keccak256("PAUSER_ROLE"); uint16 private constant _BPS_DENOMINATOR = 10_000; uint16 private constant _MAX_PLATFORM_FEE_BPS = 2500; // 25% @@ -82,14 +83,14 @@ contract PremiumManager is /** * @inheritdoc IPremiumManager */ - function pause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function pause() external override onlyRole(PAUSER_ROLE) { _pause(); } /** * @inheritdoc IPremiumManager */ - function unpause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function unpause() external override onlyRole(PAUSER_ROLE) { _unpause(); } diff --git a/src/SpecRegistry.sol b/src/SpecRegistry.sol index 8291f474..2219853b 100644 --- a/src/SpecRegistry.sol +++ b/src/SpecRegistry.sol @@ -15,6 +15,8 @@ import {ICoverPoolFactory} from "./interfaces/ICoverPoolFactory.sol"; * Once a (coverPool, specId) pair is registered, it cannot be changed to a different spec address. */ contract SpecRegistry is UUPSUpgradeable, AccessControlUpgradeable, PausableUpgradeable, ISpecRegistry { + bytes32 public constant override PAUSER_ROLE = keccak256("PAUSER_ROLE"); + ICoverPoolFactory public coverPoolFactory; mapping(address coverPool => mapping(bytes32 specId => ISpec spec)) private _specs; @@ -58,14 +60,14 @@ contract SpecRegistry is UUPSUpgradeable, AccessControlUpgradeable, PausableUpgr /** * @inheritdoc ISpecRegistry */ - function pause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function pause() external override onlyRole(PAUSER_ROLE) { _pause(); } /** * @inheritdoc ISpecRegistry */ - function unpause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function unpause() external override onlyRole(PAUSER_ROLE) { _unpause(); } diff --git a/src/Swapper.sol b/src/Swapper.sol index 9956b050..d45084ad 100644 --- a/src/Swapper.sol +++ b/src/Swapper.sol @@ -42,6 +42,7 @@ contract Swapper is UUPSUpgradeable, AccessControlUpgradeable, PausableUpgradeab bytes32 public constant override SWAP_MANAGER_ROLE = keccak256("SWAP_MANAGER_ROLE"); bytes32 public constant override SWAP_EXECUTOR_ROLE = keccak256("SWAP_EXECUTOR_ROLE"); + bytes32 public constant override PAUSER_ROLE = keccak256("PAUSER_ROLE"); address public constant override NATIVE_ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; address public override nativeWrapper; @@ -101,14 +102,14 @@ contract Swapper is UUPSUpgradeable, AccessControlUpgradeable, PausableUpgradeab /** * @inheritdoc ISwapper */ - function pause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function pause() external override onlyRole(PAUSER_ROLE) { _pause(); } /** * @inheritdoc ISwapper */ - function unpause() external override onlyRole(DEFAULT_ADMIN_ROLE) { + function unpause() external override onlyRole(PAUSER_ROLE) { _unpause(); } diff --git a/src/interfaces/IClaimManager.sol b/src/interfaces/IClaimManager.sol index 705b6d91..b009052b 100644 --- a/src/interfaces/IClaimManager.sol +++ b/src/interfaces/IClaimManager.sol @@ -401,13 +401,13 @@ interface IClaimManager is IAccessControl { /** * @notice Pauses claim operations. - * @dev Requires DEFAULT_ADMIN_ROLE. + * @dev Requires PAUSER_ROLE. */ function pause() external; /** * @notice Resumes claim operations. - * @dev Requires DEFAULT_ADMIN_ROLE. + * @dev Requires PAUSER_ROLE. */ function unpause() external; @@ -619,4 +619,9 @@ interface IClaimManager is IAccessControl { * in basis points. Cross-token claims revert with `QuoteDeviatesFromOracle` if exceeded. */ function priceDeviationToleranceBps() external view returns (uint16); + + /** + * @notice AccessControl identifier for the pause role. + */ + function PAUSER_ROLE() external view returns (bytes32); } diff --git a/src/interfaces/ICoverPoolFactory.sol b/src/interfaces/ICoverPoolFactory.sol index 0fec92ba..088b66e1 100644 --- a/src/interfaces/ICoverPoolFactory.sol +++ b/src/interfaces/ICoverPoolFactory.sol @@ -78,11 +78,13 @@ interface ICoverPoolFactory is IAccessControl { /** * @notice Pauses cover pool creation. + * @dev Requires PAUSER_ROLE. */ function pause() external; /** * @notice Resumes cover pool creation. + * @dev Requires PAUSER_ROLE. */ function unpause() external; @@ -186,4 +188,9 @@ interface ICoverPoolFactory is IAccessControl { * @return The role identifier hash. */ function CREATOR_ROLE() external view returns (bytes32); + + /** + * @notice AccessControl identifier for the pause role. + */ + function PAUSER_ROLE() external view returns (bytes32); } diff --git a/src/interfaces/IPolicyManager.sol b/src/interfaces/IPolicyManager.sol index d6fe397c..4b577134 100644 --- a/src/interfaces/IPolicyManager.sol +++ b/src/interfaces/IPolicyManager.sol @@ -301,13 +301,13 @@ interface IPolicyManager is IAccessControl { /** * @notice Pauses policy binding operations. - * @dev Requires DEFAULT_ADMIN_ROLE. + * @dev Requires PAUSER_ROLE. */ function pause() external; /** * @notice Resumes policy binding operations. - * @dev Requires DEFAULT_ADMIN_ROLE. + * @dev Requires PAUSER_ROLE. */ function unpause() external; @@ -444,4 +444,9 @@ interface IPolicyManager is IAccessControl { * @param defaulted True to mark the policy as in premium default, false to clear. */ function setPremiumDefaulted(uint96 policyId, bool defaulted) external; + + /** + * @notice AccessControl identifier for the pause role. + */ + function PAUSER_ROLE() external view returns (bytes32); } diff --git a/src/interfaces/IPremiumManager.sol b/src/interfaces/IPremiumManager.sol index 9f1727dc..226607b3 100644 --- a/src/interfaces/IPremiumManager.sol +++ b/src/interfaces/IPremiumManager.sol @@ -119,13 +119,13 @@ interface IPremiumManager is IAccessControl { /** * @notice Pauses premium processing operations. - * @dev Requires DEFAULT_ADMIN_ROLE. + * @dev Requires PAUSER_ROLE. */ function pause() external; /** * @notice Resumes premium processing operations. - * @dev Requires DEFAULT_ADMIN_ROLE. + * @dev Requires PAUSER_ROLE. */ function unpause() external; @@ -252,4 +252,9 @@ interface IPremiumManager is IAccessControl { external pure returns (uint256 platformSplit, uint256 poolSplit, uint256 restakerSplit); + + /** + * @notice AccessControl identifier for the pause role. + */ + function PAUSER_ROLE() external view returns (bytes32); } diff --git a/src/interfaces/ISpecRegistry.sol b/src/interfaces/ISpecRegistry.sol index bc648040..8a543f45 100644 --- a/src/interfaces/ISpecRegistry.sol +++ b/src/interfaces/ISpecRegistry.sol @@ -79,13 +79,13 @@ interface ISpecRegistry { /** * @notice Pauses all spec registration operations. - * @dev Can only be called by accounts with DEFAULT_ADMIN_ROLE. + * @dev Can only be called by accounts with PAUSER_ROLE. */ function pause() external; /** * @notice Unpauses all spec registration operations. - * @dev Can only be called by accounts with DEFAULT_ADMIN_ROLE. + * @dev Can only be called by accounts with PAUSER_ROLE. */ function unpause() external; @@ -134,4 +134,9 @@ interface ISpecRegistry { * @return spec Address of the authorized ISpec implementation contract. */ function resolveSpec(address coverPool, bytes32 specId) external view returns (ISpec spec); + + /** + * @notice AccessControl identifier for the pause role. + */ + function PAUSER_ROLE() external view returns (bytes32); } diff --git a/src/interfaces/ISwapper.sol b/src/interfaces/ISwapper.sol index bc471f93..731d46d8 100644 --- a/src/interfaces/ISwapper.sol +++ b/src/interfaces/ISwapper.sol @@ -196,13 +196,13 @@ interface ISwapper is IAccessControl { /** * @notice Pauses swap operations. - * @dev Requires DEFAULT_ADMIN_ROLE. + * @dev Requires PAUSER_ROLE. */ function pause() external; /** * @notice Resumes swap operations. - * @dev Requires DEFAULT_ADMIN_ROLE. + * @dev Requires PAUSER_ROLE. */ function unpause() external; @@ -323,6 +323,11 @@ interface ISwapper is IAccessControl { */ function SWAP_EXECUTOR_ROLE() external view returns (bytes32); + /** + * @notice AccessControl identifier for the pause role. + */ + function PAUSER_ROLE() external view returns (bytes32); + /** * @notice Address of the native token wrapper contract (e.g., WETH). * @return Address of the native token wrapper. diff --git a/src/mocks/MockSpecRegistry.sol b/src/mocks/MockSpecRegistry.sol index db879ea7..d3359a07 100644 --- a/src/mocks/MockSpecRegistry.sol +++ b/src/mocks/MockSpecRegistry.sol @@ -11,6 +11,8 @@ import {ISpecRegistry} from "../interfaces/ISpecRegistry.sol"; * Provides flexible claim evaluation without real oracle dependencies. */ contract MockSpecRegistry is ISpecRegistry { + bytes32 public constant override PAUSER_ROLE = keccak256("PAUSER_ROLE"); + mapping(address coverPool => mapping(bytes32 specId => ISpec spec)) private _specs; address private _defaultSpec; address public immutable OWNER; diff --git a/test/unit/ClaimManager.t.sol b/test/unit/ClaimManager.t.sol index 44276794..246747c2 100644 --- a/test/unit/ClaimManager.t.sol +++ b/test/unit/ClaimManager.t.sol @@ -158,6 +158,10 @@ contract ClaimManagerTest is Test { vm.prank(admin); claimManager.initializeV2(address(oracleFeedMock), 300); + bytes32 pauserRole = claimManager.PAUSER_ROLE(); + vm.prank(admin); + claimManager.grantRole(pauserRole, admin); + vm.label(address(claimManager), "ClaimManager"); vm.label(address(oracleFeedMock), "OraclePriceFeed"); @@ -332,9 +336,7 @@ contract ClaimManagerTest is Test { function test_pause_WhenCallerNotAdmin_Reverts() public { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, - unauthorized, - claimManager.DEFAULT_ADMIN_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, unauthorized, claimManager.PAUSER_ROLE() ) ); vm.prank(unauthorized); @@ -353,9 +355,7 @@ contract ClaimManagerTest is Test { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, - unauthorized, - claimManager.DEFAULT_ADMIN_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, unauthorized, claimManager.PAUSER_ROLE() ) ); vm.prank(unauthorized); @@ -382,6 +382,47 @@ contract ClaimManagerTest is Test { assertTrue(claimManager.paused()); } + function test_pause_WhenAdminLacksPauserRole_Reverts() public { + address adminNoPauser = makeAddr("adminNoPauser"); + bytes32 adminRole = claimManager.DEFAULT_ADMIN_ROLE(); + vm.prank(admin); + claimManager.grantRole(adminRole, adminNoPauser); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, adminNoPauser, claimManager.PAUSER_ROLE() + ) + ); + vm.prank(adminNoPauser); + claimManager.pause(); + } + + function test_pause_WhenCallerOnlyHasPauserRole_Succeeds() public { + address pauserOnly = makeAddr("pauserOnly"); + bytes32 pauserRole = claimManager.PAUSER_ROLE(); + vm.prank(admin); + claimManager.grantRole(pauserRole, pauserOnly); + + vm.prank(pauserOnly); + claimManager.pause(); + assertTrue(claimManager.paused()); + + vm.prank(pauserOnly); + claimManager.unpause(); + assertFalse(claimManager.paused()); + } + + function test_pause_WhenRandomCaller_Reverts() public { + address random = makeAddr("random"); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, random, claimManager.PAUSER_ROLE() + ) + ); + vm.prank(random); + claimManager.pause(); + } + /*////////////////////////////////////////////////////////////// SET SPEC REGISTRY //////////////////////////////////////////////////////////////*/ diff --git a/test/unit/CoverPoolFactory.t.sol b/test/unit/CoverPoolFactory.t.sol index 345476b5..45c81728 100644 --- a/test/unit/CoverPoolFactory.t.sol +++ b/test/unit/CoverPoolFactory.t.sol @@ -70,6 +70,10 @@ contract CoverPoolFactoryTest is Test { ); factory = CoverPoolFactory(proxy); + bytes32 pauserRole = factory.PAUSER_ROLE(); + vm.prank(admin); + factory.grantRole(pauserRole, admin); + creatorRole = factory.CREATOR_ROLE(); } @@ -183,7 +187,9 @@ contract CoverPoolFactoryTest is Test { //////////////////////////////////////////////////////////////*/ function test_pause_WhenCallerNotAdmin_Reverts() public { vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, creator, ADMIN_ROLE) + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, creator, factory.PAUSER_ROLE() + ) ); vm.prank(creator); factory.pause(); @@ -203,7 +209,9 @@ contract CoverPoolFactoryTest is Test { factory.pause(); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, creator, ADMIN_ROLE) + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, creator, factory.PAUSER_ROLE() + ) ); vm.prank(creator); factory.unpause(); @@ -219,6 +227,47 @@ contract CoverPoolFactoryTest is Test { assertFalse(factory.paused(), "Factory still paused"); } + function test_pause_WhenAdminLacksPauserRole_Reverts() public { + address adminNoPauser = makeAddr("adminNoPauser"); + bytes32 adminRole = factory.DEFAULT_ADMIN_ROLE(); + vm.prank(admin); + factory.grantRole(adminRole, adminNoPauser); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, adminNoPauser, factory.PAUSER_ROLE() + ) + ); + vm.prank(adminNoPauser); + factory.pause(); + } + + function test_pause_WhenCallerOnlyHasPauserRole_Succeeds() public { + address pauserOnly = makeAddr("pauserOnly"); + bytes32 pauserRole = factory.PAUSER_ROLE(); + vm.prank(admin); + factory.grantRole(pauserRole, pauserOnly); + + vm.prank(pauserOnly); + factory.pause(); + assertTrue(factory.paused(), "Factory not paused"); + + vm.prank(pauserOnly); + factory.unpause(); + assertFalse(factory.paused(), "Factory still paused"); + } + + function test_pause_WhenRandomCaller_Reverts() public { + address random = makeAddr("random"); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, random, factory.PAUSER_ROLE() + ) + ); + vm.prank(random); + factory.pause(); + } + /*////////////////////////////////////////////////////////////// CREATE COVER POOL //////////////////////////////////////////////////////////////*/ diff --git a/test/unit/PolicyManager.t.sol b/test/unit/PolicyManager.t.sol index 7c437fb1..6292d3b1 100644 --- a/test/unit/PolicyManager.t.sol +++ b/test/unit/PolicyManager.t.sol @@ -109,6 +109,10 @@ contract PolicyManagerTest is Test { ); policyManager = PolicyManager(proxy); + bytes32 pauserRole = policyManager.PAUSER_ROLE(); + vm.prank(admin); + policyManager.grantRole(pauserRole, admin); + vm.label(address(policyManager), "PolicyManager"); vm.label(address(coverPool), "CoverPool"); } @@ -232,7 +236,7 @@ contract PolicyManagerTest is Test { function test_pause_WhenCallerNotAdmin_Reverts() public { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, buyer, policyManager.DEFAULT_ADMIN_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, buyer, policyManager.PAUSER_ROLE() ) ); vm.prank(buyer); @@ -256,7 +260,7 @@ contract PolicyManagerTest is Test { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, buyer, policyManager.DEFAULT_ADMIN_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, buyer, policyManager.PAUSER_ROLE() ) ); vm.prank(buyer); @@ -272,6 +276,47 @@ contract PolicyManagerTest is Test { assertFalse(policyManager.paused(), "Manager still paused"); } + function test_pause_WhenAdminLacksPauserRole_Reverts() public { + address adminNoPauser = makeAddr("adminNoPauser"); + bytes32 adminRole = policyManager.DEFAULT_ADMIN_ROLE(); + vm.prank(admin); + policyManager.grantRole(adminRole, adminNoPauser); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, adminNoPauser, policyManager.PAUSER_ROLE() + ) + ); + vm.prank(adminNoPauser); + policyManager.pause(); + } + + function test_pause_WhenCallerOnlyHasPauserRole_Succeeds() public { + address pauserOnly = makeAddr("pauserOnly"); + bytes32 pauserRole = policyManager.PAUSER_ROLE(); + vm.prank(admin); + policyManager.grantRole(pauserRole, pauserOnly); + + vm.prank(pauserOnly); + policyManager.pause(); + assertTrue(policyManager.paused(), "Manager not paused"); + + vm.prank(pauserOnly); + policyManager.unpause(); + assertFalse(policyManager.paused(), "Manager still paused"); + } + + function test_pause_WhenRandomCaller_Reverts() public { + address random = makeAddr("random"); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, random, policyManager.PAUSER_ROLE() + ) + ); + vm.prank(random); + policyManager.pause(); + } + /*////////////////////////////////////////////////////////////// SET STAKE MANAGER //////////////////////////////////////////////////////////////*/ diff --git a/test/unit/PremiumManager.t.sol b/test/unit/PremiumManager.t.sol index b916811e..b2538754 100644 --- a/test/unit/PremiumManager.t.sol +++ b/test/unit/PremiumManager.t.sol @@ -72,8 +72,11 @@ contract PremiumManagerTest is Test { vm.label(address(premiumToken), "PremiumToken"); vm.label(address(mockPool), "MockPool"); - vm.prank(admin); + bytes32 pauserRole = premiumManager.PAUSER_ROLE(); + vm.startPrank(admin); + premiumManager.grantRole(pauserRole, admin); premiumManager.addApprovedPremiumToken(address(premiumToken)); + vm.stopPrank(); } /*////////////////////////////////////////////////////////////// @@ -123,9 +126,7 @@ contract PremiumManagerTest is Test { function test_pause_WhenCallerNotAdmin_Reverts() public { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, - premiumCollector, - premiumManager.DEFAULT_ADMIN_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, premiumCollector, premiumManager.PAUSER_ROLE() ) ); vm.prank(premiumCollector); @@ -149,9 +150,7 @@ contract PremiumManagerTest is Test { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, - premiumCollector, - premiumManager.DEFAULT_ADMIN_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, premiumCollector, premiumManager.PAUSER_ROLE() ) ); vm.prank(premiumCollector); @@ -167,6 +166,47 @@ contract PremiumManagerTest is Test { assertFalse(premiumManager.paused(), "Manager still paused"); } + function test_pause_WhenAdminLacksPauserRole_Reverts() public { + address adminNoPauser = makeAddr("adminNoPauser"); + bytes32 adminRole = premiumManager.DEFAULT_ADMIN_ROLE(); + vm.prank(admin); + premiumManager.grantRole(adminRole, adminNoPauser); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, adminNoPauser, premiumManager.PAUSER_ROLE() + ) + ); + vm.prank(adminNoPauser); + premiumManager.pause(); + } + + function test_pause_WhenCallerOnlyHasPauserRole_Succeeds() public { + address pauserOnly = makeAddr("pauserOnly"); + bytes32 pauserRole = premiumManager.PAUSER_ROLE(); + vm.prank(admin); + premiumManager.grantRole(pauserRole, pauserOnly); + + vm.prank(pauserOnly); + premiumManager.pause(); + assertTrue(premiumManager.paused(), "Manager not paused"); + + vm.prank(pauserOnly); + premiumManager.unpause(); + assertFalse(premiumManager.paused(), "Manager still paused"); + } + + function test_pause_WhenRandomCaller_Reverts() public { + address random = makeAddr("random"); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, random, premiumManager.PAUSER_ROLE() + ) + ); + vm.prank(random); + premiumManager.pause(); + } + /*////////////////////////////////////////////////////////////// SET REWARDS MANAGER //////////////////////////////////////////////////////////////*/ diff --git a/test/unit/SpecRegistry.t.sol b/test/unit/SpecRegistry.t.sol index b41805de..4f61afb9 100644 --- a/test/unit/SpecRegistry.t.sol +++ b/test/unit/SpecRegistry.t.sol @@ -65,7 +65,9 @@ contract SpecRegistryTest is Test { specId1 = keccak256("MorphoSpec"); specId2 = keccak256("MapleSpec"); + bytes32 pauserRole = registry.PAUSER_ROLE(); vm.startPrank(admin); + registry.grantRole(pauserRole, admin); registry.approveSpec(spec1); registry.approveSpec(spec2); vm.stopPrank(); @@ -167,7 +169,9 @@ contract SpecRegistryTest is Test { function test_pause_WhenCallerNotAdmin_Reverts() public { address notAdmin = makeAddr("notAdmin"); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, notAdmin, ADMIN_ROLE) + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, notAdmin, registry.PAUSER_ROLE() + ) ); vm.prank(notAdmin); registry.pause(); @@ -189,7 +193,9 @@ contract SpecRegistryTest is Test { address notAdmin = makeAddr("notAdmin"); vm.expectRevert( - abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, notAdmin, ADMIN_ROLE) + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, notAdmin, registry.PAUSER_ROLE() + ) ); vm.prank(notAdmin); registry.unpause(); @@ -205,6 +211,47 @@ contract SpecRegistryTest is Test { assertFalse(registry.paused(), "Registry still paused"); } + function test_pause_WhenAdminLacksPauserRole_Reverts() public { + address adminNoPauser = makeAddr("adminNoPauser"); + bytes32 adminRole = registry.DEFAULT_ADMIN_ROLE(); + vm.prank(admin); + registry.grantRole(adminRole, adminNoPauser); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, adminNoPauser, registry.PAUSER_ROLE() + ) + ); + vm.prank(adminNoPauser); + registry.pause(); + } + + function test_pause_WhenCallerOnlyHasPauserRole_Succeeds() public { + address pauserOnly = makeAddr("pauserOnly"); + bytes32 pauserRole = registry.PAUSER_ROLE(); + vm.prank(admin); + registry.grantRole(pauserRole, pauserOnly); + + vm.prank(pauserOnly); + registry.pause(); + assertTrue(registry.paused(), "Registry not paused"); + + vm.prank(pauserOnly); + registry.unpause(); + assertFalse(registry.paused(), "Registry still paused"); + } + + function test_pause_WhenRandomCaller_Reverts() public { + address random = makeAddr("random"); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, random, registry.PAUSER_ROLE() + ) + ); + vm.prank(random); + registry.pause(); + } + /*////////////////////////////////////////////////////////////// APPROVE SPEC //////////////////////////////////////////////////////////////*/ diff --git a/test/unit/Swapper.t.sol b/test/unit/Swapper.t.sol index ad7ad9ea..58ad607c 100644 --- a/test/unit/Swapper.t.sol +++ b/test/unit/Swapper.t.sol @@ -65,6 +65,10 @@ contract SwapperTest is Test { ); swapper = Swapper(payable(proxy)); + bytes32 pauserRole = swapper.PAUSER_ROLE(); + vm.prank(admin); + swapper.grantRole(pauserRole, admin); + vm.label(address(swapper), "Swapper"); vm.label(address(tokenIn), "TokenIn"); vm.label(address(tokenOut), "TokenOut"); @@ -178,7 +182,7 @@ contract SwapperTest is Test { function test_pause_WhenCallerNotAdmin_Reverts() public { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), swapper.DEFAULT_ADMIN_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), swapper.PAUSER_ROLE() ) ); swapper.pause(); @@ -203,12 +207,53 @@ contract SwapperTest is Test { vm.expectRevert( abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), swapper.DEFAULT_ADMIN_ROLE() + IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), swapper.PAUSER_ROLE() ) ); swapper.unpause(); } + function test_pause_WhenAdminLacksPauserRole_Reverts() public { + address adminNoPauser = makeAddr("adminNoPauser"); + bytes32 adminRole = swapper.DEFAULT_ADMIN_ROLE(); + vm.prank(admin); + swapper.grantRole(adminRole, adminNoPauser); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, adminNoPauser, swapper.PAUSER_ROLE() + ) + ); + vm.prank(adminNoPauser); + swapper.pause(); + } + + function test_pause_WhenCallerOnlyHasPauserRole_Succeeds() public { + address pauserOnly = makeAddr("pauserOnly"); + bytes32 pauserRole = swapper.PAUSER_ROLE(); + vm.prank(admin); + swapper.grantRole(pauserRole, pauserOnly); + + vm.prank(pauserOnly); + swapper.pause(); + assertTrue(swapper.paused(), "Contract should be paused"); + + vm.prank(pauserOnly); + swapper.unpause(); + assertFalse(swapper.paused(), "Contract should not be paused"); + } + + function test_pause_WhenRandomCaller_Reverts() public { + address random = makeAddr("random"); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, random, swapper.PAUSER_ROLE() + ) + ); + vm.prank(random); + swapper.pause(); + } + /*////////////////////////////////////////////////////////////// SET SWAP TARGET WHITELIST //////////////////////////////////////////////////////////////*/ diff --git a/test/unit/TimelockIntegration.t.sol b/test/unit/TimelockIntegration.t.sol index d3aefd5a..62c1804f 100644 --- a/test/unit/TimelockIntegration.t.sol +++ b/test/unit/TimelockIntegration.t.sol @@ -31,7 +31,6 @@ contract TimelockIntegrationTest is Test { multisig = makeAddr("multisig"); deployer = makeAddr("deployer"); - // Deploy TimelockController address[] memory proposers = new address[](1); proposers[0] = multisig; address[] memory executors = new address[](1); @@ -39,7 +38,6 @@ contract TimelockIntegrationTest is Test { timelock = new TimelockController(MIN_DELAY, proposers, executors, address(0)); - // Deploy contracts with deployer as temp admin Options memory opts; opts.unsafeSkipAllChecks = true; @@ -74,13 +72,17 @@ contract TimelockIntegrationTest is Test { ); claimManager = ClaimManager(payable(cmProxy)); - // Grant DEFAULT_ADMIN_ROLE to timelock IAccessControl(address(policyManager)).grantRole(DEFAULT_ADMIN_ROLE, address(timelock)); IAccessControl(address(premiumManager)).grantRole(DEFAULT_ADMIN_ROLE, address(timelock)); IAccessControl(address(specRegistry)).grantRole(DEFAULT_ADMIN_ROLE, address(timelock)); IAccessControl(address(claimManager)).grantRole(DEFAULT_ADMIN_ROLE, address(timelock)); - // Revoke deployer + bytes32 pauserRole = policyManager.PAUSER_ROLE(); + IAccessControl(address(policyManager)).grantRole(pauserRole, address(timelock)); + IAccessControl(address(premiumManager)).grantRole(pauserRole, address(timelock)); + IAccessControl(address(specRegistry)).grantRole(pauserRole, address(timelock)); + IAccessControl(address(claimManager)).grantRole(pauserRole, address(timelock)); + IAccessControl(address(policyManager)).revokeRole(DEFAULT_ADMIN_ROLE, deployer); IAccessControl(address(premiumManager)).revokeRole(DEFAULT_ADMIN_ROLE, deployer); IAccessControl(address(specRegistry)).revokeRole(DEFAULT_ADMIN_ROLE, deployer); @@ -89,9 +91,6 @@ contract TimelockIntegrationTest is Test { vm.stopPrank(); } - // ─── Role correctness - // ───────────────────────────────────────── - function test_timelockHasAdminRoleOnAllContracts() public view { assertTrue(IAccessControl(address(policyManager)).hasRole(DEFAULT_ADMIN_ROLE, address(timelock))); assertTrue(IAccessControl(address(premiumManager)).hasRole(DEFAULT_ADMIN_ROLE, address(timelock))); @@ -117,9 +116,6 @@ contract TimelockIntegrationTest is Test { assertFalse(IAccessControl(address(premiumManager)).hasRole(DEFAULT_ADMIN_ROLE, multisig)); } - // ─── Direct admin calls revert - // ──────────────────────────────── - function test_directPauseReverts() public { vm.prank(multisig); vm.expectRevert(); @@ -132,25 +128,18 @@ contract TimelockIntegrationTest is Test { premiumManager.setPlatformTreasury(address(0xBEEF)); } - // ─── Timelocked admin operations - // ────────────────────────────── - function test_timelockPause() public { bytes memory data = abi.encodeCall(PolicyManager.pause, ()); - // Schedule vm.prank(multisig); timelock.schedule(address(policyManager), 0, data, bytes32(0), bytes32(0), MIN_DELAY); - // Execute before delay — should revert vm.prank(multisig); vm.expectRevert(); timelock.execute(address(policyManager), 0, data, bytes32(0), bytes32(0)); - // Advance time past delay vm.warp(block.timestamp + MIN_DELAY); - // Execute after delay — should succeed vm.prank(multisig); timelock.execute(address(policyManager), 0, data, bytes32(0), bytes32(0)); @@ -172,9 +161,6 @@ contract TimelockIntegrationTest is Test { assertEq(premiumManager.platformTreasury(), newTreasury); } - // ─── Cancellation - // ───────────────────────────────────────────── - function test_cancelledOperationCannotExecute() public { bytes memory data = abi.encodeCall(PolicyManager.pause, ()); bytes32 id = timelock.hashOperation(address(policyManager), 0, data, bytes32(0), bytes32(0)); @@ -182,14 +168,11 @@ contract TimelockIntegrationTest is Test { vm.prank(multisig); timelock.schedule(address(policyManager), 0, data, bytes32(0), bytes32(0), MIN_DELAY); - // Cancel vm.prank(multisig); timelock.cancel(id); - // Advance time vm.warp(block.timestamp + MIN_DELAY); - // Execute should revert vm.prank(multisig); vm.expectRevert(); timelock.execute(address(policyManager), 0, data, bytes32(0), bytes32(0)); @@ -197,16 +180,12 @@ contract TimelockIntegrationTest is Test { assertFalse(policyManager.paused()); } - // ─── Premature execution reverts - // ────────────────────────────── - function test_prematureExecutionReverts() public { bytes memory data = abi.encodeCall(PolicyManager.pause, ()); vm.prank(multisig); timelock.schedule(address(policyManager), 0, data, bytes32(0), bytes32(0), MIN_DELAY); - // Try execute 1 second before delay expires vm.warp(block.timestamp + MIN_DELAY - 1); vm.prank(multisig); @@ -214,16 +193,11 @@ contract TimelockIntegrationTest is Test { timelock.execute(address(policyManager), 0, data, bytes32(0), bytes32(0)); } - // ─── Upgrade via timelock - // ───────────────────────────────────── - function test_upgradeViaTimelock() public { address oldImpl = Upgrades.getImplementationAddress(address(policyManager)); - // Deploy new implementation PolicyManager newImpl = new PolicyManager(); - // Schedule upgradeToAndCall through timelock bytes memory upgradeData = abi.encodeCall(policyManager.upgradeToAndCall, (address(newImpl), "")); vm.prank(multisig); @@ -239,9 +213,6 @@ contract TimelockIntegrationTest is Test { assertTrue(currentImpl != oldImpl); } - // ─── Unauthorized proposer reverts - // ──────────────────────────── - function test_nonProposerCannotSchedule() public { address attacker = makeAddr("attacker"); bytes memory data = abi.encodeCall(PolicyManager.pause, ()); @@ -251,9 +222,6 @@ contract TimelockIntegrationTest is Test { timelock.schedule(address(policyManager), 0, data, bytes32(0), bytes32(0), MIN_DELAY); } - // ─── Batch operations - // ───────────────────────────────────────── - function test_timelockBatchOperation() public { address[] memory targets = new address[](2); targets[0] = address(policyManager);