Skip to content

Commit c221857

Browse files
committed
feat: Add transfer admin role workflow and scripts for multi-chain support
1 parent 0774e60 commit c221857

File tree

3 files changed

+289
-0
lines changed

3 files changed

+289
-0
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Transfer 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+
17+
jobs:
18+
transfer-admin:
19+
needs: build-and-test
20+
runs-on: ubuntu-latest
21+
environment: ${{ inputs.network }}
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
with:
26+
submodules: recursive
27+
28+
- name: Install Foundry
29+
uses: foundry-rs/foundry-toolchain@v1
30+
with:
31+
version: stable
32+
cache: true
33+
34+
- name: Transfer admin role
35+
env:
36+
ADMIN_PRIVATE_KEY: ${{ secrets.ADMIN_PRIVATE_KEY }}
37+
CHAIN: ${{ inputs.network }}
38+
RPC_URL: ${{ secrets.RPC_URL }}
39+
NEW_ADMIN: ${{ vars.NEW_ADMIN_ADDRESS }}
40+
run: |
41+
make transfer-admin-single-chain

Makefile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,26 @@ 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+
transfer-admin-single-chain: # CHAIN, RPC_URL, NEW_ADMIN
203+
@echo "Transferring admin role on $(CHAIN) to: $(NEW_ADMIN)"
204+
CHAIN=$(CHAIN) forge script script/TransferAdminRole.s.sol:TransferAdminRole \
205+
--rpc-url $(RPC_URL) \
206+
$$(if [ "$(CI)" = "true" ]; then echo "--private-key $(ADMIN_PRIVATE_KEY)"; else echo "--account $(ACCOUNT)"; fi) \
207+
--broadcast \
208+
--sig "run(address)" $(NEW_ADMIN) \
209+
-vvv
210+
211+
# Accept admin role for a single chain (run by new admin)
212+
accept-admin-single-chain: # CHAIN, RPC_URL
213+
@echo "Accepting admin role on $(CHAIN)"
214+
CHAIN=$(CHAIN) forge script script/TransferAdminRole.s.sol:AcceptAdminRole \
215+
--rpc-url $(RPC_URL) \
216+
$$(if [ "$(CI)" = "true" ]; then echo "--private-key $(NEW_ADMIN_PRIVATE_KEY)"; else echo "--account $(ACCOUNT)"; fi) \
217+
--broadcast \
218+
-vvv

