From b2ec603567f9d3a033c529ec70a6f424707fea96 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 27 Aug 2026 09:00:00 +0200 Subject: [PATCH] feat(governance): add timelock controller --- src/Timelock.sol | 120 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/Timelock.sol diff --git a/src/Timelock.sol b/src/Timelock.sol new file mode 100644 index 0000000..9a3ddff --- /dev/null +++ b/src/Timelock.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.36; + +/// @title Governance timelock queuing calls behind a mandatory delay before execution. +contract Timelock { + uint256 public constant gracePeriod = 14 days; + uint256 public constant minimumDelay = 2 days; + uint256 public constant maximumDelay = 30 days; + + address public admin; + address public pendingAdmin; + uint256 public delay; + + mapping(bytes32 => bool) public queuedTransactions; + + event NewAdmin(address indexed newAdmin); + event NewPendingAdmin(address indexed newPendingAdmin); + event NewDelay(uint256 indexed newDelay); + event QueueTransaction( + bytes32 indexed txHash, address indexed target, uint256 value, string signature, bytes data, uint256 eta + ); + event CancelTransaction( + bytes32 indexed txHash, address indexed target, uint256 value, string signature, bytes data, uint256 eta + ); + event ExecuteTransaction( + bytes32 indexed txHash, address indexed target, uint256 value, string signature, bytes data, uint256 eta + ); + + constructor(address adminAddress, uint256 delayValue) { + require(delayValue >= minimumDelay, "Timelock: DELAY_SHORT"); + require(delayValue <= maximumDelay, "Timelock: DELAY_LONG"); + admin = adminAddress; + delay = delayValue; + } + + receive() external payable {} + + function setDelay(uint256 delayValue) external { + require(msg.sender == address(this), "Timelock: ONLY_SELF"); + require(delayValue >= minimumDelay, "Timelock: DELAY_SHORT"); + require(delayValue <= maximumDelay, "Timelock: DELAY_LONG"); + delay = delayValue; + emit NewDelay(delayValue); + } + + function acceptAdmin() external { + require(msg.sender == pendingAdmin, "Timelock: ONLY_PENDING"); + admin = msg.sender; + pendingAdmin = address(0); + emit NewAdmin(admin); + } + + function setPendingAdmin(address pendingAdminValue) external { + require(msg.sender == address(this), "Timelock: ONLY_SELF"); + pendingAdmin = pendingAdminValue; + emit NewPendingAdmin(pendingAdminValue); + } + + function queueTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external returns (bytes32) { + require(msg.sender == admin, "Timelock: ONLY_ADMIN"); + require(eta >= block.timestamp + delay, "Timelock: ETA_TOO_SOON"); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = true; + + emit QueueTransaction(txHash, target, value, signature, data, eta); + return txHash; + } + + function cancelTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external { + require(msg.sender == admin, "Timelock: ONLY_ADMIN"); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + queuedTransactions[txHash] = false; + + emit CancelTransaction(txHash, target, value, signature, data, eta); + } + + function executeTransaction( + address target, + uint256 value, + string calldata signature, + bytes calldata data, + uint256 eta + ) external payable returns (bytes memory) { + require(msg.sender == admin, "Timelock: ONLY_ADMIN"); + + bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); + require(queuedTransactions[txHash], "Timelock: NOT_QUEUED"); + require(block.timestamp >= eta, "Timelock: NOT_READY"); + require(block.timestamp <= eta + gracePeriod, "Timelock: STALE"); + + queuedTransactions[txHash] = false; + + bytes memory callData; + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + } + + (bool success, bytes memory returnData) = target.call{value: value}(callData); + require(success, "Timelock: EXECUTION_REVERTED"); + + emit ExecuteTransaction(txHash, target, value, signature, data, eta); + return returnData; + } +}