From 816a136a5afe0f6846f2cdf5cf2fad848362e6ea Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Wed, 20 May 2026 10:16:29 +0200 Subject: [PATCH 1/2] chore: add scripts to upgrade all coverage contracts --- script/UpgradeAllCoverageEthereum.s.sol | 264 +++++++++++++++++++++++ script/UpgradeAllCoverageSepolia.s.sol | 245 +++++++++++++++++++++ script/UpgradeClaimManagerEthereum.s.sol | 23 +- script/UpgradeSwapper.s.sol | 8 + 4 files changed, 526 insertions(+), 14 deletions(-) create mode 100644 script/UpgradeAllCoverageEthereum.s.sol create mode 100644 script/UpgradeAllCoverageSepolia.s.sol diff --git a/script/UpgradeAllCoverageEthereum.s.sol b/script/UpgradeAllCoverageEthereum.s.sol new file mode 100644 index 00000000..e4e382db --- /dev/null +++ b/script/UpgradeAllCoverageEthereum.s.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {Script, console2} from "forge-std/Script.sol"; + +import {Swapper} from "../src/Swapper.sol"; +import {ClaimManager} from "../src/ClaimManager.sol"; +import {PolicyManager} from "../src/PolicyManager.sol"; +import {PremiumManager} from "../src/PremiumManager.sol"; +import {CoverPoolFactory} from "../src/CoverPoolFactory.sol"; +import {SpecRegistry} from "../src/SpecRegistry.sol"; +import {CoverPool} from "../src/CoverPool.sol"; + +/** + * @title UpgradeAllCoverageEthereum + * @notice Hybrid upgrade orchestrator for Coverage contracts on Ethereum mainnet. + * + * @dev Two-phase execution: + * + * Phase 1 (broadcast from deployer EOA): + * Deploys 7 new implementations (Swapper, ClaimManager, PolicyManager, PremiumManager, + * CoverPoolFactory, SpecRegistry, CoverPool clone-target). + * + * Phase 2 (Safe TX Builder, multisig → TimelockController — min delay 2 days): + * Prints scheduleBatch + executeBatch calldata for 13 calls through the Timelock. + * Also prints a separate immediate multisig call for setCoverPoolImplementation + * (CREATOR_ROLE is held by the multisig directly, not the Timelock). + * + * 13-call Timelock batch: + * 1. Swapper.upgradeToAndCall(swapperImpl, "") + * 2. Swapper.setClaimManager(claimManagerProxy) <- CYS3-06 MANDATORY + * 3. ClaimManager.upgradeToAndCall(impl, initializeV2(oraclePriceFeed, toleranceBps)) + * 4. PolicyManager.upgradeToAndCall(impl, "") + * 5. PremiumManager.upgradeToAndCall(impl, "") + * 6. CoverPoolFactory.upgradeToAndCall(impl, "") + * 7. SpecRegistry.upgradeToAndCall(impl, "") + * 8-13. grantRole(PAUSER_ROLE, pauser) on each of the 6 proxies + * + * Separate immediate multisig call: + * CoverPoolFactory.setCoverPoolImplementation(coverPoolImpl) + * + * @dev Prerequisites: + * forge clean && forge build + * forge test --match-contract StorageLayoutTest -vvv + * + * @dev Environment Variables: + * DEPLOYER_ADDRESS, TIMELOCK, ADMIN, PAUSER, ORACLE_PRICE_FEED, + * PRICE_DEVIATION_TOLERANCE_BPS (default 300), + * SWAPPER_PROXY, CLAIM_MANAGER_PROXY, POLICY_MANAGER_PROXY, + * PREMIUM_MANAGER_PROXY, COVER_POOL_FACTORY_PROXY, SPEC_REGISTRY_PROXY + * + * @dev Usage: + * DEPLOYER_ADDRESS= \ + * TIMELOCK=0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7 \ + * ADMIN=0xd2d03377Fa96687e9C11380DA9956AcC5F307e2c \ + * PAUSER= \ + * ORACLE_PRICE_FEED=0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16 \ + * PRICE_DEVIATION_TOLERANCE_BPS=300 \ + * SWAPPER_PROXY=0x148Bfe2330cEe4f227addb47736a105DB427dc31 \ + * CLAIM_MANAGER_PROXY=0x3ceE181C3E78fB9968f0Fb0935d2db0723B9Cb45 \ + * POLICY_MANAGER_PROXY=0xfb771BE75365D2a1D32be198e03ce8a2125e2699 \ + * PREMIUM_MANAGER_PROXY=0xEc7322D6754709d5001B710ec4fB2547a89B3aD1 \ + * COVER_POOL_FACTORY_PROXY=0x4f3DbB70cD85bcb63303FBa8610Cb163aDDA4E66 \ + * SPEC_REGISTRY_PROXY=0x90EfF742958dd4c54ede3ED365375a14077D0A58 \ + * forge script script/UpgradeAllCoverageEthereum.s.sol --broadcast \ + * --rpc-url $ETHEREUM_RPC_URL --keystore $KEYSTORE --sender $DEPLOYER_ADDRESS + */ +contract UpgradeAllCoverageEthereum is Script { + bytes32 internal constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 internal constant ZERO = bytes32(0); + + address internal _swapperImpl; + address internal _claimManagerImpl; + address internal _policyManagerImpl; + address internal _premiumManagerImpl; + address internal _coverPoolFactoryImpl; + address internal _specRegistryImpl; + address internal _coverPoolImpl; + + address internal _swapperProxy; + address internal _claimManagerProxy; + address internal _policyManagerProxy; + address internal _premiumManagerProxy; + address internal _coverPoolFactoryProxy; + address internal _specRegistryProxy; + + function run() external { + require(block.chainid == 1, "Must run on Ethereum mainnet"); + + address deployer = vm.envAddress("DEPLOYER_ADDRESS"); + address timelock = vm.envAddress("TIMELOCK"); + address admin = vm.envAddress("ADMIN"); + address pauser = vm.envAddress("PAUSER"); + address oraclePriceFeed = vm.envAddress("ORACLE_PRICE_FEED"); + uint16 toleranceBps = SafeCast.toUint16(vm.envOr("PRICE_DEVIATION_TOLERANCE_BPS", uint256(300))); + + _swapperProxy = vm.envAddress("SWAPPER_PROXY"); + _claimManagerProxy = vm.envAddress("CLAIM_MANAGER_PROXY"); + _policyManagerProxy = vm.envAddress("POLICY_MANAGER_PROXY"); + _premiumManagerProxy = vm.envAddress("PREMIUM_MANAGER_PROXY"); + _coverPoolFactoryProxy = vm.envAddress("COVER_POOL_FACTORY_PROXY"); + _specRegistryProxy = vm.envAddress("SPEC_REGISTRY_PROXY"); + + console2.log(""); + console2.log("=== UpgradeAllCoverageEthereum ==="); + console2.log("Chain ID:", block.chainid); + console2.log("Deployer:", deployer); + console2.log("Admin (multisig):", admin); + console2.log("Timelock:", timelock); + console2.log("Pauser:", pauser); + console2.log(""); + + // Phase 1: deployer deploys implementations + vm.startBroadcast(deployer); + _deployImplementations(); + vm.stopBroadcast(); + + // Phase 2: print Timelock batch calldata + uint256 minDelay = TimelockController(payable(timelock)).getMinDelay(); + _printBatch(timelock, oraclePriceFeed, toleranceBps, pauser, minDelay); + _printImmediateCall(admin); + } + + function _deployImplementations() internal { + console2.log("--- Phase 1: Deploying implementations ---"); + + _swapperImpl = address(new Swapper()); + console2.log("Swapper impl: ", _swapperImpl); + + _claimManagerImpl = address(new ClaimManager()); + console2.log("ClaimManager impl: ", _claimManagerImpl); + + _policyManagerImpl = address(new PolicyManager()); + console2.log("PolicyManager impl: ", _policyManagerImpl); + + _premiumManagerImpl = address(new PremiumManager()); + console2.log("PremiumManager impl: ", _premiumManagerImpl); + + _coverPoolFactoryImpl = address(new CoverPoolFactory()); + console2.log("CoverPoolFactory impl: ", _coverPoolFactoryImpl); + + _specRegistryImpl = address(new SpecRegistry()); + console2.log("SpecRegistry impl: ", _specRegistryImpl); + + _coverPoolImpl = address(new CoverPool()); + console2.log("CoverPool clone-target: ", _coverPoolImpl); + + console2.log(""); + } + + function _buildTargets() internal view returns (address[] memory targets) { + targets = new address[](13); + targets[0] = _swapperProxy; + targets[1] = _swapperProxy; + targets[2] = _claimManagerProxy; + targets[3] = _policyManagerProxy; + targets[4] = _premiumManagerProxy; + targets[5] = _coverPoolFactoryProxy; + targets[6] = _specRegistryProxy; + targets[7] = _swapperProxy; + targets[8] = _claimManagerProxy; + targets[9] = _policyManagerProxy; + targets[10] = _premiumManagerProxy; + targets[11] = _coverPoolFactoryProxy; + targets[12] = _specRegistryProxy; + } + + function _buildPayloads( + address oraclePriceFeed, + uint16 toleranceBps, + address pauser + ) + internal + view + returns (bytes[] memory payloads) + { + payloads = new bytes[](13); + bytes memory gr = abi.encodeCall(IAccessControl.grantRole, (PAUSER_ROLE, pauser)); + bytes memory initV2 = abi.encodeCall(ClaimManager.initializeV2, (oraclePriceFeed, toleranceBps)); + + payloads[0] = abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (_swapperImpl, "")); + payloads[1] = abi.encodeWithSignature("setClaimManager(address)", _claimManagerProxy); + payloads[2] = abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (_claimManagerImpl, initV2)); + payloads[3] = abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (_policyManagerImpl, "")); + payloads[4] = abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (_premiumManagerImpl, "")); + payloads[5] = abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (_coverPoolFactoryImpl, "")); + payloads[6] = abi.encodeCall(UUPSUpgradeable.upgradeToAndCall, (_specRegistryImpl, "")); + payloads[7] = gr; + payloads[8] = gr; + payloads[9] = gr; + payloads[10] = gr; + payloads[11] = gr; + payloads[12] = gr; + } + + function _printBatch( + address timelock, + address oraclePriceFeed, + uint16 toleranceBps, + address pauser, + uint256 minDelay + ) + internal + view + { + address[] memory targets = _buildTargets(); + uint256[] memory values = new uint256[](13); + bytes[] memory payloads = _buildPayloads(oraclePriceFeed, toleranceBps, pauser); + + bytes memory schedData = + abi.encodeCall(TimelockController.scheduleBatch, (targets, values, payloads, ZERO, ZERO, minDelay)); + bytes memory execData = abi.encodeCall(TimelockController.executeBatch, (targets, values, payloads, ZERO, ZERO)); + bytes32 opId = keccak256(abi.encode(targets, values, payloads, ZERO, ZERO)); + + console2.log("--- Phase 2: Safe TX Builder Calldata (13-call Timelock batch) ---"); + console2.log("TimelockController:", timelock); + console2.log("Min Delay (seconds):", minDelay); + console2.log("Min Delay (days):", minDelay / 86_400); + console2.log("Operation ID:", vm.toString(opId)); + console2.log(""); + console2.log("Batch call breakdown:"); + console2.log(" [ 1] Swapper.upgradeToAndCall:", _swapperImpl); + console2.log(" [ 2] Swapper.setClaimManager:", _claimManagerProxy, " <- CYS3-06"); + console2.log(" [ 3] ClaimManager.upgradeToAndCall:", _claimManagerImpl, " + initializeV2"); + console2.log(" [ 4] PolicyManager.upgradeToAndCall:", _policyManagerImpl); + console2.log(" [ 5] PremiumManager.upgradeToAndCall:", _premiumManagerImpl); + console2.log(" [ 6] CoverPoolFactory.upgradeToAndCall:", _coverPoolFactoryImpl); + console2.log(" [ 7] SpecRegistry.upgradeToAndCall:", _specRegistryImpl); + console2.log(" [ 8] Swapper.grantRole(PAUSER_ROLE):", pauser); + console2.log(" [ 9] ClaimManager.grantRole(PAUSER_ROLE):", pauser); + console2.log(" [10] PolicyManager.grantRole(PAUSER_ROLE):", pauser); + console2.log(" [11] PremiumManager.grantRole(PAUSER_ROLE):", pauser); + console2.log(" [12] CoverPoolFactory.grantRole(PAUSER_ROLE):", pauser); + console2.log(" [13] SpecRegistry.grantRole(PAUSER_ROLE):", pauser); + console2.log(""); + console2.log("Step 1 - scheduleBatch:"); + console2.log(" To:", timelock); + console2.log(" Data:", vm.toString(schedData)); + console2.log(""); + console2.log("Step 2 - executeBatch (after delay elapsed):"); + console2.log(" To:", timelock); + console2.log(" Data:", vm.toString(execData)); + console2.log(""); + console2.log("Monitor: cast call", timelock, "'isOperationReady(bytes32)(bool)'", vm.toString(opId)); + console2.log("===================================="); + } + + function _printImmediateCall(address admin) internal view { + bytes memory data = abi.encodeWithSignature("setCoverPoolImplementation(address)", _coverPoolImpl); + console2.log(""); + console2.log("--- Immediate Multisig Call (CoverPool clone-target, CREATOR_ROLE) ---"); + console2.log("Execute AFTER executeBatch is confirmed. CREATOR_ROLE is on multisig directly."); + console2.log(" From:", admin, " (multisig)"); + console2.log(" To:", _coverPoolFactoryProxy); + console2.log(" Data:", vm.toString(data)); + console2.log(" Effect: new CoverPools get CYS3-01 USD-conversion fix."); + console2.log(" Note: existing pool clones (EIP-1167) are unaffected."); + console2.log("===================================="); + } +} diff --git a/script/UpgradeAllCoverageSepolia.s.sol b/script/UpgradeAllCoverageSepolia.s.sol new file mode 100644 index 00000000..fef93483 --- /dev/null +++ b/script/UpgradeAllCoverageSepolia.s.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {Script, console2} from "forge-std/Script.sol"; + +import {Swapper} from "../src/Swapper.sol"; +import {ClaimManager} from "../src/ClaimManager.sol"; +import {PolicyManager} from "../src/PolicyManager.sol"; +import {PremiumManager} from "../src/PremiumManager.sol"; +import {CoverPoolFactory} from "../src/CoverPoolFactory.sol"; +import {SpecRegistry} from "../src/SpecRegistry.sol"; +import {CoverPool} from "../src/CoverPool.sol"; + +/// @dev Minimal interface — avoids importing the full contract. +interface ISwapperAdmin { + function setClaimManager(address claimManager_) external; +} + +interface ICoverPoolFactoryAdmin { + function setCoverPoolImplementation(address implementation_) external; +} + +/** + * @title UpgradeAllCoverageSepolia + * @notice Single-transaction orchestrator that upgrades all six Coverage UUPS proxies on Sepolia + * and performs all required post-upgrade wiring in one `vm.startBroadcast(admin)` block. + * + * @dev Upgrade sequence (proven by anvil-fork simulation): + * 1. Deploy 7 new implementations (Swapper, ClaimManager, PolicyManager, PremiumManager, + * CoverPoolFactory, SpecRegistry, CoverPool clone-target) + * 2. Swapper: upgradeToAndCall(impl, "") + * 3. Swapper: setClaimManager(claimManagerProxy) ← CYS3-06 wiring + * 4. ClaimManager: upgradeToAndCall(impl, initializeV2(…)) ← CYS3-03 V2 storage + * 5. PolicyManager: upgradeToAndCall(impl, "") + * 6. PremiumManager: upgradeToAndCall(impl, "") + * 7. CoverPoolFactory: upgradeToAndCall(impl, "") + * 8. SpecRegistry: upgradeToAndCall(impl, "") + * 9. CoverPoolFactory: setCoverPoolImplementation(coverPoolImpl) ← CYS3-01 clone target + * 10. grantRole(PAUSER_ROLE, pauser) on all 6 UUPS proxies ← PAUSER_ROLE + * + * @dev Environment Variables: + * - DEPLOYER_ADDRESS: Public address of the keystore signer (required) + * - ADMIN: Holder of DEFAULT_ADMIN_ROLE and CREATOR_ROLE on all proxies (Sepolia multisig) + * Defaults to DEPLOYER_ADDRESS + * - PAUSER: Address to receive PAUSER_ROLE on all six proxies (required) + * - ORACLE_PRICE_FEED: OraclePriceFeed proxy address (required for initializeV2) + * - PRICE_DEVIATION_TOLERANCE_BPS: Max DEX-vs-oracle deviation in bps (optional, default 300) + * - SWAPPER_PROXY: Swapper proxy address (required) + * - CLAIM_MANAGER_PROXY: ClaimManager proxy address (required) + * - POLICY_MANAGER_PROXY: PolicyManager proxy address (required) + * - PREMIUM_MANAGER_PROXY: PremiumManager proxy address (required) + * - COVER_POOL_FACTORY_PROXY: CoverPoolFactory proxy address (required) + * - SPEC_REGISTRY_PROXY: SpecRegistry proxy address (required) + * + * @dev Usage: + * DEPLOYER_ADDRESS=$ADMIN \ + * ADMIN=$ADMIN \ + * PAUSER=$PAUSER \ + * ORACLE_PRICE_FEED=0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16 \ + * PRICE_DEVIATION_TOLERANCE_BPS=300 \ + * SWAPPER_PROXY=0x148Bfe2330cEe4f227addb47736a105DB427dc31 \ + * CLAIM_MANAGER_PROXY=0x3ceE181C3E78fB9968f0Fb0935d2db0723B9Cb45 \ + * POLICY_MANAGER_PROXY=0xfb771BE75365D2a1D32be198e03ce8a2125e2699 \ + * PREMIUM_MANAGER_PROXY=0xEc7322D6754709d5001B710ec4fB2547a89B3aD1 \ + * COVER_POOL_FACTORY_PROXY=0x4f3DbB70cD85bcb63303FBa8610Cb163aDDA4E66 \ + * SPEC_REGISTRY_PROXY=0x90EfF742958dd4c54ede3ED365375a14077D0A58 \ + * forge script script/UpgradeAllCoverageSepolia.s.sol --broadcast \ + * --rpc-url $SEPOLIA_RPC_URL --keystore $KEYSTORE --sender $ADMIN + */ +contract UpgradeAllCoverageSepolia is Script { + struct Proxies { + address swapper; + address claimManager; + address policyManager; + address premiumManager; + address coverPoolFactory; + address specRegistry; + } + + struct Impls { + address swapper; + address claimManager; + address policyManager; + address premiumManager; + address coverPoolFactory; + address specRegistry; + address coverPool; + } + + function run() external { + require(block.chainid == 11_155_111, "Must run on Sepolia"); + + address deployer = vm.envAddress("DEPLOYER_ADDRESS"); + address admin = vm.envOr("ADMIN", deployer); + address pauser = vm.envAddress("PAUSER"); + address oraclePriceFeed = vm.envAddress("ORACLE_PRICE_FEED"); + uint16 toleranceBps = SafeCast.toUint16(vm.envOr("PRICE_DEVIATION_TOLERANCE_BPS", uint256(300))); + + Proxies memory proxies = _loadProxies(); + + console2.log(""); + console2.log("=== UpgradeAllCoverageSepolia ==="); + console2.log("Chain ID:", block.chainid); + console2.log("Admin:", admin); + console2.log("Pauser:", pauser); + console2.log("OraclePriceFeed:", oraclePriceFeed); + console2.log("PriceDeviationToleranceBps:", toleranceBps); + console2.log(""); + + vm.startBroadcast(admin); + + Impls memory impls = _deployImplementations(); + + _upgradeProxies(proxies, impls, oraclePriceFeed, toleranceBps); + + _grantPauserRole(proxies, pauser); + + vm.stopBroadcast(); + + _logSummary(proxies, impls); + } + + function _loadProxies() internal view returns (Proxies memory p) { + p.swapper = vm.envAddress("SWAPPER_PROXY"); + p.claimManager = vm.envAddress("CLAIM_MANAGER_PROXY"); + p.policyManager = vm.envAddress("POLICY_MANAGER_PROXY"); + p.premiumManager = vm.envAddress("PREMIUM_MANAGER_PROXY"); + p.coverPoolFactory = vm.envAddress("COVER_POOL_FACTORY_PROXY"); + p.specRegistry = vm.envAddress("SPEC_REGISTRY_PROXY"); + } + + function _deployImplementations() internal returns (Impls memory i) { + console2.log("--- Deploying implementations ---"); + + i.swapper = address(new Swapper()); + console2.log("Swapper impl: ", i.swapper); + + i.claimManager = address(new ClaimManager()); + console2.log("ClaimManager impl: ", i.claimManager); + + i.policyManager = address(new PolicyManager()); + console2.log("PolicyManager impl: ", i.policyManager); + + i.premiumManager = address(new PremiumManager()); + console2.log("PremiumManager impl: ", i.premiumManager); + + i.coverPoolFactory = address(new CoverPoolFactory()); + console2.log("CoverPoolFactory impl: ", i.coverPoolFactory); + + i.specRegistry = address(new SpecRegistry()); + console2.log("SpecRegistry impl: ", i.specRegistry); + + i.coverPool = address(new CoverPool()); + console2.log("CoverPool clone-target: ", i.coverPool); + + console2.log(""); + } + + function _upgradeProxies(Proxies memory p, Impls memory i, address oraclePriceFeed, uint16 toleranceBps) internal { + console2.log("--- Upgrading proxies ---"); + + // Step 1: Swapper — introduces claimManager slot, lockRouteUntil, PAUSER_ROLE + UUPSUpgradeable(p.swapper).upgradeToAndCall(i.swapper, ""); + console2.log("[1] Swapper upgraded"); + + // Step 2: Wire Swapper.claimManager BEFORE upgrading ClaimManager or PolicyManager. + // Without this, bindPolicy reverts OnlyClaimManager for any cross-token policy. + ISwapperAdmin(p.swapper).setClaimManager(p.claimManager); + console2.log("[2] Swapper.setClaimManager ->", p.claimManager); + + // Step 3: ClaimManager — atomically calls initializeV2 to write oraclePriceFeed + toleranceBps + bytes memory initV2 = abi.encodeCall(ClaimManager.initializeV2, (oraclePriceFeed, toleranceBps)); + UUPSUpgradeable(p.claimManager).upgradeToAndCall(i.claimManager, initV2); + console2.log("[3] ClaimManager upgraded + initializeV2 called"); + + // Step 4: PolicyManager — renames chainlinkPriceFeed -> oraclePriceFeed, new bindPolicy logic + UUPSUpgradeable(p.policyManager).upgradeToAndCall(i.policyManager, ""); + console2.log("[4] PolicyManager upgraded"); + + // Steps 5-7: Remaining proxies (PAUSER_ROLE only, order-independent) + UUPSUpgradeable(p.premiumManager).upgradeToAndCall(i.premiumManager, ""); + console2.log("[5] PremiumManager upgraded"); + + UUPSUpgradeable(p.coverPoolFactory).upgradeToAndCall(i.coverPoolFactory, ""); + console2.log("[6] CoverPoolFactory upgraded"); + + UUPSUpgradeable(p.specRegistry).upgradeToAndCall(i.specRegistry, ""); + console2.log("[7] SpecRegistry upgraded"); + + // Step 8: Update CoverPool clone-target so new pools get CYS3-01 USD-conversion fix. + // Requires CREATOR_ROLE (held by admin/multisig on both Sepolia and mainnet). + // NOTE: Existing pool clones are EIP-1167 minimal proxies and cannot be upgraded. + ICoverPoolFactoryAdmin(p.coverPoolFactory).setCoverPoolImplementation(i.coverPool); + console2.log("[8] CoverPoolFactory.setCoverPoolImplementation ->", i.coverPool); + + console2.log(""); + } + + function _grantPauserRole(Proxies memory p, address pauser) internal { + console2.log("--- Granting PAUSER_ROLE ---"); + + bytes32 pauserRole = keccak256("PAUSER_ROLE"); + + IAccessControl(p.swapper).grantRole(pauserRole, pauser); + console2.log("PAUSER_ROLE granted on Swapper"); + + IAccessControl(p.claimManager).grantRole(pauserRole, pauser); + console2.log("PAUSER_ROLE granted on ClaimManager"); + + IAccessControl(p.policyManager).grantRole(pauserRole, pauser); + console2.log("PAUSER_ROLE granted on PolicyManager"); + + IAccessControl(p.premiumManager).grantRole(pauserRole, pauser); + console2.log("PAUSER_ROLE granted on PremiumManager"); + + IAccessControl(p.coverPoolFactory).grantRole(pauserRole, pauser); + console2.log("PAUSER_ROLE granted on CoverPoolFactory"); + + IAccessControl(p.specRegistry).grantRole(pauserRole, pauser); + console2.log("PAUSER_ROLE granted on SpecRegistry"); + + console2.log(""); + } + + function _logSummary(Proxies memory p, Impls memory i) internal view { + console2.log("=== Upgrade Complete ==="); + console2.log("Chain ID:", block.chainid); + console2.log(""); + console2.log("--- New Implementations ---"); + console2.log("Swapper: proxy=", p.swapper, " impl=", i.swapper); + console2.log("ClaimManager: proxy=", p.claimManager, " impl=", i.claimManager); + console2.log("PolicyManager: proxy=", p.policyManager, " impl=", i.policyManager); + console2.log("PremiumManager: proxy=", p.premiumManager, " impl=", i.premiumManager); + console2.log("CoverPoolFactory: proxy=", p.coverPoolFactory, " impl=", i.coverPoolFactory); + console2.log("SpecRegistry: proxy=", p.specRegistry, " impl=", i.specRegistry); + console2.log("CoverPool target: (clone-target)", i.coverPool); + console2.log(""); + console2.log("Run VerifySepoliaConfig to confirm all wiring is correct:"); + console2.log(" forge script script/VerifySepoliaConfig.s.sol --rpc-url $SEPOLIA_RPC_URL"); + console2.log("===================================="); + } +} diff --git a/script/UpgradeClaimManagerEthereum.s.sol b/script/UpgradeClaimManagerEthereum.s.sol index 908b9998..455506a9 100644 --- a/script/UpgradeClaimManagerEthereum.s.sol +++ b/script/UpgradeClaimManagerEthereum.s.sol @@ -10,17 +10,21 @@ import {UpgradeClaimManager} from "./UpgradeClaimManager.s.sol"; * 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. + * This upgrade atomically calls ClaimManager.initializeV2(oraclePriceFeed, toleranceBps) + * via upgradeToAndCall. On-chain verification confirmed that initializeV2 has NOT been + * applied on the mainnet proxy (slot 9 = 0x0, priceDeviationToleranceBps() reverts). * - * POST-UPGRADE MIGRATION: Bundle grantRole(PAUSER_ROLE, ) in the same - * scheduleBatch as the upgrade. See docs/deployment/Upgrade-PAUSER-ROLE-Mainnet.md. + * POST-UPGRADE MIGRATION: Bundle grantRole(PAUSER_ROLE, ) and + * Swapper.setClaimManager() in the same scheduleBatch as the upgrade. + * See docs/deployment/Upgrade-Ethereum.md for the full 14-call batch. * * Environment Variables: * - DEPLOYER_ADDRESS: Public address of the keystore signer * - CLAIM_MANAGER_PROXY: Address of the deployed ClaimManager proxy (required) * - TIMELOCK: TimelockController address (0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7) + * - ORACLE_PRICE_FEED: Address of the OraclePriceFeed contract (required) + * - PRICE_DEVIATION_TOLERANCE_BPS: Max DEX-vs-oracle deviation tolerance in bps + * (optional, defaults to 300 = 3%) */ contract UpgradeClaimManagerEthereum is UpgradeClaimManager { /** @@ -30,13 +34,4 @@ contract UpgradeClaimManagerEthereum is UpgradeClaimManager { 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/UpgradeSwapper.s.sol b/script/UpgradeSwapper.s.sol index 0983dd43..79ca5e61 100644 --- a/script/UpgradeSwapper.s.sol +++ b/script/UpgradeSwapper.s.sol @@ -18,6 +18,14 @@ import {UpgradeBase} from "./UpgradeBase.s.sol"; * Note: setNativeWrapper() requires whenPaused — the PAUSER_ROLE holder must pause * before admin can call it; and unpause afterwards. * + * MANDATORY POST-UPGRADE WIRING: + * After upgrading, call Swapper.setClaimManager() before ANY new + * policy is bound. Without this, PolicyManager.bindPolicy reverts OnlyClaimManager + * for every policy with cross-token collateral — ClaimManager.validateAndLockCollateralRoutes + * calls Swapper.lockRouteUntil which requires msg.sender == claimManager. Bundle this + * call in the same multisig batch / scheduleBatch as the upgrade. + * Counter-tested: slot 3 stays 0x0 if skipped; lockRouteUntil reverts 0xeb1af97e. + * * Environment Variables: * - PRIVATE_KEY: Deployer private key with DEFAULT_ADMIN_ROLE on Swapper * - SWAPPER_PROXY: Address of the deployed Swapper proxy (required) From b4db4f62ddb7967d0fcf480d731162e787e60463 Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Wed, 20 May 2026 10:18:52 +0200 Subject: [PATCH 2/2] chore: add and update scripts to verify config --- script/VerifyEthereumConfig.s.sol | 408 ++++++++++++++++++++++++++++++ script/VerifySepoliaConfig.s.sol | 104 +++++--- 2 files changed, 480 insertions(+), 32 deletions(-) create mode 100644 script/VerifyEthereumConfig.s.sol diff --git a/script/VerifyEthereumConfig.s.sol b/script/VerifyEthereumConfig.s.sol new file mode 100644 index 00000000..0f819d80 --- /dev/null +++ b/script/VerifyEthereumConfig.s.sol @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Script, console2} from "forge-std/Script.sol"; + +/// @dev Minimal view interfaces — no full imports needed. +interface IAccessControl { + function hasRole(bytes32 role, address account) external view returns (bool); +} + +interface IPausable { + function paused() external view returns (bool); +} + +interface IStakeManagerView { + function coverPoolFactory() external view returns (address); + function router() external view returns (address); +} + +interface ISlashingManagerView { + function claimManager() external view returns (address); + function sspRouter() external view returns (address); + function stakeManager() external view returns (address); +} + +interface IRewardsManagerView { + function premiumManager() external view returns (address); + function sspRouter() external view returns (address); +} + +interface ISSPRouterView { + function stakeManager() external view returns (address); + function rewardsManager() external view returns (address); + function slashingManager() external view returns (address); + function getAdapter(uint8 moduleType) external view returns (address); + function getVaultModule(address vault) external view returns (uint8); +} + +interface IOraclePriceFeedView { + function getUSDValue(address token, uint256 amount) external view returns (uint256); + function hasPriceFeed(address token) external view returns (bool); +} + +interface IAdapterView { + function oraclePriceFeed() external view returns (address); +} + +interface IPolicyManagerView { + function stakeManager() external view returns (address); + function oraclePriceFeed() external view returns (address); +} + +interface IClaimManagerView { + function slashingManager() external view returns (address); + function premiumManager() external view returns (address); + function oraclePriceFeed() external view returns (address); + function priceDeviationToleranceBps() external view returns (uint16); +} + +interface IPremiumManagerView { + function rewardsManager() external view returns (address); + function CLAIM_MANAGER_ROLE() external view returns (bytes32); +} + +interface IERC20Allowance { + function allowance(address owner, address spender) external view returns (uint256); +} + +interface ICoverPoolFactoryView { + function stakeManager() external view returns (address); +} + +interface ISwapperView { + function claimManager() external view returns (address); + function PAUSER_ROLE() external view returns (bytes32); +} + +/** + * @title VerifyEthereumConfig + * @notice Reads and prints the live on-chain configuration of all Ethereum mainnet Coverage + * contracts, verifying every cross-contract wiring and ACL needed for full operation. + * + * @dev Run view-only; no transactions are broadcast. + * + * Checks include all Sepolia checks plus: + * - DEFAULT_ADMIN_ROLE held by TimelockController (not multisig) on coverage proxies + * - OraclePriceFeed WETH feed registered (hasPriceFeed + getUSDValue) + * - ClaimManager V2 storage initialized (priceDeviationToleranceBps > 0) + * - Swapper.claimManager wired (CYS3-06) + * + * Output: + * [OK] – value matches expected + * [FAIL] – misconfigured; must be fixed before operation + * + * Optional env vars: + * PAUSER: When set, also verifies PAUSER_ROLE is granted to this address on all 6 proxies. + * + * Usage: + * forge script script/VerifyEthereumConfig.s.sol --rpc-url $ETHEREUM_RPC_URL + */ +contract VerifyEthereumConfig is Script { + // ── Coverage contracts (canonical mainnet addresses — CreateX deployment) + // ───────────── + address internal constant COVER_POOL_FACTORY = 0x4f3DbB70cD85bcb63303FBa8610Cb163aDDA4E66; + address internal constant POLICY_MANAGER = 0xfb771BE75365D2a1D32be198e03ce8a2125e2699; + address internal constant CLAIM_MANAGER = 0x3ceE181C3E78fB9968f0Fb0935d2db0723B9Cb45; + address internal constant PREMIUM_MANAGER = 0xEc7322D6754709d5001B710ec4fB2547a89B3aD1; + address internal constant SPEC_REGISTRY = 0x90EfF742958dd4c54ede3ED365375a14077D0A58; + address internal constant SWAPPER = 0x148Bfe2330cEe4f227addb47736a105DB427dc31; + + // ── Core contracts (canonical mainnet addresses — CreateX deployment) + // ───────────────── + address internal constant STAKE_MANAGER = 0x5be5220F81e76e0CF6089fb7E7aE9eF48a8D64Be; + address internal constant SLASHING_MANAGER = 0x7Bc39bf135eF3c30E542C196719c91455ff489f0; + address internal constant REWARDS_MANAGER = 0x8ae0F0B94fe782D7F055B04d4699c3cc01632b46; + address internal constant SSP_ROUTER = 0xF39E592E93A7a925a57464e0120B555A25590486; + address internal constant EIGEN_ADAPTER = 0xe6168092892E545701D92BEe10149178bE0EEDF4; + address internal constant ORACLE_PRICE_FEED = 0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16; + + // ── Governance + // ──────────────────────────────────────────────────────────────────────── + address internal constant TIMELOCK = 0x776Ab5890b6c62544dF06471A3705Ed83BeEA2f7; + address internal constant MULTISIG = 0xd2d03377Fa96687e9C11380DA9956AcC5F307e2c; + + // ── Reference token (mainnet WETH) + // ──────────────────────────────────────────────────── + address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + // ── Roles + // ───────────────────────────────────────────────────────────────────────────── + bytes32 internal constant DEFAULT_ADMIN_ROLE = bytes32(0); + bytes32 internal constant POLICY_MANAGER_ROLE = keccak256("POLICY_MANAGER_ROLE"); + bytes32 internal constant CLAIM_MANAGER_ROLE = keccak256("CLAIM_MANAGER_ROLE"); + bytes32 internal constant PREMIUM_MANAGER_ROLE = keccak256("PREMIUM_MANAGER_ROLE"); + bytes32 internal constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + // ── SSPRouter module type + // ───────────────────────────────────────────────────────────── + uint8 internal constant MODULE_EIGENLAYER = 2; + + uint256 internal _failures; + + function run() external { + require(block.chainid == 1, "Must run on Ethereum mainnet"); + + console2.log("=== VerifyEthereumConfig ==="); + console2.log("Block:", block.number); + console2.log(""); + + _checkAdminModel(); + _checkFlow1_createCoverPool(); + _checkFlow2_3_requestCoverage(); + _checkFlow4_prepareBindPolicy(); + _checkFlow5_bindPolicy(); + _checkFlow6_7_distributePremium(); + _checkFlow8_fileClaim(); + + address pauser = vm.envOr("PAUSER", address(0)); + if (pauser != address(0)) { + _checkPauserRole(pauser); + } + + console2.log(""); + if (_failures == 0) { + console2.log("[ALL OK] All checks passed. Mainnet configuration is correct."); + } else { + console2.log("[INCOMPLETE]", _failures, "check(s) failed. See [FAIL] lines above."); + } + console2.log("========================================="); + } + + // ───────────────────────────────────────────────────────────────────────── + // Admin model + // Coverage proxies: TimelockController holds DEFAULT_ADMIN_ROLE + // Core proxies: multisig holds DEFAULT_ADMIN_ROLE directly (no Timelock) + // ───────────────────────────────────────────────────────────────────────── + + function _checkAdminModel() internal { + console2.log("--- Admin Model ---"); + + address[6] memory covProxies = + [SWAPPER, CLAIM_MANAGER, POLICY_MANAGER, PREMIUM_MANAGER, COVER_POOL_FACTORY, SPEC_REGISTRY]; + string[6] memory covNames = + ["Swapper", "ClaimManager", "PolicyManager", "PremiumManager", "CoverPoolFactory", "SpecRegistry"]; + + for (uint256 i = 0; i < covProxies.length; ++i) { + _checkRole( + string.concat(covNames[i], ": DEFAULT_ADMIN_ROLE -> Timelock"), + covProxies[i], + DEFAULT_ADMIN_ROLE, + TIMELOCK + ); + } + + // Core: DEFAULT_ADMIN_ROLE on multisig (no Timelock for core — confirmed on-chain) + _checkRole( + "SlashingManager: DEFAULT_ADMIN_ROLE -> Multisig (core has no Timelock)", + SLASHING_MANAGER, + DEFAULT_ADMIN_ROLE, + MULTISIG + ); + + console2.log(""); + } + + // ───────────────────────────────────────────────────────────────────────── + // Flow 1 – CreateCoverPool + // ───────────────────────────────────────────────────────────────────────── + + function _checkFlow1_createCoverPool() internal { + console2.log("--- Flow 1: CreateCoverPool ---"); + + _checkFalse("CoverPoolFactory not paused", IPausable(COVER_POOL_FACTORY).paused()); + _check("CoverPoolFactory.stakeManager", ICoverPoolFactoryView(COVER_POOL_FACTORY).stakeManager(), STAKE_MANAGER); + + console2.log(""); + } + + // ───────────────────────────────────────────────────────────────────────── + // Flows 2-3 – RequestCoverage + // ───────────────────────────────────────────────────────────────────────── + + function _checkFlow2_3_requestCoverage() internal { + console2.log("--- Flows 2-3: RequestCoverage ---"); + + _check("PolicyManager.stakeManager", IPolicyManagerView(POLICY_MANAGER).stakeManager(), STAKE_MANAGER); + _checkRole( + "StakeManager: POLICY_MANAGER_ROLE -> PolicyManager", STAKE_MANAGER, POLICY_MANAGER_ROLE, POLICY_MANAGER + ); + _check("StakeManager.coverPoolFactory", IStakeManagerView(STAKE_MANAGER).coverPoolFactory(), COVER_POOL_FACTORY); + _check("StakeManager.router", IStakeManagerView(STAKE_MANAGER).router(), SSP_ROUTER); + _check( + "SSPRouter.getAdapter(EIGENLAYER)", ISSPRouterView(SSP_ROUTER).getAdapter(MODULE_EIGENLAYER), EIGEN_ADAPTER + ); + + console2.log(""); + } + + // ───────────────────────────────────────────────────────────────────────── + // Flow 4 – PrepareBindPolicy + // ───────────────────────────────────────────────────────────────────────── + + function _checkFlow4_prepareBindPolicy() internal { + console2.log("--- Flow 4: PrepareBindPolicy ---"); + + _check( + "SSPRouter.getAdapter(EIGENLAYER)", ISSPRouterView(SSP_ROUTER).getAdapter(MODULE_EIGENLAYER), EIGEN_ADAPTER + ); + _check("EigenAdapter.oraclePriceFeed", IAdapterView(EIGEN_ADAPTER).oraclePriceFeed(), ORACLE_PRICE_FEED); + + console2.log(""); + } + + // ───────────────────────────────────────────────────────────────────────── + // Flow 5 – BindPolicy + // ───────────────────────────────────────────────────────────────────────── + + function _checkFlow5_bindPolicy() internal { + console2.log("--- Flow 5: BindPolicy ---"); + + _check("PolicyManager.oraclePriceFeed", IPolicyManagerView(POLICY_MANAGER).oraclePriceFeed(), ORACLE_PRICE_FEED); + _check("Swapper.claimManager", ISwapperView(SWAPPER).claimManager(), CLAIM_MANAGER); + + console2.log(""); + } + + // ───────────────────────────────────────────────────────────────────────── + // Flows 6-7 – DistributePremium + // ───────────────────────────────────────────────────────────────────────── + + function _checkFlow6_7_distributePremium() internal { + console2.log("--- Flows 6-7: DistributePremium ---"); + + _check("PremiumManager.rewardsManager", IPremiumManagerView(PREMIUM_MANAGER).rewardsManager(), REWARDS_MANAGER); + + uint256 allowance = IERC20Allowance(WETH).allowance(PREMIUM_MANAGER, REWARDS_MANAGER); + if (allowance == type(uint256).max) { + console2.log("[OK] PremiumManager.allowance(WETH, RewardsManager): unlimited"); + } else if (allowance > 0) { + console2.log("[OK] PremiumManager.allowance(WETH, RewardsManager):", allowance); + } else { + console2.log("[FAIL] PremiumManager has not approved RewardsManager for WETH"); + _failures++; + } + _checkRole( + "RewardsManager: PREMIUM_MANAGER_ROLE -> PremiumManager", + REWARDS_MANAGER, + PREMIUM_MANAGER_ROLE, + PREMIUM_MANAGER + ); + _check("RewardsManager.premiumManager", IRewardsManagerView(REWARDS_MANAGER).premiumManager(), PREMIUM_MANAGER); + _check("RewardsManager.sspRouter", IRewardsManagerView(REWARDS_MANAGER).sspRouter(), SSP_ROUTER); + _check("SSPRouter.rewardsManager", ISSPRouterView(SSP_ROUTER).rewardsManager(), REWARDS_MANAGER); + + console2.log(""); + } + + // ───────────────────────────────────────────────────────────────────────── + // Flow 8 – FileClaim + // ───────────────────────────────────────────────────────────────────────── + + function _checkFlow8_fileClaim() internal { + console2.log("--- Flow 8: FileClaim ---"); + + _check("ClaimManager.slashingManager", IClaimManagerView(CLAIM_MANAGER).slashingManager(), SLASHING_MANAGER); + _check("ClaimManager.premiumManager", IClaimManagerView(CLAIM_MANAGER).premiumManager(), PREMIUM_MANAGER); + _check("ClaimManager.oraclePriceFeed", IClaimManagerView(CLAIM_MANAGER).oraclePriceFeed(), ORACLE_PRICE_FEED); + + uint16 toleranceBps = IClaimManagerView(CLAIM_MANAGER).priceDeviationToleranceBps(); + if (toleranceBps > 0) { + console2.log("[OK] ClaimManager.priceDeviationToleranceBps:", toleranceBps, "bps"); + } else { + console2.log("[FAIL] ClaimManager.priceDeviationToleranceBps is 0 -- initializeV2 not called"); + _failures++; + } + + _checkRole( + "PremiumManager: CLAIM_MANAGER_ROLE -> ClaimManager", + PREMIUM_MANAGER, + IPremiumManagerView(PREMIUM_MANAGER).CLAIM_MANAGER_ROLE(), + CLAIM_MANAGER + ); + _checkRole( + "SlashingManager: CLAIM_MANAGER_ROLE -> ClaimManager", SLASHING_MANAGER, CLAIM_MANAGER_ROLE, CLAIM_MANAGER + ); + _check("SlashingManager.claimManager", ISlashingManagerView(SLASHING_MANAGER).claimManager(), CLAIM_MANAGER); + _check("SlashingManager.sspRouter", ISlashingManagerView(SLASHING_MANAGER).sspRouter(), SSP_ROUTER); + _check("SlashingManager.stakeManager", ISlashingManagerView(SLASHING_MANAGER).stakeManager(), STAKE_MANAGER); + _check("SSPRouter.slashingManager", ISSPRouterView(SSP_ROUTER).slashingManager(), SLASHING_MANAGER); + _check("SSPRouter.stakeManager", ISSPRouterView(SSP_ROUTER).stakeManager(), STAKE_MANAGER); + + // Mainnet: WETH feed must be registered and returning a non-zero price + bool hasFeed = IOraclePriceFeedView(ORACLE_PRICE_FEED).hasPriceFeed(WETH); + if (hasFeed) { + console2.log("[OK] OraclePriceFeed.hasPriceFeed(WETH): true"); + } else { + console2.log("[FAIL] OraclePriceFeed.hasPriceFeed(WETH): false -- WETH feed not registered"); + _failures++; + } + + try IOraclePriceFeedView(ORACLE_PRICE_FEED).getUSDValue(WETH, 1e18) returns (uint256 usdValue) { + if (usdValue > 0) { + console2.log("[OK] OraclePriceFeed.getUSDValue(WETH, 1e18):", usdValue, "(USD, 8 dec)"); + } else { + console2.log("[FAIL] OraclePriceFeed.getUSDValue(WETH) returned 0"); + _failures++; + } + } catch { + console2.log("[FAIL] OraclePriceFeed.getUSDValue(WETH) reverted (stale or missing feed)"); + _failures++; + } + + console2.log(""); + } + + // ───────────────────────────────────────────────────────────────────────── + // PAUSER_ROLE — optional, enabled when PAUSER env var is set + // ───────────────────────────────────────────────────────────────────────── + + function _checkPauserRole(address pauser) internal { + console2.log("--- PAUSER_ROLE checks (PAUSER =", pauser, ") ---"); + + address[6] memory proxies = + [SWAPPER, CLAIM_MANAGER, POLICY_MANAGER, PREMIUM_MANAGER, COVER_POOL_FACTORY, SPEC_REGISTRY]; + string[6] memory names = + ["Swapper", "ClaimManager", "PolicyManager", "PremiumManager", "CoverPoolFactory", "SpecRegistry"]; + + for (uint256 i = 0; i < proxies.length; ++i) { + _checkRole(string.concat(names[i], ": PAUSER_ROLE -> pauser"), proxies[i], PAUSER_ROLE, pauser); + } + + console2.log(""); + } + + // ───────────────────────────────────────────────────────────────────────── + // Helpers + // ───────────────────────────────────────────────────────────────────────── + + function _check(string memory label, address got, address expected) internal { + if (got == expected) { + console2.log("[OK]", label, ":", got); + } else { + console2.log("[FAIL]", label); + console2.log(" expected:", expected); + console2.log(" got: ", got); + _failures++; + } + } + + function _checkRole(string memory label, address contractAddr, bytes32 role, address account) internal { + bool granted = IAccessControl(contractAddr).hasRole(role, account); + if (granted) { + console2.log("[OK]", label); + } else { + console2.log("[FAIL]", label, "(role not granted)"); + _failures++; + } + } + + function _checkFalse(string memory label, bool condition) internal { + if (!condition) { + console2.log("[OK]", label); + } else { + console2.log("[FAIL]", label, "(should be false)"); + _failures++; + } + } +} diff --git a/script/VerifySepoliaConfig.s.sol b/script/VerifySepoliaConfig.s.sol index 03120d74..362ec98e 100644 --- a/script/VerifySepoliaConfig.s.sol +++ b/script/VerifySepoliaConfig.s.sol @@ -70,6 +70,11 @@ interface ICoverPoolFactoryView { function stakeManager() external view returns (address); } +interface ISwapperView { + function claimManager() external view returns (address); + function PAUSER_ROLE() external view returns (bytes32); +} + /** * @title VerifySepoliaConfig * @notice Reads and prints the live on-chain configuration of all Sepolia contracts, @@ -81,48 +86,56 @@ interface ICoverPoolFactoryView { * Flow 1 – CreateCoverPool: CoverPoolFactory not paused, stakeManager wired * Flow 2-3– RequestCoverage: StakeManager roles + wiring, SSPRouter adapters * Flow 4 – PrepareBindPolicy: Adapter oraclePriceFeed, SSPRouter adapters - * Flow 5 – BindPolicy: SSPRouter vault module registration + * Flow 5 – BindPolicy: PolicyManager oraclePriceFeed, vault module, Swapper wiring * Flow 6-7– DistributePremium: RewardsManager wiring + PREMIUM_MANAGER_ROLE - * Flow 8 – FileClaim: SlashingManager wiring + ClaimManager↔PremiumManager + * Flow 8 – FileClaim: SlashingManager wiring + ClaimManager state + OraclePriceFeed * * Output: * [OK] – value matches expected * [FAIL] – misconfigured; must be fixed before testing * + * Optional env vars: + * PAUSER: When set, also verifies PAUSER_ROLE is granted to this address on all 6 proxies. + * * Usage: * forge script script/VerifySepoliaConfig.s.sol --rpc-url $SEPOLIA_RPC_URL */ contract VerifySepoliaConfig is Script { - // ── Coverage contracts - // ────────────────────────────────────────────────── - address internal constant COVER_POOL_FACTORY = 0x43c1AC362f88f461d2CF358D6fF76C833585B830; - address internal constant POLICY_MANAGER = 0x48Ded2CB83623d5dE7D51Eab045A452bA70C2A35; - address internal constant CLAIM_MANAGER = 0x568191F923C10A5447d8349eFD63687836D52099; - address internal constant PREMIUM_MANAGER = 0xD75357f1AA43d87edA20A128D1A525FF320cB743; - - // ── Core contracts - // ────────────────────────────────────────────────────── - address internal constant STAKE_MANAGER = 0xF26d310D98b9eB0B20d96B605d9Af15bC31f5c32; - address internal constant SLASHING_MANAGER = 0xF397bbFDB272b31366D472Fcdbf4DC0982ec8714; - address internal constant REWARDS_MANAGER = 0x6A0823fC61F970BB8a52cF95BfB8F32265Ada8b8; - address internal constant SSP_ROUTER = 0xf68c72E92f9BdB7Cdba2AF4026Bab8DC22781148; - address internal constant EIGEN_ADAPTER = 0xB4bB09dcE1C1D9d987D5dc0dC2c8770D4fbBD49b; - address internal constant ORACLE_PRICE_FEED = 0x2b48F18f73C66075168b727524c99aFE12814417; - - // ── Known E2E vault (EigenLayer wstETH DurationVaultStrategy, deployed by SSPRouter) ── + // ── Coverage contracts (canonical Sepolia addresses — CreateX deployment) + // ────────────── + address internal constant COVER_POOL_FACTORY = 0x4f3DbB70cD85bcb63303FBa8610Cb163aDDA4E66; + address internal constant POLICY_MANAGER = 0xfb771BE75365D2a1D32be198e03ce8a2125e2699; + address internal constant CLAIM_MANAGER = 0x3ceE181C3E78fB9968f0Fb0935d2db0723B9Cb45; + address internal constant PREMIUM_MANAGER = 0xEc7322D6754709d5001B710ec4fB2547a89B3aD1; + address internal constant SPEC_REGISTRY = 0x90EfF742958dd4c54ede3ED365375a14077D0A58; + address internal constant SWAPPER = 0x148Bfe2330cEe4f227addb47736a105DB427dc31; + + // ── Core contracts (canonical Sepolia addresses — CreateX deployment) + // ───────────────── + address internal constant STAKE_MANAGER = 0x5be5220F81e76e0CF6089fb7E7aE9eF48a8D64Be; + address internal constant SLASHING_MANAGER = 0x7Bc39bf135eF3c30E542C196719c91455ff489f0; + address internal constant REWARDS_MANAGER = 0x8ae0F0B94fe782D7F055B04d4699c3cc01632b46; + address internal constant SSP_ROUTER = 0xF39E592E93A7a925a57464e0120B555A25590486; + address internal constant EIGEN_ADAPTER = 0xe6168092892E545701D92BEe10149178bE0EEDF4; + address internal constant ORACLE_PRICE_FEED = 0x57F750E3B8e095A5b3AE96030F0cac5dFe2f8A16; + + // ── Known E2E vault (EigenLayer wstETH DurationVaultStrategy) + // ──────────────────────── address internal constant WSTETH_EL_STRATEGY = 0x7db0C347645bE3E19Af87d72E6220361cdD2428D; // ── Reference token - // ───────────────────────────────────────────────────── + // ─────────────────────────────────────────────────────────────────── address internal constant WETH = 0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9; // ── Roles - // ──────────────────────────────────────────────────────────────── + // ───────────────────────────────────────────────────────────────────────────── bytes32 internal constant POLICY_MANAGER_ROLE = keccak256("POLICY_MANAGER_ROLE"); bytes32 internal constant CLAIM_MANAGER_ROLE = keccak256("CLAIM_MANAGER_ROLE"); bytes32 internal constant PREMIUM_MANAGER_ROLE = keccak256("PREMIUM_MANAGER_ROLE"); + bytes32 internal constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); - // ── SSPRouter module type enum values (ISSPRouter.SSPModuleType) ───────── + // ── SSPRouter module type (ISSPRouter.SSPModuleType) + // ───────────────────────────────── uint8 internal constant MODULE_EIGENLAYER = 2; uint256 internal _failures; @@ -141,19 +154,23 @@ contract VerifySepoliaConfig is Script { _checkFlow6_7_distributePremium(); _checkFlow8_fileClaim(); + address pauser = vm.envOr("PAUSER", address(0)); + if (pauser != address(0)) { + _checkPauserRole(pauser); + } + console2.log(""); if (_failures == 0) { - console2.log("[ALL OK] All", uint256(27), "checks passed. Configuration is complete for E2E testing."); + console2.log("[ALL OK] All checks passed. Configuration is complete for E2E testing."); } else { console2.log("[INCOMPLETE]", _failures, "check(s) failed. See [FAIL] lines above."); - console2.log(" Fix: run cast send commands in docs/deployment/CoreAdminActions-Sepolia.md"); } console2.log("========================================="); } // ───────────────────────────────────────────────────────────────────────── // Flow 1 – CreateCoverPool - // Requires: CoverPoolFactory not paused, stakeManager set to new core + // Requires: CoverPoolFactory not paused, stakeManager wired // ───────────────────────────────────────────────────────────────────────── function _checkFlow1_createCoverPool() internal { @@ -179,7 +196,6 @@ contract VerifySepoliaConfig is Script { ); _check("StakeManager.coverPoolFactory", IStakeManagerView(STAKE_MANAGER).coverPoolFactory(), COVER_POOL_FACTORY); _check("StakeManager.router", IStakeManagerView(STAKE_MANAGER).router(), SSP_ROUTER); - // SSPRouter must have EigenLayer adapter so createCommittee can deploy operator sets _check( "SSPRouter.getAdapter(EIGENLAYER)", ISSPRouterView(SSP_ROUTER).getAdapter(MODULE_EIGENLAYER), EIGEN_ADAPTER ); @@ -205,7 +221,8 @@ contract VerifySepoliaConfig is Script { // ───────────────────────────────────────────────────────────────────────── // Flow 5 – BindPolicy - // Requires: vault module registered in SSPRouter (for addCommitteeVaults) + // Requires: vault module registered, PolicyManager oraclePriceFeed, + // Swapper.claimManager set (CYS3-06) // ───────────────────────────────────────────────────────────────────────── function _checkFlow5_bindPolicy() internal { @@ -213,7 +230,6 @@ contract VerifySepoliaConfig is Script { _check("PolicyManager.oraclePriceFeed", IPolicyManagerView(POLICY_MANAGER).oraclePriceFeed(), ORACLE_PRICE_FEED); - // addCommitteeVaults checks getVaultModule != NONE for each vault uint8 module = ISSPRouterView(SSP_ROUTER).getVaultModule(WSTETH_EL_STRATEGY); if (module == MODULE_EIGENLAYER) { console2.log("[OK] SSPRouter.getVaultModule(WSTETH_EL_STRATEGY): EIGENLAYER (2)"); @@ -226,13 +242,15 @@ contract VerifySepoliaConfig is Script { _failures++; } + // CYS3-06: Swapper must have claimManager set so lockRouteUntil works during bindPolicy + _check("Swapper.claimManager", ISwapperView(SWAPPER).claimManager(), CLAIM_MANAGER); + console2.log(""); } // ───────────────────────────────────────────────────────────────────────── // Flows 6-7 – DistributePremium - // Requires: PremiumManager → RewardsManager address + PREMIUM_MANAGER_ROLE - // Requires: PremiumManager has approved RewardsManager for WETH (via approveSpender) + // Requires: PremiumManager → RewardsManager + PREMIUM_MANAGER_ROLE + approval // ───────────────────────────────────────────────────────────────────────── function _checkFlow6_7_distributePremium() internal { @@ -265,8 +283,8 @@ contract VerifySepoliaConfig is Script { // ───────────────────────────────────────────────────────────────────────── // Flow 8 – FileClaim - // Requires: ClaimManager → SlashingManager address + CLAIM_MANAGER_ROLE, - // SlashingManager → SSPRouter + StakeManager + // Requires: ClaimManager V2 state (oraclePriceFeed + priceDeviationToleranceBps), + // SlashingManager wiring, OraclePriceFeed feed registered // ───────────────────────────────────────────────────────────────────────── function _checkFlow8_fileClaim() internal { @@ -274,6 +292,8 @@ contract VerifySepoliaConfig is Script { _check("ClaimManager.slashingManager", IClaimManagerView(CLAIM_MANAGER).slashingManager(), SLASHING_MANAGER); _check("ClaimManager.premiumManager", IClaimManagerView(CLAIM_MANAGER).premiumManager(), PREMIUM_MANAGER); + + // CYS3-03: V2 storage fields — both must be non-zero after initializeV2 _check("ClaimManager.oraclePriceFeed", IClaimManagerView(CLAIM_MANAGER).oraclePriceFeed(), ORACLE_PRICE_FEED); uint16 toleranceBps = IClaimManagerView(CLAIM_MANAGER).priceDeviationToleranceBps(); if (toleranceBps > 0) { @@ -282,6 +302,7 @@ contract VerifySepoliaConfig is Script { console2.log("[FAIL] ClaimManager.priceDeviationToleranceBps is 0 -- initializeV2 not called?"); _failures++; } + _checkRole( "PremiumManager: CLAIM_MANAGER_ROLE -> ClaimManager", PREMIUM_MANAGER, @@ -312,6 +333,25 @@ contract VerifySepoliaConfig is Script { console2.log(""); } + // ───────────────────────────────────────────────────────────────────────── + // PAUSER_ROLE — optional, enabled when PAUSER env var is set + // ───────────────────────────────────────────────────────────────────────── + + function _checkPauserRole(address pauser) internal { + console2.log("--- PAUSER_ROLE checks (PAUSER =", pauser, ") ---"); + + address[6] memory proxies = + [SWAPPER, CLAIM_MANAGER, POLICY_MANAGER, PREMIUM_MANAGER, COVER_POOL_FACTORY, SPEC_REGISTRY]; + string[6] memory names = + ["Swapper", "ClaimManager", "PolicyManager", "PremiumManager", "CoverPoolFactory", "SpecRegistry"]; + + for (uint256 i = 0; i < proxies.length; ++i) { + _checkRole(string.concat(names[i], ": PAUSER_ROLE -> pauser"), proxies[i], PAUSER_ROLE, pauser); + } + + console2.log(""); + } + // ───────────────────────────────────────────────────────────────────────── // Helpers // ─────────────────────────────────────────────────────────────────────────