From 67927b4660ec7ca89e332792d9e1b1fefddf0252 Mon Sep 17 00:00:00 2001 From: gfournieriExec Date: Thu, 20 Nov 2025 17:38:24 +0100 Subject: [PATCH 1/8] feat: Add emergency role transfer workflow and scripts --- .github/workflows/emergency-role-transfer.yml | 120 +++++ Makefile | 40 ++ script/TestEmergencyRoleTransferOnFork.s.sol | 428 ++++++++++++++++++ script/TransferAllRoles.s.sol | 268 +++++++++++ test/units/TransferAllRolesScript.t.sol | 262 +++++++++++ 5 files changed, 1118 insertions(+) create mode 100644 .github/workflows/emergency-role-transfer.yml create mode 100644 script/TestEmergencyRoleTransferOnFork.s.sol create mode 100644 script/TransferAllRoles.s.sol create mode 100644 test/units/TransferAllRolesScript.t.sol diff --git a/.github/workflows/emergency-role-transfer.yml b/.github/workflows/emergency-role-transfer.yml new file mode 100644 index 00000000..7391a6b9 --- /dev/null +++ b/.github/workflows/emergency-role-transfer.yml @@ -0,0 +1,120 @@ +name: Emergency Role Transfer + +on: + workflow_dispatch: + inputs: + step: + description: 'Transfer step to execute' + required: true + type: choice + options: + - grant-roles-begin-transfer + - accept-admin-revoke-old + default: grant-roles-begin-transfer + network_type: + description: 'Network type to transfer roles on' + required: true + type: choice + options: + - testnets + - sepolia-only + - arbitrum_sepolia-only + default: testnets + old_address: + description: 'Old/compromised address (for both steps)' + required: true + type: string + default: '0x9990cfb1Feb7f47297F54bef4d4EbeDf6c5463a3' + new_address: + description: 'New secure address (only needed for step 1)' + required: false + type: string + +jobs: + setup-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Set matrix based on network type + id: set-matrix + run: | + case "${{ github.event.inputs.network_type }}" in + testnets) + MATRIX='["sepolia", "arbitrum_sepolia"]' + ;; + sepolia-only) + MATRIX='["sepolia"]' + ;; + arbitrum_sepolia-only) + MATRIX='["arbitrum_sepolia"]' + ;; + esac + echo "matrix=$MATRIX" >> $GITHUB_OUTPUT + + transfer-roles: + needs: setup-matrix + runs-on: ubuntu-latest + strategy: + matrix: + network: ${{ fromJson(needs.setup-matrix.outputs.matrix) }} + fail-fast: false + environment: ${{ matrix.network }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: v1.4.4 + cache: true + + - name: Validate inputs + run: | + if [ "${{ inputs.step }}" = "grant-roles-begin-transfer" ] && [ -z "${{ inputs.new_address }}" ]; then + echo "Error: new_address is required for grant-roles-begin-transfer step" + exit 1 + fi + + - name: Execute Step 1 - Grant Roles and Begin Transfer + if: inputs.step == 'grant-roles-begin-transfer' + env: + ADMIN_PRIVATE_KEY: ${{ secrets.ADMIN_PRIVATE_KEY }} + CHAIN: ${{ matrix.network }} + RPC_URL: ${{ secrets.RPC_URL }} + OLD_ADDRESS: ${{ inputs.old_address }} + NEW_ADDRESS: ${{ inputs.new_address }} + run: | + echo "Executing Step 1: Grant roles and begin admin transfer" + echo "Chain: $CHAIN" + echo "Old address: $OLD_ADDRESS" + echo "New address: $NEW_ADDRESS" + make grant-roles-begin-transfer + + - name: Execute Step 2 - Accept Admin and Revoke Old Roles + if: inputs.step == 'accept-admin-revoke-old' + env: + NEW_ADMIN_PRIVATE_KEY: ${{ secrets.NEW_ADMIN_PRIVATE_KEY }} + CHAIN: ${{ matrix.network }} + RPC_URL: ${{ secrets.RPC_URL }} + OLD_ADDRESS: ${{ inputs.old_address }} + run: | + echo "Executing Step 2: Accept admin role and revoke old roles" + echo "Chain: $CHAIN" + echo "Old address to revoke: $OLD_ADDRESS" + make accept-admin-revoke-old + + - name: Summary + run: | + if [ "${{ inputs.step }}" = "grant-roles-begin-transfer" ]; then + echo "✅ Step 1 Complete on ${{ matrix.network }}" + echo "⏰ Wait for the delay period to pass before running Step 2" + echo "📝 Next: Run this workflow again with step=accept-admin-revoke-old" + else + echo "✅ Step 2 Complete on ${{ matrix.network }}" + echo "🎉 All roles have been transferred!" + echo "🔒 Old address has been completely removed from all contracts" + fi diff --git a/Makefile b/Makefile index 47793fff..0bb9d314 100644 --- a/Makefile +++ b/Makefile @@ -201,3 +201,43 @@ accept-default-admin-transfer: # CHAIN, RPC_URL $$(if [ "$(CI)" = "true" ]; then echo "--private-key $(NEW_DEFAULT_ADMIN_PRIVATE_KEY)"; else echo "--account $(ACCOUNT)"; fi) \ --broadcast \ -vvv + +# +# Emergency role transfer operations (for compromised addresses) +# + +# Step 1: Grant all roles to new address and begin admin transfer (run with compromised/old address) +grant-roles-begin-transfer: # CHAIN, RPC_URL, OLD_ADDRESS, NEW_ADDRESS + @echo "Step 1: Granting all roles to new address and beginning admin transfer on $(CHAIN)" + @echo "Old address: $(OLD_ADDRESS)" + @echo "New address: $(NEW_ADDRESS)" + CHAIN=$(CHAIN) OLD_ADDRESS=$(OLD_ADDRESS) NEW_ADDRESS=$(NEW_ADDRESS) \ + forge script script/TransferAllRoles.s.sol:GrantRolesAndBeginAdminTransfer \ + --rpc-url $(RPC_URL) \ + $$(if [ "$(CI)" = "true" ]; then echo "--private-key $(ADMIN_PRIVATE_KEY)"; else echo "--account $(ACCOUNT)"; fi) \ + --broadcast \ + -vvv + +# Step 2: Accept admin role and revoke all roles from old address (run with NEW address) +accept-admin-revoke-old: # CHAIN, RPC_URL, OLD_ADDRESS + @echo "Step 2: Accepting admin role and revoking all roles from old address on $(CHAIN)" + @echo "Old address to revoke: $(OLD_ADDRESS)" + CHAIN=$(CHAIN) OLD_ADDRESS=$(OLD_ADDRESS) \ + forge script script/TransferAllRoles.s.sol:AcceptAdminRoleAndRevokeOldRoles \ + --rpc-url $(RPC_URL) \ + $$(if [ "$(CI)" = "true" ]; then echo "--private-key $(NEW_ADMIN_PRIVATE_KEY)"; else echo "--account $(ACCOUNT)"; fi) \ + --broadcast \ + -vvv + +# +# Testing emergency role transfer on fork +# + +# Test the complete emergency role transfer process on a fork +test-emergency-transfer-on-fork: + @echo "Testing emergency role transfer on Arbitrum Sepolia fork..." + @echo "Make sure you have a fork running: anvil --fork-url \$$ARBITRUM_SEPOLIA_RPC_URL --port 8546" + forge script script/TestEmergencyRoleTransferOnFork.s.sol:TestEmergencyRoleTransferOnFork \ + --rpc-url http://localhost:8546 \ + --broadcast \ + -vvvv diff --git a/script/TestEmergencyRoleTransferOnFork.s.sol b/script/TestEmergencyRoleTransferOnFork.s.sol new file mode 100644 index 00000000..58b1792d --- /dev/null +++ b/script/TestEmergencyRoleTransferOnFork.s.sol @@ -0,0 +1,428 @@ +// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.22; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { + IAccessControlDefaultAdminRules +} from "@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol"; +import {RLCCrosschainToken} from "../src/RLCCrosschainToken.sol"; +import {RLCLiquidityUnifier} from "../src/RLCLiquidityUnifier.sol"; +import {IexecLayerZeroBridge} from "../src/bridges/layerZero/IexecLayerZeroBridge.sol"; +import {ConfigLib} from "./lib/ConfigLib.sol"; + +/** + * @title TestEmergencyRoleTransferOnFork + * @dev Script to test the emergency role transfer on a fork of Ethereum Sepolia or Arbitrum Sepolia + * + * This script simulates the complete role transfer process on a local fork: + * 1. Verifies compromised address has all roles + * 2. Grants roles to new address and begins admin transfer + * 3. Fast forwards past delay period + * 4. Accepts admin role and revokes old roles + * 5. Verifies new address has all roles and old address has none + * + * Usage: + * # For Arbitrum Sepolia: + * # Start fork in terminal 1: + * anvil --fork-url $ARBITRUM_SEPOLIA_RPC_URL --port 8546 + * + * # Run test in terminal 2: + * CHAIN=arbitrum_sepolia forge script script/TestEmergencyRoleTransferOnFork.s.sol:TestEmergencyRoleTransferOnFork \ + * --rpc-url http://localhost:8546 \ + * --broadcast \ + * -vv + * + * # For Ethereum Sepolia: + * # Start fork in terminal 1: + * anvil --fork-url $SEPOLIA_RPC_URL --port 8546 + * + * # Run test in terminal 2: + * CHAIN=sepolia forge script script/TestEmergencyRoleTransferOnFork.s.sol:TestEmergencyRoleTransferOnFork \ + * --rpc-url http://localhost:8546 \ + * --broadcast \ + * -vv + */ +contract TestEmergencyRoleTransferOnFork is Script { + // Contract instances (will be initialized based on chain) + IAccessControlDefaultAdminRules public tokenContract; + IexecLayerZeroBridge public iexecLayerZeroBridge; + + // Chain configuration + string public chain; + bool public isApprovalRequired; + + // Compromised address (has all roles) + address public constant OLD_ADDRESS = 0x9990cfb1Feb7f47297F54bef4d4EbeDf6c5463a3; + + // New secure address for testing + address public newAddress; + + // Role identifiers + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant TOKEN_BRIDGE_ROLE = keccak256("TOKEN_BRIDGE_ROLE"); + + function run() external { + // Get chain from environment (defaults to arbitrum_sepolia for backward compatibility) + chain = vm.envOr("CHAIN", string("arbitrum_sepolia")); + + // Load config + ConfigLib.CommonConfigParams memory params = ConfigLib.readCommonConfig(chain); + isApprovalRequired = params.approvalRequired; + + // Initialize contracts based on chain + if (isApprovalRequired) { + // Ethereum Sepolia: Use RLCLiquidityUnifier + tokenContract = IAccessControlDefaultAdminRules(params.rlcLiquidityUnifierAddress); + console.log("Using RLCLiquidityUnifier at:", params.rlcLiquidityUnifierAddress); + } else { + // Arbitrum Sepolia: Use RLCCrosschainToken + tokenContract = IAccessControlDefaultAdminRules(params.rlcCrosschainTokenAddress); + console.log("Using RLCCrosschainToken at:", params.rlcCrosschainTokenAddress); + } + + iexecLayerZeroBridge = IexecLayerZeroBridge(params.iexecLayerZeroBridgeAddress); + console.log("Using IexecLayerZeroBridge at:", params.iexecLayerZeroBridgeAddress); + + // Generate a new address for testing + newAddress = makeAddr("newSecureAddress"); + + console.log("========================================"); + console.log("Testing Emergency Role Transfer on Fork"); + console.log("========================================"); + console.log("Chain:", chain); + console.log(""); + console.log("Old (compromised) address:", OLD_ADDRESS); + console.log("New (secure) address: ", newAddress); + console.log(""); + + // Step 1: Verify initial state + console.log("--- STEP 1: Verify Initial State ---"); + verifyOldAddressHasRoles(); + verifyNewAddressHasNoRoles(); + console.log(""); + + // Step 2: Grant roles and begin admin transfer (as old address) + console.log("--- STEP 2: Grant Roles & Begin Transfer ---"); + grantRolesAndBeginTransfer(); + console.log(""); + + // Step 3: Verify intermediate state + console.log("--- STEP 3: Verify Intermediate State ---"); + verifyIntermediateState(); + console.log(""); + + // Step 4: Fast forward past delay period + console.log("--- STEP 4: Fast Forward Past Delay Period ---"); + fastForwardPastDelay(); + console.log(""); + + // Step 5: Accept admin and revoke old roles (as new address) + console.log("--- STEP 5: Accept Admin & Revoke Old Roles ---"); + acceptAdminAndRevokeOldRoles(); + console.log(""); + + // Step 6: Verify final state + console.log("--- STEP 6: Verify Final State ---"); + verifyFinalState(); + console.log(""); + + console.log("========================================"); + console.log("OK ALL TESTS PASSED!"); + console.log("========================================"); + } + + function verifyOldAddressHasRoles() internal view { + console.log("Verifying old address has roles..."); + + // Check Token Contract (RLCCrosschainToken or RLCLiquidityUnifier) + string memory tokenName = isApprovalRequired ? "RLCLiquidityUnifier" : "RLCCrosschainToken"; + bool hasDefaultAdmin = tokenContract.hasRole(DEFAULT_ADMIN_ROLE, OLD_ADDRESS); + bool hasUpgrader = tokenContract.hasRole(UPGRADER_ROLE, OLD_ADDRESS); + bool hasPauser = tokenContract.hasRole(PAUSER_ROLE, OLD_ADDRESS); + + console.log(tokenName, ":"); + console.log(" DEFAULT_ADMIN_ROLE:", hasDefaultAdmin ? "OK" : "MISSING"); + console.log(" UPGRADER_ROLE: ", hasUpgrader ? "OK" : "MISSING"); + console.log(" PAUSER_ROLE: ", hasPauser ? "OK (if present)" : "NOT SET (will skip)"); + + require(hasDefaultAdmin, string(abi.encodePacked("Old address missing DEFAULT_ADMIN_ROLE on ", tokenName))); + require(hasUpgrader, string(abi.encodePacked("Old address missing UPGRADER_ROLE on ", tokenName))); + // Note: PAUSER_ROLE might not be set, which is okay + + // Check IexecLayerZeroBridge + bool hasBridgeDefaultAdmin = iexecLayerZeroBridge.hasRole(DEFAULT_ADMIN_ROLE, OLD_ADDRESS); + bool hasBridgeUpgrader = iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, OLD_ADDRESS); + bool hasBridgePauser = iexecLayerZeroBridge.hasRole(PAUSER_ROLE, OLD_ADDRESS); + + console.log("IexecLayerZeroBridge:"); + console.log(" DEFAULT_ADMIN_ROLE:", hasBridgeDefaultAdmin ? "OK" : "MISSING"); + console.log(" UPGRADER_ROLE: ", hasBridgeUpgrader ? "OK" : "MISSING"); + console.log(" PAUSER_ROLE: ", hasBridgePauser ? "OK (if present)" : "NOT SET (will skip)"); + + require(hasBridgeDefaultAdmin, "Old address missing DEFAULT_ADMIN_ROLE on bridge"); + require(hasBridgeUpgrader, "Old address missing UPGRADER_ROLE on bridge"); + // Note: PAUSER_ROLE might not be set, which is okay + + console.log("OK Old address has core roles (ADMIN + UPGRADER)"); + } + + function verifyNewAddressHasNoRoles() internal view { + console.log("Verifying new address has no roles..."); + + require(!tokenContract.hasRole(DEFAULT_ADMIN_ROLE, newAddress), "New address already has admin on token"); + require(!tokenContract.hasRole(UPGRADER_ROLE, newAddress), "New address already has upgrader on token"); + require(!tokenContract.hasRole(PAUSER_ROLE, newAddress), "New address already has pauser on token"); + + require( + !iexecLayerZeroBridge.hasRole(DEFAULT_ADMIN_ROLE, newAddress), "New address already has admin on bridge" + ); + require( + !iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAddress), "New address already has upgrader on bridge" + ); + require(!iexecLayerZeroBridge.hasRole(PAUSER_ROLE, newAddress), "New address already has pauser on bridge"); + + console.log("OK New address has no roles (as expected)"); + } + + function grantRolesAndBeginTransfer() internal { + console.log("Impersonating old address to grant roles..."); + + vm.startBroadcast(OLD_ADDRESS); + + // Grant roles on Token Contract + string memory tokenName = isApprovalRequired ? "RLCLiquidityUnifier" : "RLCCrosschainToken"; + console.log(string(abi.encodePacked("Granting roles on ", tokenName, "..."))); + + tokenContract.grantRole(UPGRADER_ROLE, newAddress); + console.log(" - UPGRADER_ROLE granted"); + + // Only grant PAUSER_ROLE if old address has it + if (tokenContract.hasRole(PAUSER_ROLE, OLD_ADDRESS)) { + tokenContract.grantRole(PAUSER_ROLE, newAddress); + console.log(" - PAUSER_ROLE granted"); + } else { + console.log(" - PAUSER_ROLE (skipped - old address doesn't have it)"); + } + + // Only grant TOKEN_BRIDGE_ROLE for RLCCrosschainToken (not for RLCLiquidityUnifier) + if (!isApprovalRequired) { + tokenContract.grantRole(TOKEN_BRIDGE_ROLE, newAddress); + console.log(" - TOKEN_BRIDGE_ROLE granted"); + } + + console.log(string(abi.encodePacked("Beginning admin transfer on ", tokenName, "..."))); + tokenContract.beginDefaultAdminTransfer(newAddress); + (address pendingAdminToken, uint48 scheduleToken) = tokenContract.pendingDefaultAdmin(); + console.log(" - Pending admin:", pendingAdminToken); + console.log(" - Scheduled for:", uint256(scheduleToken)); + + // Grant roles on IexecLayerZeroBridge + console.log("Granting roles on IexecLayerZeroBridge..."); + iexecLayerZeroBridge.grantRole(UPGRADER_ROLE, newAddress); + console.log(" - UPGRADER_ROLE granted"); + + // Only grant PAUSER_ROLE if old address has it + if (iexecLayerZeroBridge.hasRole(PAUSER_ROLE, OLD_ADDRESS)) { + iexecLayerZeroBridge.grantRole(PAUSER_ROLE, newAddress); + console.log(" - PAUSER_ROLE granted"); + } else { + console.log(" - PAUSER_ROLE (skipped - old address doesn't have it)"); + } + + console.log("Beginning admin transfer on IexecLayerZeroBridge..."); + iexecLayerZeroBridge.beginDefaultAdminTransfer(newAddress); + (address pendingAdminBridge, uint48 scheduleBridge) = iexecLayerZeroBridge.pendingDefaultAdmin(); + console.log(" - Pending admin:", pendingAdminBridge); + console.log(" - Scheduled for:", uint256(scheduleBridge)); + + vm.stopBroadcast(); + + console.log("OK Roles granted and admin transfer begun"); + } + + function verifyIntermediateState() internal view { + console.log("Verifying intermediate state (after grant, before accept)..."); + + // New address should have non-admin roles + console.log("Checking new address has operational roles..."); + require(tokenContract.hasRole(UPGRADER_ROLE, newAddress), "New address missing UPGRADER_ROLE on token"); + + // TOKEN_BRIDGE_ROLE only exists on RLCCrosschainToken (not RLCLiquidityUnifier) + if (!isApprovalRequired) { + require( + tokenContract.hasRole(TOKEN_BRIDGE_ROLE, newAddress), "New address missing TOKEN_BRIDGE_ROLE on token" + ); + } + + require( + iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAddress), "New address missing UPGRADER_ROLE on bridge" + ); + + // New address should NOT have admin role yet + require( + !tokenContract.hasRole(DEFAULT_ADMIN_ROLE, newAddress), + "New address should not have DEFAULT_ADMIN_ROLE yet on token" + ); + require( + !iexecLayerZeroBridge.hasRole(DEFAULT_ADMIN_ROLE, newAddress), + "New address should not have DEFAULT_ADMIN_ROLE yet on bridge" + ); + + // Old address should still have admin role + console.log("Checking old address still has admin role..."); + require( + tokenContract.hasRole(DEFAULT_ADMIN_ROLE, OLD_ADDRESS), + "Old address should still have DEFAULT_ADMIN_ROLE on token" + ); + require( + iexecLayerZeroBridge.hasRole(DEFAULT_ADMIN_ROLE, OLD_ADDRESS), + "Old address should still have DEFAULT_ADMIN_ROLE on bridge" + ); + + console.log("OK Intermediate state verified"); + } + + function fastForwardPastDelay() internal { + (, uint48 scheduleToken) = tokenContract.pendingDefaultAdmin(); + (, uint48 scheduleBridge) = iexecLayerZeroBridge.pendingDefaultAdmin(); + + uint48 maxSchedule = scheduleToken > scheduleBridge ? scheduleToken : scheduleBridge; + + console.log("Current timestamp:", block.timestamp); + console.log("Delay until: ", uint256(maxSchedule)); + console.log("Fast forwarding to:", uint256(maxSchedule) + 1); + + vm.warp(uint256(maxSchedule) + 1); + + console.log("OK Fast forwarded past delay period"); + console.log("New timestamp: ", block.timestamp); + } + + function acceptAdminAndRevokeOldRoles() internal { + console.log("Impersonating new address to accept admin and revoke old roles..."); + + vm.startBroadcast(newAddress); + + // Accept admin on Token Contract + string memory tokenName = isApprovalRequired ? "RLCLiquidityUnifier" : "RLCCrosschainToken"; + console.log(string(abi.encodePacked("Accepting admin on ", tokenName, "..."))); + tokenContract.acceptDefaultAdminTransfer(); + address newAdminToken = tokenContract.defaultAdmin(); + console.log(" - New admin confirmed:", newAdminToken); + require(newAdminToken == newAddress, "Admin transfer failed on token"); + + // Revoke roles from old address on token + console.log(string(abi.encodePacked("Revoking roles from old address on ", tokenName, "..."))); + tokenContract.revokeRole(UPGRADER_ROLE, OLD_ADDRESS); + console.log(" - UPGRADER_ROLE revoked"); + + if (tokenContract.hasRole(PAUSER_ROLE, OLD_ADDRESS)) { + tokenContract.revokeRole(PAUSER_ROLE, OLD_ADDRESS); + console.log(" - PAUSER_ROLE revoked"); + } + + // TOKEN_BRIDGE_ROLE only exists on RLCCrosschainToken + if (!isApprovalRequired && tokenContract.hasRole(TOKEN_BRIDGE_ROLE, OLD_ADDRESS)) { + tokenContract.revokeRole(TOKEN_BRIDGE_ROLE, OLD_ADDRESS); + console.log(" - TOKEN_BRIDGE_ROLE revoked"); + } + + // Accept admin on IexecLayerZeroBridge + console.log("Accepting admin on IexecLayerZeroBridge..."); + iexecLayerZeroBridge.acceptDefaultAdminTransfer(); + address newAdminBridge = iexecLayerZeroBridge.defaultAdmin(); + console.log(" - New admin confirmed:", newAdminBridge); + require(newAdminBridge == newAddress, "Admin transfer failed on bridge"); + + // Revoke roles from old address on bridge + console.log("Revoking roles from old address on IexecLayerZeroBridge..."); + iexecLayerZeroBridge.revokeRole(UPGRADER_ROLE, OLD_ADDRESS); + console.log(" - UPGRADER_ROLE revoked"); + + if (iexecLayerZeroBridge.hasRole(PAUSER_ROLE, OLD_ADDRESS)) { + iexecLayerZeroBridge.revokeRole(PAUSER_ROLE, OLD_ADDRESS); + console.log(" - PAUSER_ROLE revoked"); + } + + vm.stopBroadcast(); + + console.log("OK Admin accepted and old roles revoked"); + } + + function verifyFinalState() internal view { + console.log("Verifying final state..."); + + string memory tokenName = isApprovalRequired ? "RLCLiquidityUnifier" : "RLCCrosschainToken"; + + // New address should have all roles + console.log("Checking new address has all transferred roles..."); + require( + tokenContract.hasRole(DEFAULT_ADMIN_ROLE, newAddress), + string(abi.encodePacked("New address missing DEFAULT_ADMIN_ROLE on ", tokenName)) + ); + require( + tokenContract.hasRole(UPGRADER_ROLE, newAddress), + string(abi.encodePacked("New address missing UPGRADER_ROLE on ", tokenName)) + ); + + // TOKEN_BRIDGE_ROLE only exists on RLCCrosschainToken + if (!isApprovalRequired) { + require( + tokenContract.hasRole(TOKEN_BRIDGE_ROLE, newAddress), + string(abi.encodePacked("New address missing TOKEN_BRIDGE_ROLE on ", tokenName)) + ); + } + + require( + iexecLayerZeroBridge.hasRole(DEFAULT_ADMIN_ROLE, newAddress), + "New address missing DEFAULT_ADMIN_ROLE on bridge" + ); + require( + iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAddress), "New address missing UPGRADER_ROLE on bridge" + ); + + console.log(string(abi.encodePacked(tokenName, " - New address:"))); + console.log(" DEFAULT_ADMIN_ROLE: OK"); + console.log(" UPGRADER_ROLE: OK"); + if (!isApprovalRequired) { + console.log(" TOKEN_BRIDGE_ROLE: OK"); + } + + console.log("IexecLayerZeroBridge - New address:"); + console.log(" DEFAULT_ADMIN_ROLE: OK"); + console.log(" UPGRADER_ROLE: OK"); + + // Old address should have NO roles + console.log("Checking old address has no critical roles..."); + require( + !tokenContract.hasRole(DEFAULT_ADMIN_ROLE, OLD_ADDRESS), + string(abi.encodePacked("Old address still has DEFAULT_ADMIN_ROLE on ", tokenName)) + ); + require( + !tokenContract.hasRole(UPGRADER_ROLE, OLD_ADDRESS), + string(abi.encodePacked("Old address still has UPGRADER_ROLE on ", tokenName)) + ); + + require( + !iexecLayerZeroBridge.hasRole(DEFAULT_ADMIN_ROLE, OLD_ADDRESS), + "Old address still has DEFAULT_ADMIN_ROLE on bridge" + ); + require( + !iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, OLD_ADDRESS), "Old address still has UPGRADER_ROLE on bridge" + ); + + console.log(string(abi.encodePacked(tokenName, " - Old address:"))); + console.log(" Critical roles revoked: OK"); + + console.log("IexecLayerZeroBridge - Old address:"); + console.log(" Critical roles revoked: OK"); + + console.log("OK Final state verified - Transfer complete!"); + } +} diff --git a/script/TransferAllRoles.s.sol b/script/TransferAllRoles.s.sol new file mode 100644 index 00000000..f9c6888b --- /dev/null +++ b/script/TransferAllRoles.s.sol @@ -0,0 +1,268 @@ +// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.22; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { + IAccessControlDefaultAdminRules +} from "@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol"; +import {ConfigLib} from "./lib/ConfigLib.sol"; +import {RLCCrosschainToken} from "../src/RLCCrosschainToken.sol"; +import {IexecLayerZeroBridge} from "../src/bridges/layerZero/IexecLayerZeroBridge.sol"; + +/** + * @title GrantRolesAndBeginAdminTransfer + * @dev Script to grant all roles to a new address and begin the admin transfer process. + * This is step 1 of the role migration process for a compromised address scenario. + * + * Usage: + * CHAIN=arbitrum_sepolia \ + * OLD_ADDRESS=0x9990cfb1Feb7f47297F54bef4d4EbeDf6c5463a3 \ + * NEW_ADDRESS=0x... \ + * forge script script/TransferAllRoles.s.sol:GrantRolesAndBeginAdminTransfer \ + * --rpc-url $RPC_URL \ + * --broadcast \ + * --account $ACCOUNT + */ +contract GrantRolesAndBeginAdminTransfer is Script { + /** + * @notice Main entry point for the script + * @dev Grants all roles to new address and begins admin transfer + */ + function run() external { + address oldAddress = vm.envAddress("OLD_ADDRESS"); + address newAddress = vm.envAddress("NEW_ADDRESS"); + string memory chain = vm.envString("CHAIN"); + + console.log("=== Grant Roles and Begin Admin Transfer ==="); + console.log("Chain:", chain); + console.log("Old (compromised) address:", oldAddress); + console.log("New (secure) address:", newAddress); + console.log(""); + + ConfigLib.CommonConfigParams memory params = ConfigLib.readCommonConfig(chain); + + vm.startBroadcast(); + + // Process RLCCrosschainToken (for non-mainnet chains) + if (!params.approvalRequired) { + console.log("Processing RLCCrosschainToken..."); + grantRolesAndBeginAdminTransfer( + params.rlcCrosschainTokenAddress, + oldAddress, + newAddress, + "RLCCrosschainToken", + true // has TOKEN_BRIDGE_ROLE + ); + console.log(""); + } + + // Process IexecLayerZeroBridge + console.log("Processing IexecLayerZeroBridge..."); + grantRolesAndBeginAdminTransfer( + params.iexecLayerZeroBridgeAddress, + oldAddress, + newAddress, + "IexecLayerZeroBridge", + false // no TOKEN_BRIDGE_ROLE on bridge + ); + + vm.stopBroadcast(); + + console.log(""); + console.log("=== Step 1 Complete ==="); + console.log("Next steps:"); + console.log("1. Wait for the admin transfer delay period to pass"); + console.log("2. Run AcceptAdminRoleAndRevokeOldRoles script with the NEW address"); + } + + /** + * @notice Grants all roles to new address and begins admin transfer for a contract + * @param contractAddress The address of the contract + * @param oldAddress The old (compromised) address + * @param newAddress The new (secure) address + * @param contractName The name of the contract for logging + * @param hasTokenBridgeRole Whether this contract has TOKEN_BRIDGE_ROLE + */ + function grantRolesAndBeginAdminTransfer( + address contractAddress, + address oldAddress, + address newAddress, + string memory contractName, + bool hasTokenBridgeRole + ) internal { + IAccessControlDefaultAdminRules contractInstance = IAccessControlDefaultAdminRules(contractAddress); + + // Verify old address has admin role + bytes32 defaultAdminRole = 0x00; + require( + contractInstance.hasRole(defaultAdminRole, oldAddress), + string(abi.encodePacked(contractName, ": Old address is not admin")) + ); + + // Get role identifiers + bytes32 upgraderRole = keccak256("UPGRADER_ROLE"); + bytes32 pauserRole = keccak256("PAUSER_ROLE"); + + console.log(" Granting roles to new address..."); + + // Grant UPGRADER_ROLE + if (contractInstance.hasRole(upgraderRole, oldAddress)) { + contractInstance.grantRole(upgraderRole, newAddress); + console.log(" - UPGRADER_ROLE granted"); + } + + // Grant PAUSER_ROLE + if (contractInstance.hasRole(pauserRole, oldAddress)) { + contractInstance.grantRole(pauserRole, newAddress); + console.log(" - PAUSER_ROLE granted"); + } + + // Grant TOKEN_BRIDGE_ROLE (only for RLCCrosschainToken) + if (hasTokenBridgeRole) { + bytes32 tokenBridgeRole = keccak256("TOKEN_BRIDGE_ROLE"); + if (contractInstance.hasRole(tokenBridgeRole, oldAddress)) { + contractInstance.grantRole(tokenBridgeRole, newAddress); + console.log(" - TOKEN_BRIDGE_ROLE granted"); + } + } + + // Begin admin transfer + console.log(" Beginning DEFAULT_ADMIN_ROLE transfer..."); + contractInstance.beginDefaultAdminTransfer(newAddress); + + (address pendingAdmin, uint48 schedule) = contractInstance.pendingDefaultAdmin(); + console.log(" - Pending admin:", pendingAdmin); + console.log(" - Transfer scheduled for:", uint256(schedule)); + console.log(" - Current block timestamp:", block.timestamp); + } +} + +/** + * @title AcceptAdminRoleAndRevokeOldRoles + * @dev Script to accept the admin role transfer and revoke all roles from the old address. + * This is step 2 of the role migration process. + * + * IMPORTANT: This script must be run with the NEW address's private key! + * + * Usage: + * CHAIN=arbitrum_sepolia \ + * OLD_ADDRESS=0x9990cfb1Feb7f47297F54bef4d4EbeDf6c5463a3 \ + * forge script script/TransferAllRoles.s.sol:AcceptAdminRoleAndRevokeOldRoles \ + * --rpc-url $RPC_URL \ + * --broadcast \ + * --account $NEW_ACCOUNT + */ +contract AcceptAdminRoleAndRevokeOldRoles is Script { + /** + * @notice Main entry point for the script + * @dev Accepts admin role and revokes all roles from old address + */ + function run() external { + address oldAddress = vm.envAddress("OLD_ADDRESS"); + string memory chain = vm.envString("CHAIN"); + + console.log("=== Accept Admin Role and Revoke Old Roles ==="); + console.log("Chain:", chain); + console.log("Old (compromised) address:", oldAddress); + console.log("New address (caller):", msg.sender); + console.log(""); + + ConfigLib.CommonConfigParams memory params = ConfigLib.readCommonConfig(chain); + + vm.startBroadcast(); + + // Process RLCCrosschainToken (for non-mainnet chains) + if (!params.approvalRequired) { + console.log("Processing RLCCrosschainToken..."); + acceptAdminAndRevokeOldRoles( + params.rlcCrosschainTokenAddress, + oldAddress, + "RLCCrosschainToken", + true // has TOKEN_BRIDGE_ROLE + ); + console.log(""); + } + + // Process IexecLayerZeroBridge + console.log("Processing IexecLayerZeroBridge..."); + acceptAdminAndRevokeOldRoles( + params.iexecLayerZeroBridgeAddress, + oldAddress, + "IexecLayerZeroBridge", + false // no TOKEN_BRIDGE_ROLE on bridge + ); + + vm.stopBroadcast(); + + console.log(""); + console.log("=== Step 2 Complete ==="); + console.log("All roles have been transferred to the new address!"); + console.log("Old address has been completely removed from all contracts."); + } + + /** + * @notice Accepts admin role and revokes all roles from old address + * @param contractAddress The address of the contract + * @param oldAddress The old (compromised) address to revoke roles from + * @param contractName The name of the contract for logging + * @param hasTokenBridgeRole Whether this contract has TOKEN_BRIDGE_ROLE + */ + function acceptAdminAndRevokeOldRoles( + address contractAddress, + address oldAddress, + string memory contractName, + bool hasTokenBridgeRole + ) internal { + IAccessControlDefaultAdminRules contractInstance = IAccessControlDefaultAdminRules(contractAddress); + + // Accept admin role + console.log(" Accepting DEFAULT_ADMIN_ROLE..."); + contractInstance.acceptDefaultAdminTransfer(); + + address newAdmin = contractInstance.defaultAdmin(); + console.log(" - New admin confirmed:", newAdmin); + require(newAdmin != oldAddress, string(abi.encodePacked(contractName, ": Admin not transferred"))); + + // Revoke all roles from old address + console.log(" Revoking all roles from old address..."); + + bytes32 upgraderRole = keccak256("UPGRADER_ROLE"); + bytes32 pauserRole = keccak256("PAUSER_ROLE"); + bytes32 defaultAdminRole = 0x00; + + // Revoke UPGRADER_ROLE + if (contractInstance.hasRole(upgraderRole, oldAddress)) { + contractInstance.revokeRole(upgraderRole, oldAddress); + console.log(" - UPGRADER_ROLE revoked"); + } + + // Revoke PAUSER_ROLE + if (contractInstance.hasRole(pauserRole, oldAddress)) { + contractInstance.revokeRole(pauserRole, oldAddress); + console.log(" - PAUSER_ROLE revoked"); + } + + // Revoke TOKEN_BRIDGE_ROLE (only for RLCCrosschainToken) + if (hasTokenBridgeRole) { + bytes32 tokenBridgeRole = keccak256("TOKEN_BRIDGE_ROLE"); + if (contractInstance.hasRole(tokenBridgeRole, oldAddress)) { + contractInstance.revokeRole(tokenBridgeRole, oldAddress); + console.log(" - TOKEN_BRIDGE_ROLE revoked"); + } + } + + // Verify old address no longer has any roles + bool hasUpgrader = contractInstance.hasRole(upgraderRole, oldAddress); + bool hasPauser = contractInstance.hasRole(pauserRole, oldAddress); + bool hasAdmin = contractInstance.hasRole(defaultAdminRole, oldAddress); + + require(!hasUpgrader && !hasPauser && !hasAdmin, + string(abi.encodePacked(contractName, ": Old address still has roles"))); + + console.log(" - All roles successfully revoked from old address"); + } +} diff --git a/test/units/TransferAllRolesScript.t.sol b/test/units/TransferAllRolesScript.t.sol new file mode 100644 index 00000000..dbc70a3e --- /dev/null +++ b/test/units/TransferAllRolesScript.t.sol @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +import {TestHelperOz5} from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol"; +import {TestUtils} from "./utils/TestUtils.sol"; +import {RLCCrosschainToken} from "../../src/RLCCrosschainToken.sol"; +import {IexecLayerZeroBridge} from "../../src/bridges/layerZero/IexecLayerZeroBridge.sol"; +import { + IAccessControlDefaultAdminRules +} from "@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol"; + +contract TransferAllRolesScriptTest is TestHelperOz5 { + using TestUtils for *; + + IexecLayerZeroBridge public iexecLayerZeroBridge; + RLCCrosschainToken public rlcCrosschainToken; + + address public mockEndpoint; + address public oldAdmin = makeAddr("oldAdmin"); // Compromised address with ALL roles + address public newAdmin = makeAddr("newAdmin"); // New secure address + + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant TOKEN_BRIDGE_ROLE = keccak256("TOKEN_BRIDGE_ROLE"); + + function setUp() public virtual override { + super.setUp(); + setUpEndpoints(2, LibraryType.UltraLightNode); + mockEndpoint = address(endpoints[1]); + + // Deploy contracts with the old admin having ALL roles (matching real scenario) + TestUtils.DeploymentResult memory deploymentResult = TestUtils.setupDeployment( + TestUtils.DeploymentParams({ + iexecLayerZeroBridgeContractName: "IexecLayerZeroBridge", + lzEndpointSource: mockEndpoint, + lzEndpointDestination: mockEndpoint, + initialAdmin: oldAdmin, + initialUpgrader: oldAdmin, // Same as admin (compromised has all roles) + initialPauser: oldAdmin // Same as admin (compromised has all roles) + }) + ); + + iexecLayerZeroBridge = deploymentResult.iexecLayerZeroBridgeWithoutApproval; + rlcCrosschainToken = deploymentResult.rlcCrosschainToken; + + // Grant all additional roles to oldAdmin (simulating real scenario where admin granted themselves all roles) + vm.startPrank(oldAdmin); + // Token roles + rlcCrosschainToken.grantRole(PAUSER_ROLE, oldAdmin); + rlcCrosschainToken.grantRole(TOKEN_BRIDGE_ROLE, address(iexecLayerZeroBridge)); + + // Bridge roles + iexecLayerZeroBridge.grantRole(PAUSER_ROLE, oldAdmin); + vm.stopPrank(); + } + + function test_GrantRolesAndBeginTransfer() public { + // Verify initial state - old admin has all roles + _verifyOldAdminHasAllRoles(); + _verifyNewAdminHasNoRoles(); + + // Step 1: Grant roles and begin admin transfer (as old admin) + vm.startPrank(oldAdmin); + + // Grant roles on RLCCrosschainToken + rlcCrosschainToken.grantRole(UPGRADER_ROLE, newAdmin); + rlcCrosschainToken.grantRole(PAUSER_ROLE, newAdmin); + rlcCrosschainToken.grantRole(TOKEN_BRIDGE_ROLE, newAdmin); + rlcCrosschainToken.beginDefaultAdminTransfer(newAdmin); + + // Grant roles on IexecLayerZeroBridge + iexecLayerZeroBridge.grantRole(UPGRADER_ROLE, newAdmin); + iexecLayerZeroBridge.grantRole(PAUSER_ROLE, newAdmin); + iexecLayerZeroBridge.beginDefaultAdminTransfer(newAdmin); + + vm.stopPrank(); + + // Verify new admin has non-admin roles but not DEFAULT_ADMIN yet + assertTrue(rlcCrosschainToken.hasRole(UPGRADER_ROLE, newAdmin), "New admin should have UPGRADER_ROLE on token"); + assertTrue(rlcCrosschainToken.hasRole(PAUSER_ROLE, newAdmin), "New admin should have PAUSER_ROLE on token"); + assertTrue( + rlcCrosschainToken.hasRole(TOKEN_BRIDGE_ROLE, newAdmin), "New admin should have TOKEN_BRIDGE_ROLE on token" + ); + assertFalse( + rlcCrosschainToken.hasRole(rlcCrosschainToken.DEFAULT_ADMIN_ROLE(), newAdmin), + "New admin should not have DEFAULT_ADMIN_ROLE yet" + ); + + assertTrue( + iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAdmin), "New admin should have UPGRADER_ROLE on bridge" + ); + assertTrue(iexecLayerZeroBridge.hasRole(PAUSER_ROLE, newAdmin), "New admin should have PAUSER_ROLE on bridge"); + assertFalse( + iexecLayerZeroBridge.hasRole(iexecLayerZeroBridge.DEFAULT_ADMIN_ROLE(), newAdmin), + "New admin should not have DEFAULT_ADMIN_ROLE yet" + ); + + // Verify pending admin transfer + (address pendingAdminToken,) = rlcCrosschainToken.pendingDefaultAdmin(); + (address pendingAdminBridge,) = iexecLayerZeroBridge.pendingDefaultAdmin(); + assertEq(pendingAdminToken, newAdmin, "Pending admin should be new admin on token"); + assertEq(pendingAdminBridge, newAdmin, "Pending admin should be new admin on bridge"); + } + + function test_AcceptAdminAndRevokeOldRoles() public { + // First, complete step 1 + test_GrantRolesAndBeginTransfer(); + + // Fast forward past the delay period + (, uint48 scheduleToken) = rlcCrosschainToken.pendingDefaultAdmin(); + vm.warp(scheduleToken + 1); + + // Step 2: Accept admin and revoke old roles (as new admin) + vm.startPrank(newAdmin); + + // Accept admin role on RLCCrosschainToken + rlcCrosschainToken.acceptDefaultAdminTransfer(); + + // Accept admin role on IexecLayerZeroBridge + iexecLayerZeroBridge.acceptDefaultAdminTransfer(); + + // Revoke all roles from old admin on RLCCrosschainToken + rlcCrosschainToken.revokeRole(UPGRADER_ROLE, oldAdmin); + rlcCrosschainToken.revokeRole(PAUSER_ROLE, oldAdmin); + rlcCrosschainToken.revokeRole(TOKEN_BRIDGE_ROLE, oldAdmin); + + // Revoke all roles from old admin on IexecLayerZeroBridge + iexecLayerZeroBridge.revokeRole(UPGRADER_ROLE, oldAdmin); + iexecLayerZeroBridge.revokeRole(PAUSER_ROLE, oldAdmin); + + vm.stopPrank(); + + // Verify new admin has all roles + assertTrue( + rlcCrosschainToken.hasRole(rlcCrosschainToken.DEFAULT_ADMIN_ROLE(), newAdmin), + "New admin should have DEFAULT_ADMIN_ROLE on token" + ); + assertTrue(rlcCrosschainToken.hasRole(UPGRADER_ROLE, newAdmin), "New admin should have UPGRADER_ROLE on token"); + assertTrue(rlcCrosschainToken.hasRole(PAUSER_ROLE, newAdmin), "New admin should have PAUSER_ROLE on token"); + assertTrue( + rlcCrosschainToken.hasRole(TOKEN_BRIDGE_ROLE, newAdmin), "New admin should have TOKEN_BRIDGE_ROLE on token" + ); + + assertTrue( + iexecLayerZeroBridge.hasRole(iexecLayerZeroBridge.DEFAULT_ADMIN_ROLE(), newAdmin), + "New admin should have DEFAULT_ADMIN_ROLE on bridge" + ); + assertTrue( + iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAdmin), "New admin should have UPGRADER_ROLE on bridge" + ); + assertTrue(iexecLayerZeroBridge.hasRole(PAUSER_ROLE, newAdmin), "New admin should have PAUSER_ROLE on bridge"); + + // Verify old admin has no roles + _verifyOldAdminHasNoRoles(); + } + + function test_CannotAcceptBeforeDelay() public { + // First, complete step 1 + test_GrantRolesAndBeginTransfer(); + + // Try to accept before delay (should revert) + vm.prank(newAdmin); + vm.expectRevert(); + rlcCrosschainToken.acceptDefaultAdminTransfer(); + + vm.prank(newAdmin); + vm.expectRevert(); + iexecLayerZeroBridge.acceptDefaultAdminTransfer(); + } + + function test_OldAdminStillHasAccessDuringDelay() public { + // First, complete step 1 + test_GrantRolesAndBeginTransfer(); + + // Verify old admin still has DEFAULT_ADMIN_ROLE during delay + assertTrue( + rlcCrosschainToken.hasRole(rlcCrosschainToken.DEFAULT_ADMIN_ROLE(), oldAdmin), + "Old admin should still have DEFAULT_ADMIN_ROLE during delay" + ); + assertTrue( + iexecLayerZeroBridge.hasRole(iexecLayerZeroBridge.DEFAULT_ADMIN_ROLE(), oldAdmin), + "Old admin should still have DEFAULT_ADMIN_ROLE during delay" + ); + } + + function _verifyOldAdminHasAllRoles() internal view { + // RLCCrosschainToken + assertTrue( + rlcCrosschainToken.hasRole(rlcCrosschainToken.DEFAULT_ADMIN_ROLE(), oldAdmin), + "Old admin should have DEFAULT_ADMIN_ROLE on token" + ); + assertTrue(rlcCrosschainToken.hasRole(UPGRADER_ROLE, oldAdmin), "Old admin should have UPGRADER_ROLE on token"); + assertTrue(rlcCrosschainToken.hasRole(PAUSER_ROLE, oldAdmin), "Old admin should have PAUSER_ROLE on token"); + + // IexecLayerZeroBridge + assertTrue( + iexecLayerZeroBridge.hasRole(iexecLayerZeroBridge.DEFAULT_ADMIN_ROLE(), oldAdmin), + "Old admin should have DEFAULT_ADMIN_ROLE on bridge" + ); + assertTrue( + iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, oldAdmin), "Old admin should have UPGRADER_ROLE on bridge" + ); + assertTrue(iexecLayerZeroBridge.hasRole(PAUSER_ROLE, oldAdmin), "Old admin should have PAUSER_ROLE on bridge"); + } + + function _verifyNewAdminHasNoRoles() internal view { + // RLCCrosschainToken + assertFalse( + rlcCrosschainToken.hasRole(rlcCrosschainToken.DEFAULT_ADMIN_ROLE(), newAdmin), + "New admin should not have DEFAULT_ADMIN_ROLE on token" + ); + assertFalse( + rlcCrosschainToken.hasRole(UPGRADER_ROLE, newAdmin), "New admin should not have UPGRADER_ROLE on token" + ); + assertFalse(rlcCrosschainToken.hasRole(PAUSER_ROLE, newAdmin), "New admin should not have PAUSER_ROLE on token"); + assertFalse( + rlcCrosschainToken.hasRole(TOKEN_BRIDGE_ROLE, newAdmin), + "New admin should not have TOKEN_BRIDGE_ROLE on token" + ); + + // IexecLayerZeroBridge + assertFalse( + iexecLayerZeroBridge.hasRole(iexecLayerZeroBridge.DEFAULT_ADMIN_ROLE(), newAdmin), + "New admin should not have DEFAULT_ADMIN_ROLE on bridge" + ); + assertFalse( + iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAdmin), "New admin should not have UPGRADER_ROLE on bridge" + ); + assertFalse( + iexecLayerZeroBridge.hasRole(PAUSER_ROLE, newAdmin), "New admin should not have PAUSER_ROLE on bridge" + ); + } + + function _verifyOldAdminHasNoRoles() internal view { + // RLCCrosschainToken + assertFalse( + rlcCrosschainToken.hasRole(rlcCrosschainToken.DEFAULT_ADMIN_ROLE(), oldAdmin), + "Old admin should not have DEFAULT_ADMIN_ROLE on token" + ); + assertFalse( + rlcCrosschainToken.hasRole(UPGRADER_ROLE, oldAdmin), "Old admin should not have UPGRADER_ROLE on token" + ); + assertFalse(rlcCrosschainToken.hasRole(PAUSER_ROLE, oldAdmin), "Old admin should not have PAUSER_ROLE on token"); + assertFalse( + rlcCrosschainToken.hasRole(TOKEN_BRIDGE_ROLE, oldAdmin), + "Old admin should not have TOKEN_BRIDGE_ROLE on token" + ); + + // IexecLayerZeroBridge + assertFalse( + iexecLayerZeroBridge.hasRole(iexecLayerZeroBridge.DEFAULT_ADMIN_ROLE(), oldAdmin), + "Old admin should not have DEFAULT_ADMIN_ROLE on bridge" + ); + assertFalse( + iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, oldAdmin), "Old admin should not have UPGRADER_ROLE on bridge" + ); + assertFalse( + iexecLayerZeroBridge.hasRole(PAUSER_ROLE, oldAdmin), "Old admin should not have PAUSER_ROLE on bridge" + ); + } +} From 6168fab11c6f19ba30d55843f4f2e00bb5c5be3a Mon Sep 17 00:00:00 2001 From: gfournieriExec Date: Thu, 20 Nov 2025 17:52:04 +0100 Subject: [PATCH 2/8] chore: Update emergency role transfer workflow for improved clarity and maintainability --- .github/workflows/emergency-role-transfer.yml | 168 +++++++++++++++--- 1 file changed, 141 insertions(+), 27 deletions(-) diff --git a/.github/workflows/emergency-role-transfer.yml b/.github/workflows/emergency-role-transfer.yml index 7391a6b9..a0251b84 100644 --- a/.github/workflows/emergency-role-transfer.yml +++ b/.github/workflows/emergency-role-transfer.yml @@ -3,14 +3,15 @@ name: Emergency Role Transfer on: workflow_dispatch: inputs: - step: - description: 'Transfer step to execute' + execution_mode: + description: 'Execution mode' required: true type: choice options: - - grant-roles-begin-transfer - - accept-admin-revoke-old - default: grant-roles-begin-transfer + - full-transfer-with-delay + - step-1-only + - step-2-only + default: full-transfer-with-delay network_type: description: 'Network type to transfer roles on' required: true @@ -20,15 +21,6 @@ on: - sepolia-only - arbitrum_sepolia-only default: testnets - old_address: - description: 'Old/compromised address (for both steps)' - required: true - type: string - default: '0x9990cfb1Feb7f47297F54bef4d4EbeDf6c5463a3' - new_address: - description: 'New secure address (only needed for step 1)' - required: false - type: string jobs: setup-matrix: @@ -74,47 +66,169 @@ jobs: - name: Validate inputs run: | - if [ "${{ inputs.step }}" = "grant-roles-begin-transfer" ] && [ -z "${{ inputs.new_address }}" ]; then - echo "Error: new_address is required for grant-roles-begin-transfer step" + echo "Using addresses from GitHub Variables:" + echo " OLD_ADDRESS: ${{ vars.OLD_ADDRESS }}" + echo " NEW_ADDRESS: ${{ vars.NEW_ADDRESS }}" + echo "" + + if [ "${{ inputs.execution_mode }}" = "step-1-only" ] || [ "${{ inputs.execution_mode }}" = "full-transfer-with-delay" ]; then + if [ -z "${{ vars.NEW_ADDRESS }}" ]; then + echo "Error: NEW_ADDRESS variable is not set in GitHub Variables" + echo "Please configure NEW_ADDRESS in repository settings -> Variables" + exit 1 + fi + fi + + if [ -z "${{ vars.OLD_ADDRESS }}" ]; then + echo "Error: OLD_ADDRESS variable is not set in GitHub Variables" + echo "Please configure OLD_ADDRESS in repository settings -> Variables" exit 1 fi - name: Execute Step 1 - Grant Roles and Begin Transfer - if: inputs.step == 'grant-roles-begin-transfer' + if: inputs.execution_mode == 'step-1-only' || inputs.execution_mode == 'full-transfer-with-delay' env: ADMIN_PRIVATE_KEY: ${{ secrets.ADMIN_PRIVATE_KEY }} CHAIN: ${{ matrix.network }} RPC_URL: ${{ secrets.RPC_URL }} - OLD_ADDRESS: ${{ inputs.old_address }} - NEW_ADDRESS: ${{ inputs.new_address }} + OLD_ADDRESS: ${{ vars.OLD_ADDRESS }} + NEW_ADDRESS: ${{ vars.NEW_ADDRESS }} run: | - echo "Executing Step 1: Grant roles and begin admin transfer" + echo "=========================================" + echo "STEP 1: Grant Roles and Begin Transfer" + echo "=========================================" echo "Chain: $CHAIN" echo "Old address: $OLD_ADDRESS" echo "New address: $NEW_ADDRESS" + echo "" make grant-roles-begin-transfer + - name: Wait for Delay Period + if: inputs.execution_mode == 'full-transfer-with-delay' + env: + CHAIN: ${{ matrix.network }} + RPC_URL: ${{ secrets.RPC_URL }} + run: | + echo "=========================================" + echo "WAITING FOR DELAY PERIOD" + echo "=========================================" + echo "Querying contract for delay schedule..." + + # Get the scheduled timestamp from the contract + # This script will query the pendingDefaultAdmin and calculate wait time + cat > get_delay.sh << 'EOF' + #!/bin/bash + + # Query RLCCrosschainToken or RLCLiquidityUnifier + if [ "$CHAIN" = "sepolia" ]; then + CONTRACT_ADDRESS="0x7198CA5eAeFE7416d4f3900b58Ff1bEA33771A65" + CONTRACT_NAME="RLCLiquidityUnifier" + else + CONTRACT_ADDRESS="0x9923eD3cbd90CD78b910c475f9A731A6e0b8C963" + CONTRACT_NAME="RLCCrosschainToken" + fi + + echo "Contract: $CONTRACT_NAME at $CONTRACT_ADDRESS" + + # Get pending admin info (returns address and uint48 schedule) + RESULT=$(cast call $CONTRACT_ADDRESS "pendingDefaultAdmin()(address,uint48)" --rpc-url $RPC_URL) + + # Extract the timestamp (second value) + SCHEDULED_TIME=$(echo $RESULT | awk '{print $2}') + CURRENT_TIME=$(date +%s) + + echo "Current timestamp: $CURRENT_TIME" + echo "Scheduled timestamp: $SCHEDULED_TIME" + + # Calculate wait time (minimum required by contract) + WAIT_SECONDS=$((SCHEDULED_TIME - CURRENT_TIME)) + + # Add 1 second to ensure we're past the scheduled time + TOTAL_WAIT=$((WAIT_SECONDS + 1)) + + if [ $TOTAL_WAIT -le 0 ]; then + echo "✅ Delay period has already passed!" + echo "Proceeding immediately with Step 2..." + exit 0 + fi + + echo "" + echo "⏰ Delay period:" + echo " - Required wait: $WAIT_SECONDS seconds ($((WAIT_SECONDS / 60)) minutes)" + echo " - Safety buffer: 1 second" + echo " - Total wait: $TOTAL_WAIT seconds ($((TOTAL_WAIT / 60)) minutes)" + echo "" + echo "Waiting until: $(date -u -r $((SCHEDULED_TIME + 1)) '+%Y-%m-%d %H:%M:%S UTC')" + echo "" + + # Sleep in chunks to show progress + INTERVAL=60 # Show progress every minute + ELAPSED=0 + + while [ $ELAPSED -lt $TOTAL_WAIT ]; do + REMAINING=$((TOTAL_WAIT - ELAPSED)) + if [ $REMAINING -lt $INTERVAL ]; then + sleep $REMAINING + ELAPSED=$TOTAL_WAIT + else + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) + echo "⏳ Progress: $((ELAPSED / 60))/$((TOTAL_WAIT / 60)) minutes elapsed..." + fi + done + + echo "" + echo "✅ Delay period complete! Proceeding to Step 2..." + EOF + + chmod +x get_delay.sh + ./get_delay.sh + - name: Execute Step 2 - Accept Admin and Revoke Old Roles - if: inputs.step == 'accept-admin-revoke-old' + if: inputs.execution_mode == 'step-2-only' || inputs.execution_mode == 'full-transfer-with-delay' env: NEW_ADMIN_PRIVATE_KEY: ${{ secrets.NEW_ADMIN_PRIVATE_KEY }} CHAIN: ${{ matrix.network }} RPC_URL: ${{ secrets.RPC_URL }} - OLD_ADDRESS: ${{ inputs.old_address }} + OLD_ADDRESS: ${{ vars.OLD_ADDRESS }} run: | - echo "Executing Step 2: Accept admin role and revoke old roles" + echo "=========================================" + echo "STEP 2: Accept Admin and Revoke Old Roles" + echo "=========================================" echo "Chain: $CHAIN" echo "Old address to revoke: $OLD_ADDRESS" + echo "" make accept-admin-revoke-old - name: Summary run: | - if [ "${{ inputs.step }}" = "grant-roles-begin-transfer" ]; then - echo "✅ Step 1 Complete on ${{ matrix.network }}" + echo "" + echo "=========================================" + echo "SUMMARY" + echo "=========================================" + + if [ "${{ inputs.execution_mode }}" = "full-transfer-with-delay" ]; then + echo "✅ FULL TRANSFER COMPLETE on ${{ matrix.network }}" + echo "" + echo "Steps completed:" + echo " 1. ✅ Granted operational roles to new address" + echo " 2. ✅ Began admin transfer with delay period" + echo " 3. ✅ Waited for minimum contract delay period" + echo " 4. ✅ Accepted admin role with new address" + echo " 5. ✅ Revoked all roles from old address" + echo "" + echo "🎉 All roles have been transferred!" + echo "🔒 Old address has been completely removed from all contracts" + + elif [ "${{ inputs.execution_mode }}" = "step-1-only" ]; then + echo "✅ STEP 1 COMPLETE on ${{ matrix.network }}" + echo "" echo "⏰ Wait for the delay period to pass before running Step 2" - echo "📝 Next: Run this workflow again with step=accept-admin-revoke-old" + echo "📝 Next: Run this workflow again with execution_mode=step-2-only" + else - echo "✅ Step 2 Complete on ${{ matrix.network }}" + echo "✅ STEP 2 COMPLETE on ${{ matrix.network }}" + echo "" echo "🎉 All roles have been transferred!" echo "🔒 Old address has been completely removed from all contracts" fi From d2a3367f31a04c2d9a9d385cf44bf7551eb253fa Mon Sep 17 00:00:00 2001 From: gfournieriExec Date: Fri, 21 Nov 2025 09:34:02 +0100 Subject: [PATCH 3/8] fix: error to trigger --- .github/workflows/emergency-role-transfer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/emergency-role-transfer.yml b/.github/workflows/emergency-role-transfer.yml index a0251b84..83f3f33e 100644 --- a/.github/workflows/emergency-role-transfer.yml +++ b/.github/workflows/emergency-role-transfer.yml @@ -1,7 +1,7 @@ name: Emergency Role Transfer on: - workflow_dispatch: + workflow_dispatch: inputs: execution_mode: description: 'Execution mode' From aaff9a696cde710bcb72b4532ca757a7fac02e5f Mon Sep 17 00:00:00 2001 From: gfournieriExec Date: Fri, 21 Nov 2025 09:51:11 +0100 Subject: [PATCH 4/8] fix: correct indentation for workflow_dispatch in emergency role transfer YAML --- .github/workflows/emergency-role-transfer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/emergency-role-transfer.yml b/.github/workflows/emergency-role-transfer.yml index 83f3f33e..a0251b84 100644 --- a/.github/workflows/emergency-role-transfer.yml +++ b/.github/workflows/emergency-role-transfer.yml @@ -1,7 +1,7 @@ name: Emergency Role Transfer on: - workflow_dispatch: + workflow_dispatch: inputs: execution_mode: description: 'Execution mode' From 030631d2143c150b75f3f2bf9118f8f3a2a42dba Mon Sep 17 00:00:00 2001 From: gfournieriExec Date: Fri, 21 Nov 2025 12:04:21 +0100 Subject: [PATCH 5/8] fix: forgot RLC LU --- script/TransferAllRoles.s.sol | 41 +++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/script/TransferAllRoles.s.sol b/script/TransferAllRoles.s.sol index f9c6888b..3d1ee055 100644 --- a/script/TransferAllRoles.s.sol +++ b/script/TransferAllRoles.s.sol @@ -47,7 +47,6 @@ contract GrantRolesAndBeginAdminTransfer is Script { vm.startBroadcast(); - // Process RLCCrosschainToken (for non-mainnet chains) if (!params.approvalRequired) { console.log("Processing RLCCrosschainToken..."); grantRolesAndBeginAdminTransfer( @@ -55,20 +54,30 @@ contract GrantRolesAndBeginAdminTransfer is Script { oldAddress, newAddress, "RLCCrosschainToken", - true // has TOKEN_BRIDGE_ROLE + false // has TOKEN_BRIDGE_ROLE + ); + console.log(""); + } else { + console.log("Processing RLCLiquidityUnifier..."); + grantRolesAndBeginAdminTransfer( + params.rlcLiquidityUnifierAddress, + oldAddress, + newAddress, + "RLCLiquidityUnifier", + false // has TOKEN_BRIDGE_ROLE ); console.log(""); } // Process IexecLayerZeroBridge - console.log("Processing IexecLayerZeroBridge..."); - grantRolesAndBeginAdminTransfer( - params.iexecLayerZeroBridgeAddress, - oldAddress, - newAddress, - "IexecLayerZeroBridge", - false // no TOKEN_BRIDGE_ROLE on bridge - ); + // console.log("Processing IexecLayerZeroBridge..."); + // grantRolesAndBeginAdminTransfer( + // params.iexecLayerZeroBridgeAddress, + // oldAddress, + // newAddress, + // "IexecLayerZeroBridge", + // false // no TOKEN_BRIDGE_ROLE on bridge + // ); vm.stopBroadcast(); @@ -175,14 +184,22 @@ contract AcceptAdminRoleAndRevokeOldRoles is Script { vm.startBroadcast(); - // Process RLCCrosschainToken (for non-mainnet chains) if (!params.approvalRequired) { console.log("Processing RLCCrosschainToken..."); acceptAdminAndRevokeOldRoles( params.rlcCrosschainTokenAddress, oldAddress, "RLCCrosschainToken", - true // has TOKEN_BRIDGE_ROLE + false // has TOKEN_BRIDGE_ROLE + ); + console.log(""); + } else { + console.log("Processing RLCLiquidityUnifier..."); + acceptAdminAndRevokeOldRoles( + params.rlcLiquidityUnifierAddress, + oldAddress, + "RLCLiquidityUnifier", + false // has TOKEN_BRIDGE_ROLE ); console.log(""); } From 5e068c94d3997023d50d29a76471f276731683b9 Mon Sep 17 00:00:00 2001 From: gfournieriExec Date: Fri, 21 Nov 2025 12:30:59 +0100 Subject: [PATCH 6/8] refactor: simplify role transfer functions by removing TOKEN_BRIDGE_ROLE parameter --- script/TransferAllRoles.s.sol | 68 ++++++++--------------------------- 1 file changed, 15 insertions(+), 53 deletions(-) diff --git a/script/TransferAllRoles.s.sol b/script/TransferAllRoles.s.sol index 3d1ee055..f2ab7f3f 100644 --- a/script/TransferAllRoles.s.sol +++ b/script/TransferAllRoles.s.sol @@ -53,8 +53,7 @@ contract GrantRolesAndBeginAdminTransfer is Script { params.rlcCrosschainTokenAddress, oldAddress, newAddress, - "RLCCrosschainToken", - false // has TOKEN_BRIDGE_ROLE + "RLCCrosschainToken" ); console.log(""); } else { @@ -63,21 +62,19 @@ contract GrantRolesAndBeginAdminTransfer is Script { params.rlcLiquidityUnifierAddress, oldAddress, newAddress, - "RLCLiquidityUnifier", - false // has TOKEN_BRIDGE_ROLE + "RLCLiquidityUnifier" ); console.log(""); } // Process IexecLayerZeroBridge - // console.log("Processing IexecLayerZeroBridge..."); - // grantRolesAndBeginAdminTransfer( - // params.iexecLayerZeroBridgeAddress, - // oldAddress, - // newAddress, - // "IexecLayerZeroBridge", - // false // no TOKEN_BRIDGE_ROLE on bridge - // ); + console.log("Processing IexecLayerZeroBridge..."); + grantRolesAndBeginAdminTransfer( + params.iexecLayerZeroBridgeAddress, + oldAddress, + newAddress, + "IexecLayerZeroBridge" + ); vm.stopBroadcast(); @@ -94,14 +91,12 @@ contract GrantRolesAndBeginAdminTransfer is Script { * @param oldAddress The old (compromised) address * @param newAddress The new (secure) address * @param contractName The name of the contract for logging - * @param hasTokenBridgeRole Whether this contract has TOKEN_BRIDGE_ROLE */ function grantRolesAndBeginAdminTransfer( address contractAddress, address oldAddress, address newAddress, - string memory contractName, - bool hasTokenBridgeRole + string memory contractName ) internal { IAccessControlDefaultAdminRules contractInstance = IAccessControlDefaultAdminRules(contractAddress); @@ -117,29 +112,15 @@ contract GrantRolesAndBeginAdminTransfer is Script { bytes32 pauserRole = keccak256("PAUSER_ROLE"); console.log(" Granting roles to new address..."); - - // Grant UPGRADER_ROLE if (contractInstance.hasRole(upgraderRole, oldAddress)) { contractInstance.grantRole(upgraderRole, newAddress); console.log(" - UPGRADER_ROLE granted"); } - - // Grant PAUSER_ROLE if (contractInstance.hasRole(pauserRole, oldAddress)) { contractInstance.grantRole(pauserRole, newAddress); console.log(" - PAUSER_ROLE granted"); } - - // Grant TOKEN_BRIDGE_ROLE (only for RLCCrosschainToken) - if (hasTokenBridgeRole) { - bytes32 tokenBridgeRole = keccak256("TOKEN_BRIDGE_ROLE"); - if (contractInstance.hasRole(tokenBridgeRole, oldAddress)) { - contractInstance.grantRole(tokenBridgeRole, newAddress); - console.log(" - TOKEN_BRIDGE_ROLE granted"); - } - } - - // Begin admin transfer + console.log(" Beginning DEFAULT_ADMIN_ROLE transfer..."); contractInstance.beginDefaultAdminTransfer(newAddress); @@ -189,8 +170,7 @@ contract AcceptAdminRoleAndRevokeOldRoles is Script { acceptAdminAndRevokeOldRoles( params.rlcCrosschainTokenAddress, oldAddress, - "RLCCrosschainToken", - false // has TOKEN_BRIDGE_ROLE + "RLCCrosschainToken" ); console.log(""); } else { @@ -198,19 +178,15 @@ contract AcceptAdminRoleAndRevokeOldRoles is Script { acceptAdminAndRevokeOldRoles( params.rlcLiquidityUnifierAddress, oldAddress, - "RLCLiquidityUnifier", - false // has TOKEN_BRIDGE_ROLE + "RLCLiquidityUnifier" ); console.log(""); } - - // Process IexecLayerZeroBridge console.log("Processing IexecLayerZeroBridge..."); acceptAdminAndRevokeOldRoles( params.iexecLayerZeroBridgeAddress, oldAddress, - "IexecLayerZeroBridge", - false // no TOKEN_BRIDGE_ROLE on bridge + "IexecLayerZeroBridge" ); vm.stopBroadcast(); @@ -226,13 +202,11 @@ contract AcceptAdminRoleAndRevokeOldRoles is Script { * @param contractAddress The address of the contract * @param oldAddress The old (compromised) address to revoke roles from * @param contractName The name of the contract for logging - * @param hasTokenBridgeRole Whether this contract has TOKEN_BRIDGE_ROLE */ function acceptAdminAndRevokeOldRoles( address contractAddress, address oldAddress, - string memory contractName, - bool hasTokenBridgeRole + string memory contractName ) internal { IAccessControlDefaultAdminRules contractInstance = IAccessControlDefaultAdminRules(contractAddress); @@ -251,27 +225,15 @@ contract AcceptAdminRoleAndRevokeOldRoles is Script { bytes32 pauserRole = keccak256("PAUSER_ROLE"); bytes32 defaultAdminRole = 0x00; - // Revoke UPGRADER_ROLE if (contractInstance.hasRole(upgraderRole, oldAddress)) { contractInstance.revokeRole(upgraderRole, oldAddress); console.log(" - UPGRADER_ROLE revoked"); } - - // Revoke PAUSER_ROLE if (contractInstance.hasRole(pauserRole, oldAddress)) { contractInstance.revokeRole(pauserRole, oldAddress); console.log(" - PAUSER_ROLE revoked"); } - // Revoke TOKEN_BRIDGE_ROLE (only for RLCCrosschainToken) - if (hasTokenBridgeRole) { - bytes32 tokenBridgeRole = keccak256("TOKEN_BRIDGE_ROLE"); - if (contractInstance.hasRole(tokenBridgeRole, oldAddress)) { - contractInstance.revokeRole(tokenBridgeRole, oldAddress); - console.log(" - TOKEN_BRIDGE_ROLE revoked"); - } - } - // Verify old address no longer has any roles bool hasUpgrader = contractInstance.hasRole(upgraderRole, oldAddress); bool hasPauser = contractInstance.hasRole(pauserRole, oldAddress); From dba7acb7bed9f35297039ce09506e11ebd24e63d Mon Sep 17 00:00:00 2001 From: gfournieriExec Date: Fri, 21 Nov 2025 12:38:22 +0100 Subject: [PATCH 7/8] chore: update scripts for improved clarity and maintainability --- script/TestEmergencyRoleTransferOnFork.s.sol | 44 +++++------ script/TransferAllRoles.s.sol | 77 +++++++------------- test/units/TransferAllRolesScript.t.sol | 4 +- 3 files changed, 49 insertions(+), 76 deletions(-) diff --git a/script/TestEmergencyRoleTransferOnFork.s.sol b/script/TestEmergencyRoleTransferOnFork.s.sol index 58b1792d..74a0fe25 100644 --- a/script/TestEmergencyRoleTransferOnFork.s.sol +++ b/script/TestEmergencyRoleTransferOnFork.s.sol @@ -17,29 +17,29 @@ import {ConfigLib} from "./lib/ConfigLib.sol"; /** * @title TestEmergencyRoleTransferOnFork * @dev Script to test the emergency role transfer on a fork of Ethereum Sepolia or Arbitrum Sepolia - * + * * This script simulates the complete role transfer process on a local fork: * 1. Verifies compromised address has all roles * 2. Grants roles to new address and begins admin transfer * 3. Fast forwards past delay period * 4. Accepts admin role and revokes old roles * 5. Verifies new address has all roles and old address has none - * + * * Usage: * # For Arbitrum Sepolia: * # Start fork in terminal 1: * anvil --fork-url $ARBITRUM_SEPOLIA_RPC_URL --port 8546 - * + * * # Run test in terminal 2: * CHAIN=arbitrum_sepolia forge script script/TestEmergencyRoleTransferOnFork.s.sol:TestEmergencyRoleTransferOnFork \ * --rpc-url http://localhost:8546 \ * --broadcast \ * -vv - * + * * # For Ethereum Sepolia: * # Start fork in terminal 1: * anvil --fork-url $SEPOLIA_RPC_URL --port 8546 - * + * * # Run test in terminal 2: * CHAIN=sepolia forge script script/TestEmergencyRoleTransferOnFork.s.sol:TestEmergencyRoleTransferOnFork \ * --rpc-url http://localhost:8546 \ @@ -70,11 +70,11 @@ contract TestEmergencyRoleTransferOnFork is Script { function run() external { // Get chain from environment (defaults to arbitrum_sepolia for backward compatibility) chain = vm.envOr("CHAIN", string("arbitrum_sepolia")); - + // Load config ConfigLib.CommonConfigParams memory params = ConfigLib.readCommonConfig(chain); isApprovalRequired = params.approvalRequired; - + // Initialize contracts based on chain if (isApprovalRequired) { // Ethereum Sepolia: Use RLCLiquidityUnifier @@ -85,10 +85,10 @@ contract TestEmergencyRoleTransferOnFork is Script { tokenContract = IAccessControlDefaultAdminRules(params.rlcCrosschainTokenAddress); console.log("Using RLCCrosschainToken at:", params.rlcCrosschainTokenAddress); } - + iexecLayerZeroBridge = IexecLayerZeroBridge(params.iexecLayerZeroBridgeAddress); console.log("Using IexecLayerZeroBridge at:", params.iexecLayerZeroBridgeAddress); - + // Generate a new address for testing newAddress = makeAddr("newSecureAddress"); @@ -182,9 +182,7 @@ contract TestEmergencyRoleTransferOnFork is Script { require( !iexecLayerZeroBridge.hasRole(DEFAULT_ADMIN_ROLE, newAddress), "New address already has admin on bridge" ); - require( - !iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAddress), "New address already has upgrader on bridge" - ); + require(!iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAddress), "New address already has upgrader on bridge"); require(!iexecLayerZeroBridge.hasRole(PAUSER_ROLE, newAddress), "New address already has pauser on bridge"); console.log("OK New address has no roles (as expected)"); @@ -198,7 +196,7 @@ contract TestEmergencyRoleTransferOnFork is Script { // Grant roles on Token Contract string memory tokenName = isApprovalRequired ? "RLCLiquidityUnifier" : "RLCCrosschainToken"; console.log(string(abi.encodePacked("Granting roles on ", tokenName, "..."))); - + tokenContract.grantRole(UPGRADER_ROLE, newAddress); console.log(" - UPGRADER_ROLE granted"); @@ -252,7 +250,7 @@ contract TestEmergencyRoleTransferOnFork is Script { // New address should have non-admin roles console.log("Checking new address has operational roles..."); require(tokenContract.hasRole(UPGRADER_ROLE, newAddress), "New address missing UPGRADER_ROLE on token"); - + // TOKEN_BRIDGE_ROLE only exists on RLCCrosschainToken (not RLCLiquidityUnifier) if (!isApprovalRequired) { require( @@ -260,9 +258,7 @@ contract TestEmergencyRoleTransferOnFork is Script { ); } - require( - iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAddress), "New address missing UPGRADER_ROLE on bridge" - ); + require(iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAddress), "New address missing UPGRADER_ROLE on bridge"); // New address should NOT have admin role yet require( @@ -363,18 +359,18 @@ contract TestEmergencyRoleTransferOnFork is Script { // New address should have all roles console.log("Checking new address has all transferred roles..."); require( - tokenContract.hasRole(DEFAULT_ADMIN_ROLE, newAddress), + tokenContract.hasRole(DEFAULT_ADMIN_ROLE, newAddress), string(abi.encodePacked("New address missing DEFAULT_ADMIN_ROLE on ", tokenName)) ); require( - tokenContract.hasRole(UPGRADER_ROLE, newAddress), + tokenContract.hasRole(UPGRADER_ROLE, newAddress), string(abi.encodePacked("New address missing UPGRADER_ROLE on ", tokenName)) ); - + // TOKEN_BRIDGE_ROLE only exists on RLCCrosschainToken if (!isApprovalRequired) { require( - tokenContract.hasRole(TOKEN_BRIDGE_ROLE, newAddress), + tokenContract.hasRole(TOKEN_BRIDGE_ROLE, newAddress), string(abi.encodePacked("New address missing TOKEN_BRIDGE_ROLE on ", tokenName)) ); } @@ -383,9 +379,7 @@ contract TestEmergencyRoleTransferOnFork is Script { iexecLayerZeroBridge.hasRole(DEFAULT_ADMIN_ROLE, newAddress), "New address missing DEFAULT_ADMIN_ROLE on bridge" ); - require( - iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAddress), "New address missing UPGRADER_ROLE on bridge" - ); + require(iexecLayerZeroBridge.hasRole(UPGRADER_ROLE, newAddress), "New address missing UPGRADER_ROLE on bridge"); console.log(string(abi.encodePacked(tokenName, " - New address:"))); console.log(" DEFAULT_ADMIN_ROLE: OK"); @@ -405,7 +399,7 @@ contract TestEmergencyRoleTransferOnFork is Script { string(abi.encodePacked("Old address still has DEFAULT_ADMIN_ROLE on ", tokenName)) ); require( - !tokenContract.hasRole(UPGRADER_ROLE, OLD_ADDRESS), + !tokenContract.hasRole(UPGRADER_ROLE, OLD_ADDRESS), string(abi.encodePacked("Old address still has UPGRADER_ROLE on ", tokenName)) ); diff --git a/script/TransferAllRoles.s.sol b/script/TransferAllRoles.s.sol index f2ab7f3f..777cf15f 100644 --- a/script/TransferAllRoles.s.sol +++ b/script/TransferAllRoles.s.sol @@ -46,23 +46,17 @@ contract GrantRolesAndBeginAdminTransfer is Script { ConfigLib.CommonConfigParams memory params = ConfigLib.readCommonConfig(chain); vm.startBroadcast(); - + if (!params.approvalRequired) { console.log("Processing RLCCrosschainToken..."); grantRolesAndBeginAdminTransfer( - params.rlcCrosschainTokenAddress, - oldAddress, - newAddress, - "RLCCrosschainToken" + params.rlcCrosschainTokenAddress, oldAddress, newAddress, "RLCCrosschainToken" ); console.log(""); } else { console.log("Processing RLCLiquidityUnifier..."); grantRolesAndBeginAdminTransfer( - params.rlcLiquidityUnifierAddress, - oldAddress, - newAddress, - "RLCLiquidityUnifier" + params.rlcLiquidityUnifierAddress, oldAddress, newAddress, "RLCLiquidityUnifier" ); console.log(""); } @@ -70,14 +64,11 @@ contract GrantRolesAndBeginAdminTransfer is Script { // Process IexecLayerZeroBridge console.log("Processing IexecLayerZeroBridge..."); grantRolesAndBeginAdminTransfer( - params.iexecLayerZeroBridgeAddress, - oldAddress, - newAddress, - "IexecLayerZeroBridge" + params.iexecLayerZeroBridgeAddress, oldAddress, newAddress, "IexecLayerZeroBridge" ); vm.stopBroadcast(); - + console.log(""); console.log("=== Step 1 Complete ==="); console.log("Next steps:"); @@ -99,7 +90,7 @@ contract GrantRolesAndBeginAdminTransfer is Script { string memory contractName ) internal { IAccessControlDefaultAdminRules contractInstance = IAccessControlDefaultAdminRules(contractAddress); - + // Verify old address has admin role bytes32 defaultAdminRole = 0x00; require( @@ -110,7 +101,7 @@ contract GrantRolesAndBeginAdminTransfer is Script { // Get role identifiers bytes32 upgraderRole = keccak256("UPGRADER_ROLE"); bytes32 pauserRole = keccak256("PAUSER_ROLE"); - + console.log(" Granting roles to new address..."); if (contractInstance.hasRole(upgraderRole, oldAddress)) { contractInstance.grantRole(upgraderRole, newAddress); @@ -123,7 +114,7 @@ contract GrantRolesAndBeginAdminTransfer is Script { console.log(" Beginning DEFAULT_ADMIN_ROLE transfer..."); contractInstance.beginDefaultAdminTransfer(newAddress); - + (address pendingAdmin, uint48 schedule) = contractInstance.pendingDefaultAdmin(); console.log(" - Pending admin:", pendingAdmin); console.log(" - Transfer scheduled for:", uint256(schedule)); @@ -164,33 +155,21 @@ contract AcceptAdminRoleAndRevokeOldRoles is Script { ConfigLib.CommonConfigParams memory params = ConfigLib.readCommonConfig(chain); vm.startBroadcast(); - + if (!params.approvalRequired) { console.log("Processing RLCCrosschainToken..."); - acceptAdminAndRevokeOldRoles( - params.rlcCrosschainTokenAddress, - oldAddress, - "RLCCrosschainToken" - ); + acceptAdminAndRevokeOldRoles(params.rlcCrosschainTokenAddress, oldAddress, "RLCCrosschainToken"); console.log(""); } else { console.log("Processing RLCLiquidityUnifier..."); - acceptAdminAndRevokeOldRoles( - params.rlcLiquidityUnifierAddress, - oldAddress, - "RLCLiquidityUnifier" - ); + acceptAdminAndRevokeOldRoles(params.rlcLiquidityUnifierAddress, oldAddress, "RLCLiquidityUnifier"); console.log(""); } console.log("Processing IexecLayerZeroBridge..."); - acceptAdminAndRevokeOldRoles( - params.iexecLayerZeroBridgeAddress, - oldAddress, - "IexecLayerZeroBridge" - ); + acceptAdminAndRevokeOldRoles(params.iexecLayerZeroBridgeAddress, oldAddress, "IexecLayerZeroBridge"); vm.stopBroadcast(); - + console.log(""); console.log("=== Step 2 Complete ==="); console.log("All roles have been transferred to the new address!"); @@ -203,28 +182,26 @@ contract AcceptAdminRoleAndRevokeOldRoles is Script { * @param oldAddress The old (compromised) address to revoke roles from * @param contractName The name of the contract for logging */ - function acceptAdminAndRevokeOldRoles( - address contractAddress, - address oldAddress, - string memory contractName - ) internal { + function acceptAdminAndRevokeOldRoles(address contractAddress, address oldAddress, string memory contractName) + internal + { IAccessControlDefaultAdminRules contractInstance = IAccessControlDefaultAdminRules(contractAddress); - + // Accept admin role console.log(" Accepting DEFAULT_ADMIN_ROLE..."); contractInstance.acceptDefaultAdminTransfer(); - + address newAdmin = contractInstance.defaultAdmin(); console.log(" - New admin confirmed:", newAdmin); require(newAdmin != oldAddress, string(abi.encodePacked(contractName, ": Admin not transferred"))); - + // Revoke all roles from old address console.log(" Revoking all roles from old address..."); - + bytes32 upgraderRole = keccak256("UPGRADER_ROLE"); bytes32 pauserRole = keccak256("PAUSER_ROLE"); bytes32 defaultAdminRole = 0x00; - + if (contractInstance.hasRole(upgraderRole, oldAddress)) { contractInstance.revokeRole(upgraderRole, oldAddress); console.log(" - UPGRADER_ROLE revoked"); @@ -233,15 +210,17 @@ contract AcceptAdminRoleAndRevokeOldRoles is Script { contractInstance.revokeRole(pauserRole, oldAddress); console.log(" - PAUSER_ROLE revoked"); } - + // Verify old address no longer has any roles bool hasUpgrader = contractInstance.hasRole(upgraderRole, oldAddress); bool hasPauser = contractInstance.hasRole(pauserRole, oldAddress); bool hasAdmin = contractInstance.hasRole(defaultAdminRole, oldAddress); - - require(!hasUpgrader && !hasPauser && !hasAdmin, - string(abi.encodePacked(contractName, ": Old address still has roles"))); - + + require( + !hasUpgrader && !hasPauser && !hasAdmin, + string(abi.encodePacked(contractName, ": Old address still has roles")) + ); + console.log(" - All roles successfully revoked from old address"); } } diff --git a/test/units/TransferAllRolesScript.t.sol b/test/units/TransferAllRolesScript.t.sol index dbc70a3e..102d1d15 100644 --- a/test/units/TransferAllRolesScript.t.sol +++ b/test/units/TransferAllRolesScript.t.sol @@ -49,8 +49,8 @@ contract TransferAllRolesScriptTest is TestHelperOz5 { // Token roles rlcCrosschainToken.grantRole(PAUSER_ROLE, oldAdmin); rlcCrosschainToken.grantRole(TOKEN_BRIDGE_ROLE, address(iexecLayerZeroBridge)); - - // Bridge roles + + // Bridge roles iexecLayerZeroBridge.grantRole(PAUSER_ROLE, oldAdmin); vm.stopPrank(); } From bfdaa726dd60bd28fa3423288b9c01bad67c1f6a Mon Sep 17 00:00:00 2001 From: gfournieriExec Date: Fri, 21 Nov 2025 13:32:52 +0100 Subject: [PATCH 8/8] fix: update sender address variable in SendFromArbitrumToEthereum and SendFromEthereumToArbitrum scripts --- .env.template | 1 + script/SendFromArbitrumToEthereum.s.sol | 2 +- script/SendFromEthereumToArbitrum.s.sol | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.env.template b/.env.template index f06d8e55..df440c2b 100644 --- a/.env.template +++ b/.env.template @@ -37,6 +37,7 @@ ETHERSCAN_API_KEY= # =========================================== # Recipient address for cross-chain transfers RECIPIENT_ADDRESS= +SENDER_ADDRESS= # =========================================== # ADMIN CONFIGURATION diff --git a/script/SendFromArbitrumToEthereum.s.sol b/script/SendFromArbitrumToEthereum.s.sol index 1039eb23..bf386cf5 100644 --- a/script/SendFromArbitrumToEthereum.s.sol +++ b/script/SendFromArbitrumToEthereum.s.sol @@ -40,7 +40,7 @@ contract SendFromArbitrumToEthereum is Script { IexecLayerZeroBridge sourceBridge = IexecLayerZeroBridge(sourceParams.iexecLayerZeroBridgeAddress); IERC20 rlcToken = IERC20(sourceParams.rlcCrosschainTokenAddress); - address sender = vm.envAddress("RECIPIENT_ADDRESS"); + address sender = vm.envAddress("SENDER_ADDRESS"); address recipient = vm.envAddress("RECIPIENT_ADDRESS"); // Check sender's balance diff --git a/script/SendFromEthereumToArbitrum.s.sol b/script/SendFromEthereumToArbitrum.s.sol index ac707272..079dc1a9 100644 --- a/script/SendFromEthereumToArbitrum.s.sol +++ b/script/SendFromEthereumToArbitrum.s.sol @@ -36,7 +36,7 @@ contract SendFromEthereumToArbitrum is Script { IexecLayerZeroBridge sourceBridge = IexecLayerZeroBridge(sourceParams.iexecLayerZeroBridgeAddress); IERC20 rlcToken = IERC20(sourceParams.rlcToken); - address sender = vm.envAddress("RECIPIENT_ADDRESS"); + address sender = vm.envAddress("SENDER_ADDRESS"); address recipient = vm.envAddress("RECIPIENT_ADDRESS"); // Check sender's balance