Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
14 changes: 14 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,17 @@ MULTISIG_OWNER_2_TESTNET=<owner2_address>
MULTISIG_OWNER_3_TESTNET=<owner3_address>
MULTISIG_OWNER_4_TESTNET=<owner4_address>
MULTISIG_OWNER_5_TESTNET=<owner5_address>

# Timelock configuration for legacy LBC ownership
# Delay is in seconds. Temporary default in code is 7 days.
TIMELOCK_MIN_DELAY_MAINNET=<delay_seconds>
TIMELOCK_PROPOSER_MAINNET=<proposer_multisig_address>
TIMELOCK_EXECUTOR_MAINNET=<executor_multisig_address>

TIMELOCK_MIN_DELAY_TESTNET=<delay_seconds>
TIMELOCK_PROPOSER_TESTNET=<proposer_multisig_address>
TIMELOCK_EXECUTOR_TESTNET=<executor_multisig_address>

TIMELOCK_MIN_DELAY_LOCAL=<delay_seconds>
TIMELOCK_PROPOSER_LOCAL=<proposer_multisig_address>
TIMELOCK_EXECUTOR_LOCAL=<executor_multisig_address>
102 changes: 98 additions & 4 deletions script/HelperConfig.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ contract HelperConfig is Script {
bool mainnet;
address existingProxy;
address existingAdmin;
uint256 timelockMinDelay;
address timelockProposer;
address timelockExecutor;
address timelockAdmin;
}

/// @notice Configuration for the new Flyover contracts
Expand All @@ -29,6 +33,10 @@ contract HelperConfig is Script {
uint256 btcBlockTime;
bool mainnet;
uint48 adminDelay;
uint256 timelockMinDelay;
address timelockProposer;
address timelockExecutor;
address timelockAdmin;
}

NetworkConfig private cachedConfig;
Expand Down Expand Up @@ -94,7 +102,26 @@ contract HelperConfig is Script {
btcBlockTime: vm.envOr("BTC_BLOCK_TIME_MAINNET", uint256(600)),
mainnet: true,
existingProxy: vm.envOr("EXISTING_PROXY_MAINNET", address(0)),
existingAdmin: vm.envOr("EXISTING_ADMIN_MAINNET", address(0))
existingAdmin: vm.envOr("EXISTING_ADMIN_MAINNET", address(0)),
timelockMinDelay: vm.envOr(
"TIMELOCK_MIN_DELAY_MAINNET",
uint256(7 days)
),
timelockProposer: vm.envOr(
"TIMELOCK_PROPOSER_MAINNET",
vm.envOr(
"MULTISIG_ADDRESS_MAINNET",
address(0x633D1233eD6251108b61A8365CEEd271BF3e3C9b)
)
),
timelockExecutor: vm.envOr(
"TIMELOCK_EXECUTOR_MAINNET",
vm.envOr(
"MULTISIG_ADDRESS_MAINNET",
address(0x633D1233eD6251108b61A8365CEEd271BF3e3C9b)
)
),
timelockAdmin: vm.envOr("TIMELOCK_ADMIN_MAINNET", address(0))
});
}

Expand Down Expand Up @@ -134,7 +161,26 @@ contract HelperConfig is Script {
existingAdmin: vm.envOr(
"EXISTING_ADMIN_TESTNET",
address(0x93891ACe405cC4F7b9974C22e34D6479eE6425e5)
)
),
timelockMinDelay: vm.envOr(
"TIMELOCK_MIN_DELAY_TESTNET",
uint256(7 days)
),
timelockProposer: vm.envOr(
"TIMELOCK_PROPOSER_TESTNET",
vm.envOr(
"MULTISIG_ADDRESS_TESTNET",
address(0x27ad02ABf893F8e01f0089EDE607A76FbB3F1Cd3)
)
),
timelockExecutor: vm.envOr(
"TIMELOCK_EXECUTOR_TESTNET",
vm.envOr(
"MULTISIG_ADDRESS_TESTNET",
address(0x27ad02ABf893F8e01f0089EDE607A76FbB3F1Cd3)
)
),
timelockAdmin: vm.envOr("TIMELOCK_ADMIN_TESTNET", address(0))
});
}

