Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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>
55 changes: 53 additions & 2 deletions script/HelperConfig.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ contract HelperConfig is Script {
bool mainnet;
address existingProxy;
address existingAdmin;
uint256 timelockMinDelay;
address timelockProposer;
address timelockExecutor;
}

/// @notice Configuration for the new Flyover contracts
Expand Down Expand Up @@ -101,7 +104,25 @@ 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)
)
)
});
}

Expand Down Expand Up @@ -141,6 +162,24 @@ 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)
)
)
});
}
Expand Down Expand Up @@ -170,7 +209,19 @@ 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)
)
});
}

Expand Down
140 changes: 140 additions & 0 deletions script/legacy/deployment/ChangeOwnerToTimelock.s.sol
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should have this script also for the split architecture, because this is going to be merged for the v2.6.0 when that architecture is supposed to be already deployed

Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// 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 {LiquidityBridgeContractV2} from "../../../src/legacy/LiquidityBridgeContractV2.sol";
import {LiquidityBridgeContractAdmin} from "../../../src/legacy/LiquidityBridgeContractAdmin.sol";

contract ChangeOwnerToTimelock is Script {
bytes32 internal constant ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

error ProxyAddressNotProvided();
error TimelockProposerIsZero();
error TimelockExecutorIsZero();
error OnlyCurrentLbcOwnerCanTransferOwnership();
error ContractOwnerTransferFailed();
error AdminAddressNotFound();
error OnlyCurrentProxyAdminOwnerCanTransferOwnership();
error ProxyAdminOwnerTransferFailed();

function run() external {
HelperConfig helper = new HelperConfig();
HelperConfig.NetworkConfig memory cfg = helper.getConfig();

uint256 deployerKey = helper.getDeployerPrivateKey();
vm.rememberKey(deployerKey);

address proxyAddress = cfg.existingProxy;
if (proxyAddress == address(0)) {
revert ProxyAddressNotProvided();
}
if (cfg.timelockProposer == address(0)) {
revert TimelockProposerIsZero();
}
if (cfg.timelockExecutor == address(0)) {
revert TimelockExecutorIsZero();
}

address[] memory proposers = new address[](1);
proposers[0] = cfg.timelockProposer;

address[] memory executors = new address[](1);
executors[0] = cfg.timelockExecutor;

vm.startBroadcast(deployerKey);

TimelockController timelock = new TimelockController(
cfg.timelockMinDelay,
proposers,
executors,
address(0)
);

_transferContractOwnership(proxyAddress, address(timelock));
_transferProxyAdminOwnership(proxyAddress, address(timelock));

vm.stopBroadcast();

_logFinalState(proxyAddress, timelock, cfg);
}

function _transferContractOwnership(
address proxyAddress,
address timelock
) internal {
LiquidityBridgeContractV2 contract_ = LiquidityBridgeContractV2(
payable(proxyAddress)
);
address currentOwner = contract_.owner();
if (currentOwner != timelock) {
if (currentOwner != msg.sender) {
revert OnlyCurrentLbcOwnerCanTransferOwnership();
}
contract_.transferOwnership(timelock);
}
if (contract_.owner() != timelock) {
revert ContractOwnerTransferFailed();
}
}

function _transferProxyAdminOwnership(
address proxyAddress,
address timelock
) internal {
address adminAddress = address(
uint160(uint256(vm.load(proxyAddress, ADMIN_SLOT)))
);
if (adminAddress == address(0)) {
revert AdminAddressNotFound();
}

LiquidityBridgeContractAdmin admin = LiquidityBridgeContractAdmin(
adminAddress
);
address currentAdminOwner = admin.owner();
if (currentAdminOwner != timelock) {
if (currentAdminOwner != msg.sender) {
revert OnlyCurrentProxyAdminOwnerCanTransferOwnership();
}
admin.transferOwnership(timelock);
}
if (admin.owner() != timelock) {
revert ProxyAdminOwnerTransferFailed();
}
}

function _logFinalState(
address proxyAddress,
TimelockController timelock,
HelperConfig.NetworkConfig memory cfg
) internal view {
LiquidityBridgeContractV2 contract_ = LiquidityBridgeContractV2(
payable(proxyAddress)
);
address adminAddress = address(
uint160(uint256(vm.load(proxyAddress, ADMIN_SLOT)))
);
LiquidityBridgeContractAdmin admin = LiquidityBridgeContractAdmin(
adminAddress
);

console.log("=== Timelock ownership setup complete ===");
console.log("Timelock:", address(timelock));
console.log("Timelock minDelay:", timelock.getMinDelay());
console.log("LBC owner:", contract_.owner());
console.log("Expected timelock:", address(timelock));
console.log("ProxyAdmin owner:", admin.owner());
console.log("Expected timelock:", address(timelock));
console.log(
"Proposer role granted:",
timelock.hasRole(timelock.PROPOSER_ROLE(), cfg.timelockProposer)
);
console.log(
"Executor role granted:",
timelock.hasRole(timelock.EXECUTOR_ROLE(), cfg.timelockExecutor)
);
}
}
150 changes: 150 additions & 0 deletions test/deployment/ChangeOwnerToTimelock.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;

