From 955c0546d4dee63cbe31e4badc7c8cf089e4a62b Mon Sep 17 00:00:00 2001 From: eloqjava Date: Tue, 4 Nov 2025 15:17:38 -0800 Subject: [PATCH 1/5] added timelock and tests --- .env.example | 11 ++ Makefile | 23 +++ script/deploy/DeployTimelock.sol | 63 ++++++++ test/DeployTimelock.t.sol | 242 +++++++++++++++++++++++++++++++ 4 files changed, 339 insertions(+) create mode 100644 script/deploy/DeployTimelock.sol create mode 100644 test/DeployTimelock.t.sol diff --git a/.env.example b/.env.example index 1395889..5a00b8b 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,13 @@ # update with your mnemonic MNEMONIC="test test test test test test test test test test test junk" + +# .env +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 diff --git a/Makefile b/Makefile index 0a9d521..f2e3d9b 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 diff --git a/script/deploy/DeployTimelock.sol b/script/deploy/DeployTimelock.sol new file mode 100644 index 0000000..d8bb199 --- /dev/null +++ b/script/deploy/DeployTimelock.sol @@ -0,0 +1,63 @@ + +// 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 Contract name for salt computation + string public constant CONTRACT_NAME = "TimelockController"; + + /// @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 using CREATE3 for deterministic addressing + 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; + } +} diff --git a/test/DeployTimelock.t.sol b/test/DeployTimelock.t.sol new file mode 100644 index 0000000..ee92cd5 --- /dev/null +++ b/test/DeployTimelock.t.sol @@ -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"); + } + } +} From cafd4d8723b270130952967ee920a305887efc52 Mon Sep 17 00:00:00 2001 From: eloqjava Date: Tue, 4 Nov 2025 15:23:46 -0800 Subject: [PATCH 2/5] cleaned up comments --- script/deploy/DeployTimelock.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/deploy/DeployTimelock.sol b/script/deploy/DeployTimelock.sol index d8bb199..96c6e12 100644 --- a/script/deploy/DeployTimelock.sol +++ b/script/deploy/DeployTimelock.sol @@ -32,7 +32,7 @@ contract DeployTimelock is Script, DeployHelpers { vm.startBroadcast(); - // Deploy TimelockController using CREATE3 for deterministic addressing + // Deploy TimelockController timelockAddress = _deployTimelockController(minDelay, proposers, executors, admin); vm.stopBroadcast(); From c02a12e0ba34c8bf4ab19a4a25eb5cff7530a583 Mon Sep 17 00:00:00 2001 From: eloqjava Date: Tue, 4 Nov 2025 15:30:11 -0800 Subject: [PATCH 3/5] fixed env requirement for CI tests --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f2e3d9b..fe835d7 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Load environment variables from .env file -include .env +-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 From 140729a0cb8efe59aa6175b3c201ed837f669890 Mon Sep 17 00:00:00 2001 From: eloqjava Date: Tue, 4 Nov 2025 15:33:17 -0800 Subject: [PATCH 4/5] removed create3 salt --- script/deploy/DeployTimelock.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/script/deploy/DeployTimelock.sol b/script/deploy/DeployTimelock.sol index 96c6e12..df05379 100644 --- a/script/deploy/DeployTimelock.sol +++ b/script/deploy/DeployTimelock.sol @@ -10,8 +10,6 @@ import { TimelockController } from "../../lib/openzeppelin-contracts-upgradeable import { DeployHelpers } from "./DeployHelpers.sol"; contract DeployTimelock is Script, DeployHelpers { - /// @dev Contract name for salt computation - string public constant CONTRACT_NAME = "TimelockController"; /// @dev Deploy with native Forge arguments function run( From a104cadf70f921ab52ece782ba86aac96f1e1afa Mon Sep 17 00:00:00 2001 From: eloqjava Date: Tue, 4 Nov 2025 15:43:55 -0800 Subject: [PATCH 5/5] copilot nit --- .env.example | 2 +- script/deploy/DeployTimelock.sol | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 5a00b8b..cea8418 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ # update with your mnemonic MNEMONIC="test test test test test test test test test test test junk" -# .env +# Deployment Parameters ETH_RPC_URL=https://eth-sepolia.g.alchemy.com/ ETHERSCAN_API_KEY= PRIVATE_KEY= diff --git a/script/deploy/DeployTimelock.sol b/script/deploy/DeployTimelock.sol index df05379..13d140e 100644 --- a/script/deploy/DeployTimelock.sol +++ b/script/deploy/DeployTimelock.sol @@ -1,4 +1,3 @@ - // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.20 <0.9.0;