Expand Down Expand Up @@ -163,7 +209,20 @@ contract HelperConfig is Script {
btcBlockTime: vm.envOr("BTC_BLOCK_TIME_LOCAL", uint256(600)),
mainnet: false,
existingProxy: vm.envOr("EXISTING_PROXY_LOCAL", address(0)),
existingAdmin: vm.envOr("EXISTING_ADMIN_LOCAL", address(0))
existingAdmin: vm.envOr("EXISTING_ADMIN_LOCAL", address(0)),
timelockMinDelay: vm.envOr(
"TIMELOCK_MIN_DELAY_LOCAL",
uint256(7 days)
),
timelockProposer: vm.envOr(
"TIMELOCK_PROPOSER_LOCAL",
address(0x1000000000000000000000000000000000000001)
),
timelockExecutor: vm.envOr(
"TIMELOCK_EXECUTOR_LOCAL",
address(0x1000000000000000000000000000000000000002)
),
timelockAdmin: vm.envOr("TIMELOCK_ADMIN_LOCAL", address(0))
});
}

Expand Down Expand Up @@ -231,6 +290,28 @@ contract HelperConfig is Script {
mainnet: isMainnet,
adminDelay: uint48(
vm.envOr(string.concat("ADMIN_DELAY_", suffix), uint256(0))
),
timelockMinDelay: vm.envOr(
string.concat("TIMELOCK_MIN_DELAY_", suffix),
uint256(7 days)
),
timelockProposer: vm.envOr(
string.concat("TIMELOCK_PROPOSER_", suffix),
vm.envOr(
string.concat("MULTISIG_ADDRESS_", suffix),
address(0)
)
),
timelockExecutor: vm.envOr(
string.concat("TIMELOCK_EXECUTOR_", suffix),
vm.envOr(
string.concat("MULTISIG_ADDRESS_", suffix),
address(0)
)
),
timelockAdmin: vm.envOr(
string.concat("TIMELOCK_ADMIN_", suffix),
address(0)
)
});
}
Expand All @@ -255,7 +336,20 @@ contract HelperConfig is Script {
),
btcBlockTime: vm.envOr("BTC_BLOCK_TIME_LOCAL", uint256(600)),
mainnet: false,
adminDelay: uint48(vm.envOr("ADMIN_DELAY_LOCAL", uint256(0)))
adminDelay: uint48(vm.envOr("ADMIN_DELAY_LOCAL", uint256(0))),
timelockMinDelay: vm.envOr(
"TIMELOCK_MIN_DELAY_LOCAL",
uint256(7 days)
),
timelockProposer: vm.envOr(
"TIMELOCK_PROPOSER_LOCAL",
address(0x1000000000000000000000000000000000000001)
),
timelockExecutor: vm.envOr(
"TIMELOCK_EXECUTOR_LOCAL",
address(0x1000000000000000000000000000000000000002)
),
timelockAdmin: vm.envOr("TIMELOCK_ADMIN_LOCAL", address(0))
});
}
}
153 changes: 153 additions & 0 deletions script/deployment/ChangeOwnerToTimelock.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import {Script, console} from "lib/forge-std/src/Script.sol";
import {HelperConfig} from "../HelperConfig.s.sol";
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

