Skip to content

Commit 597ef5b

Browse files
feat: Add transfer admin role workflow and scripts for multi-chain su… (#90)
Co-authored-by: Zied Guesmi <[email protected]>
1 parent 459e2ed commit 597ef5b

File tree

6 files changed

+479
-0
lines changed

6 files changed

+479
-0
lines changed

.env.template

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,8 @@ ETHERSCAN_API_KEY=
3737
# ===========================================
3838
# Recipient address for cross-chain transfers
3939
RECIPIENT_ADDRESS=
40+
41+
# ===========================================
42+
# ADMIN CONFIGURATION
43+
# ===========================================
44+
NEW_DEFAULT_ADMIN=
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Transfer Default Admin Role
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
network:
7+
description: 'Network to transfer admin role on'
8+
required: true
9+
type: choice
10+
options:
11+
- ethereum
12+
- arbitrum
13+
- sepolia
14+
- arbitrum_sepolia
15+
default: sepolia
16+
new_default_admin_address:
17+
description: 'New admin address'
18+
required: true
19+
type: string
20+
21+
jobs:
22+
# TODO: check if setup-matrix is needed
23+
transfer-admin:
24+
runs-on: ubuntu-latest
25+
environment: ${{ inputs.network }}
26+
27+
steps:
28+
- uses: actions/checkout@v4
29+
with:
30+
submodules: recursive
31+
32+
- name: Install Foundry
33+
uses: foundry-rs/foundry-toolchain@v1
34+
with:
35+
version: stable
36+
cache: true
37+
38+
- name: Transfer default admin role
39+
env:
40+
ADMIN_PRIVATE_KEY: ${{ secrets.ADMIN_PRIVATE_KEY }}
41+
CHAIN: ${{ inputs.network }}
42+
RPC_URL: ${{ secrets.RPC_URL }}
43+
NEW_DEFAULT_ADMIN: ${{ inputs.new_default_admin_address }}
44+
run: make begin-default-admin-transfer

Makefile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,25 @@ send-tokens-to-ethereum-mainnet:
193193
--account $(ACCOUNT) \
194194
--broadcast \
195195
-vvv
196+
197+
#
198+
# Admin role transfer operations
199+
#
200+
201+
# Transfer admin role for a single chain
202+
begin-default-admin-transfer: # CHAIN, RPC_URL, NEW_DEFAULT_ADMIN
203+
@echo "Transferring admin role on $(CHAIN) to: $(NEW_DEFAULT_ADMIN)"
204+
CHAIN=$(CHAIN) NEW_DEFAULT_ADMIN=$(NEW_DEFAULT_ADMIN) forge script script/TransferAdminRole.s.sol:BeginTransferAdminRole \
205+
--rpc-url $(RPC_URL) \
206+
$$(if [ "$(CI)" = "true" ]; then echo "--private-key $(ADMIN_PRIVATE_KEY)"; else echo "--account $(ACCOUNT)"; fi) \
207+
--broadcast \
208+
-vvv
209+
210+
# Accept admin role for a single chain (run by new admin)
211+
accept-default-admin-transfer: # CHAIN, RPC_URL
212+
@echo "Accepting admin role on $(CHAIN)"
213+
CHAIN=$(CHAIN) forge script script/TransferAdminRole.s.sol:AcceptAdminRole \
214+
--rpc-url $(RPC_URL) \
215+
$$(if [ "$(CI)" = "true" ]; then echo "--private-key $(NEW_DEFAULT_ADMIN_PRIVATE_KEY)"; else echo "--account $(ACCOUNT)"; fi) \
216+
--broadcast \
217+
-vvv

script/TransferAdminRole.s.sol

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH <[email protected]>
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
pragma solidity ^0.8.22;
5+
6+
import {Script} from "forge-std/Script.sol";
7+
import {console} from "forge-std/console.sol";
8+
import {IAccessControlDefaultAdminRules} from
9+
"@openzeppelin/contracts/access/extensions/IAccessControlDefaultAdminRules.sol";
10+
import {ConfigLib} from "./lib/ConfigLib.sol";
11+
import {RLCLiquidityUnifier} from "../src/RLCLiquidityUnifier.sol";
12+
import {RLCCrosschainToken} from "../src/RLCCrosschainToken.sol";
13+
import {IexecLayerZeroBridge} from "../src/bridges/layerZero/IexecLayerZeroBridge.sol";
14+
15+
/**
16+
* @title BeginTransferAdminRole
17+
* @dev Script to transfer the default admin role to a new admin address
18+
* for all deployed smart contracts on the current chain.
19+
*/
20+
contract BeginTransferAdminRole is Script {
21+
/**
22+
* @notice Transfers the default admin role to a new admin for all contracts on the current chain
23+
* @dev This function automatically detects which contracts are deployed on the current chain
24+
* based on the configuration and transfers admin roles accordingly
25+
*/
26+
function run() external virtual {
27+
address newAdmin = vm.envAddress("NEW_DEFAULT_ADMIN");
28+
string memory chain = vm.envString("CHAIN");
29+
console.log("Starting admin role transfer on chain:", chain);
30+
console.log("New admin address:", newAdmin);
31+
32+
ConfigLib.CommonConfigParams memory params = ConfigLib.readCommonConfig(chain);
33+
vm.startBroadcast();
34+
beginTransferForAllContracts(params, newAdmin);
35+
vm.stopBroadcast();
36+
}
37+
/**
38+
* @notice Validates that the new admin is different from the current admin
39+
* @param currentDefaultAdmin The current admin address
40+
* @param newAdmin The new admin address
41+
*/
42+
43+
function validateAdminTransfer(address currentDefaultAdmin, address newAdmin) internal pure {
44+
require(newAdmin != address(0), "BeginTransferAdminRole: new admin cannot be zero address");
45+
require(
46+
newAdmin != currentDefaultAdmin, "BeginTransferAdminRole: New admin must be different from current admin"
47+
);
48+
}
49+
50+
/**
51+
* @notice Begins the admin transfer process for all relevant contracts
52+
* @param params The configuration parameters for the current chain
53+
* @param newAdmin The new admin address
54+
*/
55+
function beginTransferForAllContracts(ConfigLib.CommonConfigParams memory params, address newAdmin) internal {
56+
if (params.approvalRequired) {
57+
beginTransfer(params.rlcLiquidityUnifierAddress, newAdmin, "RLCLiquidityUnifier");
58+
} else {
59+
beginTransfer(params.rlcCrosschainTokenAddress, newAdmin, "RLCCrosschainToken");
60+
}
61+
beginTransfer(params.iexecLayerZeroBridgeAddress, newAdmin, "IexecLayerZeroBridge");
62+
}
63+
64+
/**
65+
* @notice Transfers the default admin role for any contract implementing IAccessControlDefaultAdminRules
66+
* @param contractAddress The address of the contract
67+
* @param newAdmin The new admin address
68+
* @param contractName The name of the contract for logging purposes
69+
*/
70+
function beginTransfer(address contractAddress, address newAdmin, string memory contractName) public virtual {
71+
IAccessControlDefaultAdminRules contractInstance = IAccessControlDefaultAdminRules(contractAddress);
72+
73+
address currentAdmin = contractInstance.defaultAdmin();
74+
console.log("Current admin for", contractName, ":", currentAdmin);
75+
validateAdminTransfer(currentAdmin, newAdmin);
76+
contractInstance.beginDefaultAdminTransfer(newAdmin);
77+
console.log("Admin transfer initiated for", contractName, "at:", contractAddress);
78+
}
79+
}
80+
81+
/**
82+
* @title AcceptAdminRole
83+
* @dev Script to accept the default admin role transfer for all contracts on the current chain.
84+
* This script should be run by the new admin after the BeginTransferAdminRole script has been executed.
85+
*/
86+
contract AcceptAdminRole is Script {
87+
/**
88+
* @notice Accepts the default admin role transfer for all contracts on the current chain
89+
* @dev This function should be called by the new admin to complete the transfer process
90+
*/
91+
function run() external virtual {
92+
string memory chain = vm.envString("CHAIN");
93+
console.log("Accepting admin role transfer on chain:", chain);
94+
ConfigLib.CommonConfigParams memory params = ConfigLib.readCommonConfig(chain);
95+
96+
vm.startBroadcast();
97+
acceptAdminRoleTransfer(params);
98+
vm.stopBroadcast();
99+
}
100+
101+
/**
102+
* @notice Accepts the default admin role transfer for all contracts on the current chain
103+
* @dev This function should be called by the new admin to complete the transfer process
104+
*/
105+
function acceptAdminRoleTransfer(ConfigLib.CommonConfigParams memory params) internal {
106+
if (params.approvalRequired) {
107+
acceptContractAdmin(params.rlcLiquidityUnifierAddress, "RLCLiquidityUnifier");
108+
} else {
109+
acceptContractAdmin(params.rlcCrosschainTokenAddress, "RLCCrosschainToken");
110+
}
111+
acceptContractAdmin(params.iexecLayerZeroBridgeAddress, "IexecLayerZeroBridge");
112+
}
113+
114+
/**
115+
* @notice Accepts the default admin role transfer for any contract implementing IAccessControlDefaultAdminRules
116+
* @param contractAddress The address of the contract
117+
* @param contractName The name of the contract for logging purposes
118+
*/
119+
function acceptContractAdmin(address contractAddress, string memory contractName) internal virtual {
120+
console.log("Accepting admin role for", contractName, "at:", contractAddress);
121+
IAccessControlDefaultAdminRules contractInstance = IAccessControlDefaultAdminRules(contractAddress);
122+
contractInstance.acceptDefaultAdminTransfer();
123+
console.log("New admin for", contractName, ":", contractInstance.defaultAdmin());
124+
}
125+
}

0 commit comments

Comments
 (0)