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/.github/workflows/emergency-role-transfer.yml b/.github/workflows/emergency-role-transfer.yml new file mode 100644 index 00000000..a0251b84 --- /dev/null +++ b/.github/workflows/emergency-role-transfer.yml @@ -0,0 +1,234 @@ +name: Emergency Role Transfer + +on: + workflow_dispatch: + inputs: + execution_mode: + description: 'Execution mode' + required: true + type: choice + options: + - 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 + type: choice + options: + - testnets + - sepolia-only + - arbitrum_sepolia-only + default: testnets + +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: | + 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.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: ${{ vars.OLD_ADDRESS }} + NEW_ADDRESS: ${{ vars.NEW_ADDRESS }} + run: | + 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.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: ${{ vars.OLD_ADDRESS }} + run: | + 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: | + 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 execution_mode=step-2-only" + + else + 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 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/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 diff --git a/script/TestEmergencyRoleTransferOnFork.s.sol b/script/TestEmergencyRoleTransferOnFork.s.sol new file mode 100644 index 00000000..74a0fe25 --- /dev/null +++ b/script/TestEmergencyRoleTransferOnFork.s.sol @@ -0,0 +1,422 @@ +// 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..777cf15f --- /dev/null +++ b/script/TransferAllRoles.s.sol @@ -0,0 +1,226 @@ +// 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(); + + if (!params.approvalRequired) { + console.log("Processing RLCCrosschainToken..."); + grantRolesAndBeginAdminTransfer( + params.rlcCrosschainTokenAddress, oldAddress, newAddress, "RLCCrosschainToken" + ); + console.log(""); + } else { + console.log("Processing RLCLiquidityUnifier..."); + grantRolesAndBeginAdminTransfer( + params.rlcLiquidityUnifierAddress, oldAddress, newAddress, "RLCLiquidityUnifier" + ); + console.log(""); + } + + // Process IexecLayerZeroBridge + console.log("Processing IexecLayerZeroBridge..."); + grantRolesAndBeginAdminTransfer( + params.iexecLayerZeroBridgeAddress, oldAddress, newAddress, "IexecLayerZeroBridge" + ); + + 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 + */ + function grantRolesAndBeginAdminTransfer( + address contractAddress, + address oldAddress, + address newAddress, + string memory contractName + ) 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..."); + if (contractInstance.hasRole(upgraderRole, oldAddress)) { + contractInstance.grantRole(upgraderRole, newAddress); + console.log(" - UPGRADER_ROLE granted"); + } + if (contractInstance.hasRole(pauserRole, oldAddress)) { + contractInstance.grantRole(pauserRole, newAddress); + console.log(" - PAUSER_ROLE granted"); + } + + 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(); + + if (!params.approvalRequired) { + console.log("Processing RLCCrosschainToken..."); + acceptAdminAndRevokeOldRoles(params.rlcCrosschainTokenAddress, oldAddress, "RLCCrosschainToken"); + console.log(""); + } else { + console.log("Processing RLCLiquidityUnifier..."); + acceptAdminAndRevokeOldRoles(params.rlcLiquidityUnifierAddress, oldAddress, "RLCLiquidityUnifier"); + console.log(""); + } + console.log("Processing 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!"); + 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 + */ + 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"); + } + if (contractInstance.hasRole(pauserRole, oldAddress)) { + 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")) + ); + + 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..102d1d15 --- /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" + ); + } +}