/// @title ChangeOwnerToTimelock (Split Architecture)
/// @notice Deploys a TimelockController and transfers ownership of the shared
/// ProxyAdmin to it, gating contract upgrades behind a time delay.
/// @dev Only the ProxyAdmin is transferred to the timelock. The DEFAULT_ADMIN_ROLE
/// on individual contracts (CollateralManagement, PegIn, PegOut, FlyoverDiscovery)
/// is intentionally left unchanged so that time-sensitive operations like
/// emergency pause can be executed immediately.
/// Proposers and executors are read from timelock-roles.json, falling back
/// to the single-address config values if the file is not available.
contract ChangeOwnerToTimelock is Script {
error NoProposersConfigured();
error NoExecutorsConfigured();
error ProxyAdminAddressNotProvided();
error ProxyAdminOwnerTransferFailed();

function run() external {
HelperConfig helper = new HelperConfig();
HelperConfig.FlyoverConfig memory cfg = helper.getFlyoverConfig();
uint256 deployerKey = helper.getDeployerPrivateKey();
vm.rememberKey(deployerKey);

address proxyAdminAddress = vm.envAddress("PROXY_ADMIN");
if (proxyAdminAddress == address(0)) {
revert ProxyAdminAddressNotProvided();
}

(address[] memory proposers, address[] memory executors) = _readRoles(
cfg
);

vm.startBroadcast(deployerKey);
TimelockController timelock = execute(
proxyAdminAddress,
cfg.timelockMinDelay,
proposers,
executors,
cfg.timelockAdmin
);
vm.stopBroadcast();

_logFinalState(proxyAdminAddress, timelock, proposers, executors);
}

/// @notice Core logic: deploys a TimelockController and transfers ProxyAdmin
/// ownership to it. No console.log calls -- safe for broadcast.
function execute(
address proxyAdminAddress,
uint256 minDelay,
address[] memory proposers,
address[] memory executors,
address admin
) public returns (TimelockController) {
if (proposers.length == 0) {
revert NoProposersConfigured();
}
if (executors.length == 0) {
revert NoExecutorsConfigured();
}

TimelockController timelock = new TimelockController(
minDelay,
proposers,
executors,
admin
);

ProxyAdmin proxyAdmin = ProxyAdmin(proxyAdminAddress);
if (proxyAdmin.owner() != address(timelock)) {
proxyAdmin.transferOwnership(address(timelock));
if (proxyAdmin.owner() != address(timelock)) {
revert ProxyAdminOwnerTransferFailed();
}
}

return timelock;
}

function _readRoles(
HelperConfig.FlyoverConfig memory cfg
)
internal
view
returns (address[] memory proposers, address[] memory executors)
{
string memory json = vm.readFile(
"script/deployment/timelock-roles.json"
);
string memory networkKey = _networkKey();

proposers = vm.parseJsonAddressArray(
json,
string.concat(".", networkKey, ".proposers")
);
executors = vm.parseJsonAddressArray(
json,
string.concat(".", networkKey, ".executors")
);

if (proposers.length == 0 && cfg.timelockProposer != address(0)) {
proposers = new address[](1);
proposers[0] = cfg.timelockProposer;
}
if (executors.length == 0 && cfg.timelockExecutor != address(0)) {
executors = new address[](1);
executors[0] = cfg.timelockExecutor;
}
}

function _networkKey() internal view returns (string memory) {
uint256 chainId = block.chainid;
if (chainId == 30) return "rskMainnet";
if (chainId == 31) return "rskTestnet";
return "rskRegtest";
}

function _logFinalState(
address proxyAdminAddress,
TimelockController timelock,
address[] memory proposers,
address[] memory executors
) internal view {
console.log("=== Timelock ownership setup complete ===");
console.log("Timelock:", address(timelock));
console.log("Timelock minDelay:", timelock.getMinDelay());
console.log("ProxyAdmin:", proxyAdminAddress);
console.log("ProxyAdmin owner:", ProxyAdmin(proxyAdminAddress).owner());

bytes32 proposerRole = timelock.PROPOSER_ROLE();
bytes32 executorRole = timelock.EXECUTOR_ROLE();
for (uint256 i = 0; i < proposers.length; i++) {
console.log(
"Proposer:",
proposers[i],
timelock.hasRole(proposerRole, proposers[i])
);
}
for (uint256 i = 0; i < executors.length; i++) {
console.log(
"Executor:",
executors[i],
timelock.hasRole(executorRole, executors[i])
);
}
}
}
10 changes: 10 additions & 0 deletions script/deployment/timelock-roles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"rskMainnet": {
"proposers": ["0x633D1233eD6251108b61A8365CEEd271BF3e3C9b"],
"executors": ["0x633D1233eD6251108b61A8365CEEd271BF3e3C9b"]
},
"rskTestnet": {
"proposers": ["0x27ad02ABf893F8e01f0089EDE607A76FbB3F1Cd3"],
"executors": ["0x27ad02ABf893F8e01f0089EDE607A76FbB3F1Cd3"]
}
}
Loading
Loading