Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ ETHERSCAN_API_KEY=
# ===========================================
# Recipient address for cross-chain transfers
RECIPIENT_ADDRESS=
SENDER_ADDRESS=

# ===========================================
# ADMIN CONFIGURATION
Expand Down
234 changes: 234 additions & 0 deletions .github/workflows/emergency-role-transfer.yml
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion script/SendFromArbitrumToEthereum.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion script/SendFromEthereumToArbitrum.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading