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
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
# update with your mnemonic
MNEMONIC="test test test test test test test test test test test junk"

# Deployment Parameters
ETH_RPC_URL=https://eth-sepolia.g.alchemy.com/
ETHERSCAN_API_KEY=
PRIVATE_KEY=

# TimelockController Parameters
TIMELOCK_MIN_DELAY=259200 # 3 days in seconds
TIMELOCK_PROPOSERS=0x1234567890123456789012345678901234567890,0x0987654321098765432109876543210987654321
TIMELOCK_EXECUTORS=0x13cb6ae34a13a0977f4d7101ebc24b87bb23f0d5,0x14cb6ae34a13a0977f4d7101ebc24b87bb23f0d6
TIMELOCK_ADMIN=0x0000000000000000000000000000000000000000
Copy link
Collaborator

Choose a reason for hiding this comment

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

should be 0x0 address for the actual deployments

23 changes: 23 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Load environment variables from .env file
-include .env

# coverage report
coverage :; forge coverage --report lcov && lcov --remove ./lcov.info -o ./lcov.info 'test/*' && genhtml lcov.info --branch-coverage --output-dir coverage

Expand All @@ -20,3 +23,23 @@ sizes:

clean:
forge clean

# Deployment targets
deploy-timelock:
@echo "=== Deploying TimelockController ==="
@echo "Network: ${ETH_RPC_URL}"
@echo "Min Delay: ${TIMELOCK_MIN_DELAY}"
@echo "Proposers: ${TIMELOCK_PROPOSERS}"
@echo "Executors: ${TIMELOCK_EXECUTORS}"
@echo "Admin: ${TIMELOCK_ADMIN}"
@forge script script/deploy/DeployTimelock.sol:DeployTimelock \
${TIMELOCK_MIN_DELAY} \
"[${TIMELOCK_PROPOSERS}]" \
"[${TIMELOCK_EXECUTORS}]" \
${TIMELOCK_ADMIN} \
--rpc-url ${ETH_RPC_URL} \
--private-key ${PRIVATE_KEY} \
--broadcast \
--verify \
--etherscan-api-key ${ETHERSCAN_API_KEY} \
-vvv
60 changes: 60 additions & 0 deletions script/deploy/DeployTimelock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.20 <0.9.0;

import { Script } from "../../lib/forge-std/src/Script.sol";
import { console2 } from "../../lib/forge-std/src/console2.sol";
import { TimelockController } from "../../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/governance/TimelockController.sol";

import { DeployHelpers } from "./DeployHelpers.sol";

contract DeployTimelock is Script, DeployHelpers {

/// @dev Deploy with native Forge arguments
function run(
uint256 minDelay,
address[] memory proposers,
address[] memory executors,
address admin
) public returns (address timelockAddress) {
// Validate parameters
require(proposers.length > 0, "DeployTimelock: At least one proposer required");
require(executors.length > 0, "DeployTimelock: At least one executor required");

console2.log("=== Deploying TimelockController ===");
console2.log("Min delay:", minDelay);
console2.log("Number of proposers:", proposers.length);
console2.log("Number of executors:", executors.length);
console2.log("Admin:", admin);

vm.startBroadcast();

// Deploy TimelockController
timelockAddress = _deployTimelockController(minDelay, proposers, executors, admin);

vm.stopBroadcast();

console2.log("=== Deployment Complete ===");
console2.log("TimelockController deployed at:", timelockAddress);

return timelockAddress;
}

function _deployTimelockController(
uint256 minDelay,
address[] memory proposers,
address[] memory executors,
address admin
) internal returns (address) {
// Deploy TimelockController directly using new for simplicity
TimelockController timelock = new TimelockController(minDelay, proposers, executors, admin);

address timelockAddress = address(timelock);

// Verify deployment
require(timelockAddress != address(0), "DeployTimelock: Deployment failed");
require(timelockAddress.code.length > 0, "DeployTimelock: No code at deployed address");

return timelockAddress;
}
}
242 changes: 242 additions & 0 deletions test/DeployTimelock.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.20 <0.9.0;

import { Test } from "../lib/forge-std/src/Test.sol";
import { TimelockController } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/governance/TimelockController.sol";

import { DeployTimelock } from "../script/deploy/DeployTimelock.sol";

contract DeployTimelockTest is Test {
DeployTimelock internal _deployer;

address internal _deployerAddress;
address internal _proposer1;
address internal _proposer2;
address internal _executor1;
address internal _executor2;
address internal _admin;

uint256 internal constant _DEFAULT_MIN_DELAY = 3 days;
uint256 internal constant _CUSTOM_MIN_DELAY = 1 days;

bytes32 internal constant _PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
bytes32 internal constant _EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
bytes32 internal constant _CANCELLER_ROLE = keccak256("CANCELLER_ROLE");
bytes32 internal constant _DEFAULT_ADMIN_ROLE = 0x00;

function setUp() external {
_deployer = new DeployTimelock();

_deployerAddress = address(this);
_proposer1 = makeAddr("proposer1");
_proposer2 = makeAddr("proposer2");
_executor1 = makeAddr("executor1");
_executor2 = makeAddr("executor2");
_admin = makeAddr("admin");
}

function test_run_singleProposerAndExecutor() external {
address[] memory proposers = new address[](1);
proposers[0] = _proposer1;

address[] memory executors = new address[](1);
executors[0] = _executor1;

address timelockAddress = _deployer.run(_DEFAULT_MIN_DELAY, proposers, executors, _admin);

// Verify deployment
assertTrue(timelockAddress != address(0), "Timelock should be deployed");
assertTrue(timelockAddress.code.length > 0, "Timelock should have code");

TimelockController timelock = TimelockController(payable(timelockAddress));

// Verify configuration
assertEq(timelock.getMinDelay(), _DEFAULT_MIN_DELAY, "Min delay should match");
assertTrue(timelock.hasRole(_PROPOSER_ROLE, _proposer1), "Proposer1 should have proposer role");
assertTrue(timelock.hasRole(_EXECUTOR_ROLE, _executor1), "Executor1 should have executor role");
assertTrue(timelock.hasRole(_DEFAULT_ADMIN_ROLE, _admin), "Admin should have admin role");
}

function test_run_multipleProposersAndExecutors() external {
address[] memory proposers = new address[](2);
proposers[0] = _proposer1;
proposers[1] = _proposer2;

address[] memory executors = new address[](2);
executors[0] = _executor1;
executors[1] = _executor2;

address timelockAddress = _deployer.run(_CUSTOM_MIN_DELAY, proposers, executors, address(0));

TimelockController timelock = TimelockController(payable(timelockAddress));

// Verify configuration
assertEq(timelock.getMinDelay(), _CUSTOM_MIN_DELAY, "Min delay should match");
assertTrue(timelock.hasRole(_PROPOSER_ROLE, _proposer1), "Proposer1 should have proposer role");
assertTrue(timelock.hasRole(_PROPOSER_ROLE, _proposer2), "Proposer2 should have proposer role");
assertTrue(timelock.hasRole(_EXECUTOR_ROLE, _executor1), "Executor1 should have executor role");
assertTrue(timelock.hasRole(_EXECUTOR_ROLE, _executor2), "Executor2 should have executor role");
assertFalse(timelock.hasRole(_DEFAULT_ADMIN_ROLE, address(0)), "Zero address should not have admin role");
}

function test_run_noAdmin() external {
address[] memory proposers = new address[](1);
proposers[0] = _proposer1;

address[] memory executors = new address[](1);
executors[0] = _executor1;

address timelockAddress = _deployer.run(_DEFAULT_MIN_DELAY, proposers, executors, address(0));

TimelockController timelock = TimelockController(payable(timelockAddress));

// Verify no admin role is assigned
assertFalse(timelock.hasRole(_DEFAULT_ADMIN_ROLE, address(0)), "Zero address should not have admin role");
assertFalse(timelock.hasRole(_DEFAULT_ADMIN_ROLE, _admin), "Admin should not have admin role");
}

function test_run_proposersAlsoHaveCancellerRole() external {
address[] memory proposers = new address[](1);
proposers[0] = _proposer1;

address[] memory executors = new address[](1);
executors[0] = _executor1;

address timelockAddress = _deployer.run(_DEFAULT_MIN_DELAY, proposers, executors, _admin);

TimelockController timelock = TimelockController(payable(timelockAddress));

// Proposers should also have canceller role by default in OpenZeppelin TimelockController
assertTrue(timelock.hasRole(_CANCELLER_ROLE, _proposer1), "Proposer should also have canceller role");
}

function test_run_revertsWithEmptyProposers() external {
address[] memory proposers = new address[](0);

address[] memory executors = new address[](1);
executors[0] = _executor1;

vm.expectRevert("DeployTimelock: At least one proposer required");
_deployer.run(_DEFAULT_MIN_DELAY, proposers, executors, _admin);
}

function test_run_revertsWithEmptyExecutors() external {
address[] memory proposers = new address[](1);
proposers[0] = _proposer1;

address[] memory executors = new address[](0);

vm.expectRevert("DeployTimelock: At least one executor required");
_deployer.run(_DEFAULT_MIN_DELAY, proposers, executors, _admin);
}

function test_run_multipleDifferentDeployments() external {
address[] memory proposers = new address[](1);
proposers[0] = _proposer1;

address[] memory executors = new address[](1);
executors[0] = _executor1;

// First deployment
address firstAddress = _deployer.run(_DEFAULT_MIN_DELAY, proposers, executors, _admin);

// Second deployment with different parameters should create different address
address[] memory differentProposers = new address[](1);
differentProposers[0] = _proposer2;

address secondAddress = _deployer.run(_CUSTOM_MIN_DELAY, differentProposers, executors, address(0));

// Addresses should be different since we're using regular deployment
assertTrue(firstAddress != secondAddress, "Different deployments should have different addresses");
}

function test_run_zeroMinDelay() external {
address[] memory proposers = new address[](1);
proposers[0] = _proposer1;

address[] memory executors = new address[](1);
executors[0] = _executor1;

address timelockAddress = _deployer.run(0, proposers, executors, _admin);

TimelockController timelock = TimelockController(payable(timelockAddress));

// Should accept zero min delay
assertEq(timelock.getMinDelay(), 0, "Min delay should be zero");
}

function test_run_largeMinDelay() external {
address[] memory proposers = new address[](1);
proposers[0] = _proposer1;

address[] memory executors = new address[](1);
executors[0] = _executor1;

uint256 largeDelay = 365 days;
address timelockAddress = _deployer.run(largeDelay, proposers, executors, _admin);

TimelockController timelock = TimelockController(payable(timelockAddress));

// Should accept large min delay
assertEq(timelock.getMinDelay(), largeDelay, "Min delay should match large delay");
}

function test_run_sameAddressInMultipleRoles() external {
address[] memory proposers = new address[](1);
proposers[0] = _proposer1;

address[] memory executors = new address[](1);
executors[0] = _proposer1; // Same address as proposer

address timelockAddress = _deployer.run(_DEFAULT_MIN_DELAY, proposers, executors, _proposer1);

TimelockController timelock = TimelockController(payable(timelockAddress));

// Verify same address can have multiple roles
assertTrue(timelock.hasRole(_PROPOSER_ROLE, _proposer1), "Should have proposer role");
assertTrue(timelock.hasRole(_EXECUTOR_ROLE, _proposer1), "Should have executor role");
assertTrue(timelock.hasRole(_DEFAULT_ADMIN_ROLE, _proposer1), "Should have admin role");
}

function testFuzz_run_validParameters(
uint256 minDelay,
uint8 proposerCount,
uint8 executorCount
) external {
// Bound inputs to reasonable ranges
minDelay = bound(minDelay, 0, 365 days);
proposerCount = uint8(bound(proposerCount, 1, 10));
executorCount = uint8(bound(executorCount, 1, 10));

// Create proposer and executor arrays
address[] memory proposers = new address[](proposerCount);
address[] memory executors = new address[](executorCount);

for (uint256 i = 0; i < proposerCount; i++) {
proposers[i] = makeAddr(string(abi.encodePacked("proposer", i)));
}

for (uint256 i = 0; i < executorCount; i++) {
executors[i] = makeAddr(string(abi.encodePacked("executor", i)));
}

address timelockAddress = _deployer.run(minDelay, proposers, executors, _admin);

TimelockController timelock = TimelockController(payable(timelockAddress));

// Verify deployment and basic configuration
assertTrue(timelockAddress != address(0), "Should deploy timelock");
assertEq(timelock.getMinDelay(), minDelay, "Should set correct min delay");

// Verify all proposers have role
for (uint256 i = 0; i < proposerCount; i++) {
assertTrue(timelock.hasRole(_PROPOSER_ROLE, proposers[i]), "Proposer should have role");
}

// Verify all executors have role
for (uint256 i = 0; i < executorCount; i++) {
assertTrue(timelock.hasRole(_EXECUTOR_ROLE, executors[i]), "Executor should have role");
}
}
}