diff --git a/.env.example b/.env.example index 1395889..cea8418 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" + +# 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 diff --git a/Makefile b/Makefile index 0a9d521..fe835d7 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..13d140e --- /dev/null +++ b/script/deploy/DeployTimelock.sol @@ -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; + } +} 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"); + } + } +}