script/TransferAdminRole.s.sol

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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 {AccessControlDefaultAdminRulesUpgradeable} from
9+
"@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.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 TransferAdminRole
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 TransferAdminRole is Script {
21+
22+
/**
23+
* @notice Transfers the default admin role to a new admin for all contracts on the current chain
24+
* @param newAdmin The address that will become the new default admin
25+
* @dev This function automatically detects which contracts are deployed on the current chain
26+
* based on the configuration and transfers admin roles accordingly
27+
*/
28+
function run(address newAdmin) external {
29+
require(newAdmin != address(0), "TransferAdminRole: New admin cannot be zero address");
30+
31+
string memory chain = vm.envString("CHAIN");
32+
console.log("Starting admin role transfer on chain:", chain);
33+
console.log("New admin address:", newAdmin);
34+
35+
ConfigLib.CommonConfigParams memory params = ConfigLib.readCommonConfig(chain);
36+
37+
vm.startBroadcast();
38+
39+
// Transfer admin role for contracts deployed on this chain
40+
if (params.approvalRequired) {
41+
// This is a mainnet chain (Ethereum/Sepolia) - has RLCLiquidityUnifier
42+
transferRLCLiquidityUnifierAdmin(params.rlcLiquidityUnifierAddress, newAdmin);
43+
} else {
44+
// This is a Layer 2 chain (Arbitrum/Arbitrum Sepolia) - has RLCCrosschainToken
45+
transferRLCCrosschainTokenAdmin(params.rlcCrosschainTokenAddress, newAdmin);
46+
}
47+
48+
// All chains have IexecLayerZeroBridge
49+
transferIexecLayerZeroBridgeAdmin(params.iexecLayerZeroBridgeAddress, newAdmin);
50+
51+
vm.stopBroadcast();
52+
53+
console.log("Admin role transfer completed successfully on chain:", chain);
54+
}
55+
56+
/**
57+
* @notice Transfers admin role for RLCLiquidityUnifier contract
58+
* @param contractAddress The address of the RLCLiquidityUnifier contract
59+
* @param newAdmin The new admin address
60+
*/
61+
function transferRLCLiquidityUnifierAdmin(address contractAddress, address newAdmin) internal {
62+
require(contractAddress != address(0), "TransferAdminRole: RLCLiquidityUnifier address cannot be zero");
63+
64+
console.log("Transferring admin role for RLCLiquidityUnifier at:", contractAddress);
65+
66+
RLCLiquidityUnifier liquidityUnifier = RLCLiquidityUnifier(contractAddress);
67+
68+
// Get current admin to verify permissions
69+
address currentAdmin = liquidityUnifier.owner();
70+
console.log("Current admin:", currentAdmin);
71+
72+
// Begin the admin transfer process
73+
liquidityUnifier.beginDefaultAdminTransfer(newAdmin);
74+
75+
console.log("Admin transfer initiated for RLCLiquidityUnifier");
76+
console.log("New admin must call acceptDefaultAdminTransfer() to complete the transfer");
77+
}
78+
79+
/**
80+
* @notice Transfers admin role for RLCCrosschainToken contract
81+
* @param contractAddress The address of the RLCCrosschainToken contract
82+
* @param newAdmin The new admin address
83+
*/
84+
function transferRLCCrosschainTokenAdmin(address contractAddress, address newAdmin) internal {
85+
require(contractAddress != address(0), "TransferAdminRole: RLCCrosschainToken address cannot be zero");
86+
87+
console.log("Transferring admin role for RLCCrosschainToken at:", contractAddress);
88+
89+
RLCCrosschainToken crosschainToken = RLCCrosschainToken(contractAddress);
90+
91+
// Get current admin to verify permissions
92+
address currentAdmin = crosschainToken.owner();
93+
console.log("Current admin:", currentAdmin);
94+
95+
// Begin the admin transfer process
96+
crosschainToken.beginDefaultAdminTransfer(newAdmin);
97+
98+
console.log("Admin transfer initiated for RLCCrosschainToken");
99+
console.log("New admin must call acceptDefaultAdminTransfer() to complete the transfer");
100+
}
101+
102+
/**
103+
* @notice Transfers admin role for IexecLayerZeroBridge contract
104+
* @param contractAddress The address of the IexecLayerZeroBridge contract
105+
* @param newAdmin The new admin address
106+
*/
107+
function transferIexecLayerZeroBridgeAdmin(address contractAddress, address newAdmin) internal {
108+
require(contractAddress != address(0), "TransferAdminRole: IexecLayerZeroBridge address cannot be zero");
109+
110+
console.log("Transferring admin role for IexecLayerZeroBridge at:", contractAddress);
111+
112+
IexecLayerZeroBridge bridge = IexecLayerZeroBridge(contractAddress);
113+
114+
// Get current admin to verify permissions
115+
address currentAdmin = bridge.owner();
116+
console.log("Current admin:", currentAdmin);
117+
118+
// Begin the admin transfer process
119+
bridge.beginDefaultAdminTransfer(newAdmin);
120+
121+
console.log("Admin transfer initiated for IexecLayerZeroBridge");
122+
console.log("New admin must call acceptDefaultAdminTransfer() to complete the transfer");
123+
}
124+
}
125+
126+
/**
127+
* @title AcceptAdminRole
128+
* @dev Script to accept the default admin role transfer for all contracts on the current chain.
129+
* This script should be run by the new admin after the TransferAdminRole script has been executed.
130+
*
131+
* Usage:
132+
* forge script script/TransferAdminRole.s.sol:AcceptAdminRole \
133+
* --rpc-url <RPC_URL> \
134+
* --account <NEW_ADMIN_ACCOUNT> \
135+
* --broadcast \
136+
* -vvv
137+
*
138+
* Environment variables required:
139+
* - CHAIN: The chain identifier (e.g., "ethereum", "arbitrum", "sepolia", "arbitrum_sepolia")
140+
*/
141+
contract AcceptAdminRole is Script {
142+
143+
/**
144+
* @notice Accepts the default admin role transfer for all contracts on the current chain
145+
* @dev This function should be called by the new admin to complete the transfer process
146+
*/
147+
function run() external {
148+
string memory chain = vm.envString("CHAIN");
149+
console.log("Accepting admin role transfer on chain:", chain);
150+
151+
ConfigLib.CommonConfigParams memory params = ConfigLib.readCommonConfig(chain);
152+
153+
vm.startBroadcast();
154+
155+
// Accept admin role for contracts deployed on this chain
156+
if (params.approvalRequired) {
157+
// This is a mainnet chain (Ethereum/Sepolia) - has RLCLiquidityUnifier
158+
acceptRLCLiquidityUnifierAdmin(params.rlcLiquidityUnifierAddress);
159+
} else {
160+
// This is a Layer 2 chain (Arbitrum/Arbitrum Sepolia) - has RLCCrosschainToken
161+
acceptRLCCrosschainTokenAdmin(params.rlcCrosschainTokenAddress);
162+
}
163+
164+
// All chains have IexecLayerZeroBridge
165+
acceptIexecLayerZeroBridgeAdmin(params.iexecLayerZeroBridgeAddress);
166+
167+
vm.stopBroadcast();
168+
169+
console.log("Admin role transfer acceptance completed successfully on chain:", chain);
170+
}
171+
172+
/**
173+
* @notice Accepts admin role for RLCLiquidityUnifier contract
174+
* @param contractAddress The address of the RLCLiquidityUnifier contract
175+
*/
176+
function acceptRLCLiquidityUnifierAdmin(address contractAddress) internal {
177+
require(contractAddress != address(0), "AcceptAdminRole: RLCLiquidityUnifier address cannot be zero");
178+
179+
console.log("Accepting admin role for RLCLiquidityUnifier at:", contractAddress);
180+
181+
RLCLiquidityUnifier liquidityUnifier = RLCLiquidityUnifier(contractAddress);
182+
183+
// Accept the admin transfer
184+
liquidityUnifier.acceptDefaultAdminTransfer();
185+
186+
console.log("Admin role accepted for RLCLiquidityUnifier");
187+
console.log("New admin:", liquidityUnifier.owner());
188+
}
189+
190+
/**
191+
* @notice Accepts admin role for RLCCrosschainToken contract
192+
* @param contractAddress The address of the RLCCrosschainToken contract
193+
*/
194+
function acceptRLCCrosschainTokenAdmin(address contractAddress) internal {
195+
require(contractAddress != address(0), "AcceptAdminRole: RLCCrosschainToken address cannot be zero");
196+
197+
console.log("Accepting admin role for RLCCrosschainToken at:", contractAddress);
198+
199+
RLCCrosschainToken crosschainToken = RLCCrosschainToken(contractAddress);
200+
201+
// Accept the admin transfer
202+
crosschainToken.acceptDefaultAdminTransfer();
203+
204+
console.log("Admin role accepted for RLCCrosschainToken");
205+
console.log("New admin:", crosschainToken.owner());
206+
}
207+
208+
/**
209+
* @notice Accepts admin role for IexecLayerZeroBridge contract
210+
* @param contractAddress The address of the IexecLayerZeroBridge contract
211+
*/
212+
function acceptIexecLayerZeroBridgeAdmin(address contractAddress) internal {
213+
require(contractAddress != address(0), "AcceptAdminRole: IexecLayerZeroBridge address cannot be zero");
214+
215+
console.log("Accepting admin role for IexecLayerZeroBridge at:", contractAddress);
216+
217+
IexecLayerZeroBridge bridge = IexecLayerZeroBridge(contractAddress);
218+
219+
// Accept the admin transfer
220+
bridge.acceptDefaultAdminTransfer();
221+
222+
console.log("Admin role accepted for IexecLayerZeroBridge");
223+
console.log("New admin:", bridge.owner());
224+
}
225+
}

0 commit comments

Comments
 (0)