import "lib/forge-std/src/Test.sol";
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
import {HelperConfig} from "../../script/HelperConfig.s.sol";
import {LiquidityBridgeContract} from "../../src/legacy/LiquidityBridgeContract.sol";
import {LiquidityBridgeContractProxy} from "../../src/legacy/LiquidityBridgeContractProxy.sol";
import {LiquidityBridgeContractAdmin} from "../../src/legacy/LiquidityBridgeContractAdmin.sol";

contract ChangeOwnerToTimelockTest is Test {
bytes32 internal constant ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

HelperConfig public helperConfig;
LiquidityBridgeContract public lbcImpl;
LiquidityBridgeContractProxy public proxy;
LiquidityBridgeContractAdmin public admin;
TimelockController public timelock;

address public proposer;
address public executor;
uint256 public minDelay;

function setUp() public {
helperConfig = new HelperConfig();
proposer = makeAddr("proposer");
executor = makeAddr("executor");
minDelay = 7 days;

HelperConfig.NetworkConfig memory cfg = helperConfig.getConfig();

lbcImpl = new LiquidityBridgeContract();
admin = new LiquidityBridgeContractAdmin();

bytes memory initData = abi.encodeCall(
LiquidityBridgeContract.initialize,
(
payable(cfg.bridge),
cfg.minimumCollateral,
cfg.minimumPegIn,
cfg.rewardPercentage,
cfg.resignDelayBlocks,
cfg.dustThreshold,
cfg.btcBlockTime,
cfg.mainnet
)
);
proxy = new LiquidityBridgeContractProxy(
address(lbcImpl),
address(admin),
initData
);

address[] memory proposers = new address[](1);
proposers[0] = proposer;
address[] memory executors = new address[](1);
executors[0] = executor;

timelock = new TimelockController(
minDelay,
proposers,
executors,
address(0)
);
}

function test_TransfersLegacyOwnershipsToTimelock() public {
LiquidityBridgeContract lbcProxy = LiquidityBridgeContract(
payable(address(proxy))
);
LiquidityBridgeContractAdmin actualAdmin = _getProxyAdmin();

lbcProxy.transferOwnership(address(timelock));
vm.prank(actualAdmin.owner());
actualAdmin.transferOwnership(address(timelock));

assertEq(lbcProxy.owner(), address(timelock), "LBC owner mismatch");
assertEq(
actualAdmin.owner(),
address(timelock),
"ProxyAdmin owner mismatch"
);
assertEq(timelock.getMinDelay(), minDelay, "minDelay mismatch");
assertTrue(
timelock.hasRole(timelock.PROPOSER_ROLE(), proposer),
"proposer missing"
);
assertTrue(
timelock.hasRole(timelock.EXECUTOR_ROLE(), executor),
"executor missing"
);
}

function test_OwnerOnlyOperationIsDelayedByTimelock() public {
LiquidityBridgeContract lbcProxy = LiquidityBridgeContract(
payable(address(proxy))
);
lbcProxy.transferOwnership(address(timelock));

address newOwner = makeAddr("newOwner");
bytes memory payload = abi.encodeWithSignature(
"transferOwnership(address)",
newOwner
);
bytes32 predecessor = bytes32(0);
bytes32 salt = keccak256("owner-op");

vm.expectRevert();
lbcProxy.transferOwnership(newOwner);

vm.prank(proposer);
timelock.schedule(
address(lbcProxy),
0,
payload,
predecessor,
salt,
minDelay
);

vm.prank(executor);
vm.expectRevert();
timelock.execute(address(lbcProxy), 0, payload, predecessor, salt);

vm.warp(block.timestamp + minDelay);
vm.prank(executor);
timelock.execute(address(lbcProxy), 0, payload, predecessor, salt);

assertEq(lbcProxy.owner(), newOwner, "timelocked owner-op failed");
}

function test_ProxyAdminStoredInExpectedSlot() public {
address proxyAdminAddress = address(
uint160(uint256(vm.load(address(proxy), ADMIN_SLOT)))
);
assertTrue(proxyAdminAddress != address(0), "EIP-1967 admin missing");
}

function _getProxyAdmin()
internal
view
returns (LiquidityBridgeContractAdmin)
{
address proxyAdminAddress = address(
uint160(uint256(vm.load(address(proxy), ADMIN_SLOT)))
);
return LiquidityBridgeContractAdmin(proxyAdminAddress);
}
}
Loading