Description:
Governance contract for decentralized decision-making with timelock mechanism for delayed execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"contracts/GovernorTimelock.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.25;
import { ERC20Helper } from "../modules/erc20-helper/src/ERC20Helper.sol";
import { IGovernorTimelock } from "./interfaces/IGovernorTimelock.sol";
contract GovernorTimelock is IGovernorTimelock {
/**************************************************************************************************************************************/
/*** Storage ***/
/**************************************************************************************************************************************/
bytes32 public override constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE");
bytes32 public override constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE");
bytes32 public override constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE");
bytes32 public override constant ROLE_ADMIN = keccak256("ROLE_ADMIN");
uint32 public override constant MIN_DELAY = 1 days;
uint32 public override constant MIN_EXECUTION_WINDOW = 1 days;
address public override pendingTokenWithdrawer;
address public override tokenWithdrawer;
uint256 public override latestProposalId;
TimelockParameters public override defaultTimelockParameters;
mapping(uint256 => Proposal) public override proposals;
mapping(address => mapping(bytes32 => bool)) public override hasRole;
mapping(address => mapping(bytes4 => TimelockParameters)) public override functionTimelockParameters;
/**************************************************************************************************************************************/
/*** Modifiers ***/
/**************************************************************************************************************************************/
modifier onlyRole(bytes32 role_) {
require(hasRole[msg.sender][role_], "GT:NOT_AUTHORIZED");
_;
}
modifier onlySelf() {
require(msg.sender == address(this), "GT:NOT_SELF");
_;
}
/**************************************************************************************************************************************/
/*** Constructor ***/
/**************************************************************************************************************************************/
constructor(address tokenWithdrawer_, address proposer_, address executor_, address canceller_, address roleAdmin_) {
tokenWithdrawer = tokenWithdrawer_;
defaultTimelockParameters = TimelockParameters({ delay: MIN_DELAY, executionWindow: MIN_EXECUTION_WINDOW });
emit DefaultTimelockSet(MIN_DELAY, MIN_EXECUTION_WINDOW);
_updateRole(PROPOSER_ROLE, proposer_, true);
_updateRole(EXECUTOR_ROLE, executor_, true);
_updateRole(CANCELLER_ROLE, canceller_, true);
_updateRole(ROLE_ADMIN, roleAdmin_, true);
}
/**************************************************************************************************************************************/
/*** Token Withdrawer Functions ***/
/**************************************************************************************************************************************/
function acceptTokenWithdrawer() external override {
address newTokenWithdrawer_ = pendingTokenWithdrawer;
require(msg.sender == newTokenWithdrawer_, "GT:ATW:NOT_AUTHORIZED");
tokenWithdrawer = newTokenWithdrawer_;
pendingTokenWithdrawer = address(0);
emit TokenWithdrawerAccepted(newTokenWithdrawer_);
}
function setPendingTokenWithdrawer(address newPendingTokenWithdrawer_) external override onlyRole(ROLE_ADMIN) {
pendingTokenWithdrawer = newPendingTokenWithdrawer_;
emit PendingTokenWithdrawerSet(newPendingTokenWithdrawer_);
}
function withdrawERC20Token(address token_, uint256 amount_) external override {
require(msg.sender == tokenWithdrawer, "GT:WET:NOT_AUTHORIZED");
require(ERC20Helper.transfer(token_, msg.sender, amount_), "GT:WET:TRANSFER_FAILED");
emit ERC20TokenWithdrawn(token_, msg.sender, amount_);
}
/**************************************************************************************************************************************/
/*** Timelock Configuration ***/
/**************************************************************************************************************************************/
function setDefaultTimelockParameters(uint32 delay_, uint32 executionWindow_) external override onlySelf {
require(delay_ >= MIN_DELAY, "GT:SDTP:INVALID_DELAY");
require(executionWindow_ >= MIN_EXECUTION_WINDOW, "GT:SDTP:INVALID_EXEC_WINDOW");
defaultTimelockParameters = TimelockParameters({ delay: delay_, executionWindow: executionWindow_ });
emit DefaultTimelockSet(delay_, executionWindow_);
}
function setFunctionTimelockParameters(
address target_,
bytes4 functionSelector_,
uint32 delay_,
uint32 executionWindow_
)
external override onlySelf
{
// Both delay_ & executionWindow_ must be zero to use defaults, or both must meet minimums.
require(
(delay_ == 0 && executionWindow_ == 0) ||
(delay_ >= MIN_DELAY && executionWindow_ >= MIN_EXECUTION_WINDOW),
"GT:SFTP:INVALID_PARAMETERS"
);
functionTimelockParameters[target_][functionSelector_] = TimelockParameters({ delay: delay_, executionWindow: executionWindow_ });
emit FunctionTimelockSet(target_, functionSelector_, delay_, executionWindow_);
}
/**************************************************************************************************************************************/
/*** Role Management ***/
/**************************************************************************************************************************************/
function updateRole(bytes32 role_, address account_, bool grantRole_) external override onlySelf {
_updateRole(role_, account_, grantRole_);
}
function proposeRoleUpdates(
bytes32[] calldata roles_,
address[] calldata accounts_,
bool[] calldata shouldGrant_
)
external override onlyRole(ROLE_ADMIN)
{
require(roles_.length > 0, "GT:PRU:EMPTY_ARRAY");
require(roles_.length == accounts_.length, "GT:PRU:INVALID_ACCOUNTS_LENGTH");
require(roles_.length == shouldGrant_.length, "GT:PRU:INVALID_SHOULD_GRANT_LENGTH");
for (uint256 i = 0; i < roles_.length; i++) {
_scheduleProposal(address(this), this.updateRole.selector, abi.encode(roles_[i], accounts_[i], shouldGrant_[i]));
}
}
/**************************************************************************************************************************************/
/*** Proposal Management ***/
/**************************************************************************************************************************************/
function executeProposals(
uint256[] calldata proposalIds_,
address[] calldata targets_,
bytes[] calldata data_
)
external override onlyRole(EXECUTOR_ROLE)
{
require(proposalIds_.length != 0, "GT:EP:EMPTY_ARRAY");
require(proposalIds_.length == targets_.length, "GT:EP:INVALID_TARGETS_LENGTH");
require(proposalIds_.length == data_.length, "GT:EP:INVALID_DATA_LENGTH");
for (uint256 i = 0; i < proposalIds_.length; i++) {
Proposal memory proposal_ = proposals[proposalIds_[i]];
bytes32 expectedProposalHash_ = keccak256(abi.encode(targets_[i], data_[i]));
require(proposals[proposalIds_[i]].proposalHash != bytes32(0), "GT:EP:PROPOSAL_NOT_FOUND");
require(isExecutable(proposalIds_[i]), "GT:EP:NOT_EXECUTABLE");
require(expectedProposalHash_ == proposal_.proposalHash, "GT:EP:INVALID_DATA");
delete proposals[proposalIds_[i]];
_call(targets_[i], data_[i]);
emit ProposalExecuted(proposalIds_[i]);
}
}
function scheduleProposals(address[] calldata targets_, bytes[] calldata data_) external override onlyRole(PROPOSER_ROLE) {
require(targets_.length != 0, "GT:SP:EMPTY_ARRAY");
require(targets_.length == data_.length, "GT:SP:ARRAY_LENGTH_MISMATCH");
for (uint256 i = 0; i < targets_.length; i++) {
bytes4 selector_ = bytes4(data_[i][:4]);
bytes memory params_ = data_[i][4:];
require(!_isUpdatingRoles(targets_[i], selector_), "GT:SP:UPDATE_ROLE_NOT_ALLOWED");
_scheduleProposal(targets_[i], selector_, params_);
}
}
function unscheduleProposals(uint256[] calldata proposalIds_) external override onlyRole(CANCELLER_ROLE) {
for (uint256 i = 0; i < proposalIds_.length; i++) {
require(proposals[proposalIds_[i]].proposalHash != 0, "GT:UP:PROPOSAL_NOT_FOUND");
require(proposals[proposalIds_[i]].isUnschedulable, "GT:UP:NOT_UNSCHEDULABLE");
delete proposals[proposalIds_[i]];
emit ProposalUnscheduled(proposalIds_[i]);
}
}
/**************************************************************************************************************************************/
/*** View Functions ***/
/**************************************************************************************************************************************/
function isExecutable(uint256 proposalId_) public override view returns (bool isExecutable_) {
isExecutable_ = block.timestamp >= proposals[proposalId_].delayedUntil && block.timestamp <= proposals[proposalId_].validUntil;
}
/**************************************************************************************************************************************/
/*** Internal Functions ***/
/**************************************************************************************************************************************/
function _call(address target_, bytes calldata calldata_) internal {
( bool success_, bytes memory returndata_ ) = target_.call(calldata_);
if (success_) {
return;
}
if (returndata_.length > 0) {
assembly ("memory-safe") {
let size_ := mload(returndata_)
revert(add(32, returndata_), size_)
}
} else {
revert("GT:EP:CALL_FAILED");
}
}
function _getTimelockParameters(
address target_, bytes4 selector_, bytes memory parameters_
)
internal view returns (TimelockParameters memory timelockParameters_)
{
// Use prior timelock params if set when updating timelock params.
if (target_ == address(this) && selector_ == this.setFunctionTimelockParameters.selector) {
( target_, selector_, , ) = abi.decode(parameters_, (address, bytes4, uint32, uint32));
}
uint32 functionDelay_ = functionTimelockParameters[target_][selector_].delay;
timelockParameters_ =
functionDelay_ == 0 ? defaultTimelockParameters : functionTimelockParameters[target_][selector_];
}
function _isUpdatingRoles(address target_, bytes4 selector_) internal view returns (bool isUpdatingRoles_) {
isUpdatingRoles_ = target_ == address(this) && selector_ == this.updateRole.selector;
}
function _updateRole(bytes32 role_, address account_, bool grantRole_) internal {
require(hasRole[account_][role_] != grantRole_, "GT:UR:ROLE_NOT_CHANGED");
hasRole[account_][role_] = grantRole_;
emit RoleUpdated(role_, account_, grantRole_);
}
function _scheduleProposal(address target_, bytes4 selector_, bytes memory parameters_) internal {
require(target_.code.length > 0, "GT:SP:EMPTY_ADDRESS");
TimelockParameters memory timelockParameters_ = _getTimelockParameters(target_, selector_, parameters_);
Proposal memory proposal_ = Proposal({
proposalHash: keccak256(abi.encode(target_, bytes.concat(selector_, parameters_))),
scheduledAt: uint32(block.timestamp),
delayedUntil: uint32(block.timestamp) + timelockParameters_.delay,
validUntil: uint32(block.timestamp) + timelockParameters_.delay + timelockParameters_.executionWindow,
isUnschedulable: !_isUpdatingRoles(target_, selector_)
});
uint256 latestProposalId_ = ++latestProposalId;
proposals[latestProposalId_] = proposal_;
emit ProposalScheduled(latestProposalId_, proposal_);
}
}
"
},
"modules/erc20-helper/src/ERC20Helper.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.7;
import { IERC20Like } from "./interfaces/IERC20Like.sol";
/**
* @title Small Library to standardize erc20 token interactions.
*/
library ERC20Helper {
/**************************************************************************************************************************************/
/*** Internal Functions ***/
/**************************************************************************************************************************************/
function transfer(address token_, address to_, uint256 amount_) internal returns (bool success_) {
return _call(token_, abi.encodeWithSelector(IERC20Like.transfer.selector, to_, amount_));
}
function transferFrom(address token_, address from_, address to_, uint256 amount_) internal returns (bool success_) {
return _call(token_, abi.encodeWithSelector(IERC20Like.transferFrom.selector, from_, to_, amount_));
}
function approve(address token_, address spender_, uint256 amount_) internal returns (bool success_) {
// If setting approval to zero fails, return false.
if (!_call(token_, abi.encodeWithSelector(IERC20Like.approve.selector, spender_, uint256(0)))) return false;
// If `amount_` is zero, return true as the previous step already did this.
if (amount_ == uint256(0)) return true;
// Return the result of setting the approval to `amount_`.
return _call(token_, abi.encodeWithSelector(IERC20Like.approve.selector, spender_, amount_));
}
function _call(address token_, bytes memory data_) private returns (bool success_) {
if (token_.code.length == uint256(0)) return false;
bytes memory returnData;
( success_, returnData ) = token_.call(data_);
return success_ && (returnData.length == uint256(0) || abi.decode(returnData, (bool)));
}
}
"
},
"contracts/interfaces/IGovernorTimelock.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.7;
interface IGovernorTimelock {
/**************************************************************************************************************************************/
/*** Structs ***/
/**************************************************************************************************************************************/
struct Proposal {
bytes32 proposalHash;
bool isUnschedulable;
uint32 scheduledAt;
uint32 delayedUntil;
uint32 validUntil;
}
struct TimelockParameters {
uint32 delay;
uint32 executionWindow;
}
/**************************************************************************************************************************************/
/*** Events ***/
/**************************************************************************************************************************************/
/**
* @notice Emitted when the default timelock parameters are set
* @param delay The new default delay
* @param executionWindow The new default execution window
*/
event DefaultTimelockSet(uint32 delay, uint32 executionWindow);
/**
* @notice Emitted when tokens are withdrawn from the governor timelock contract
* @param token The address of the token withdrawn
* @param receiver The address of the receiver of the tokens
* @param amount The amount of tokens withdrawn
*/
event ERC20TokenWithdrawn(address indexed token, address indexed receiver, uint256 amount);
/**
* @notice Emitted when the function timelock parameters are set
* @param target The target of the function
* @param functionSelector The function selector
* @param delay The new delay
* @param executionWindow The new execution window
*/
event FunctionTimelockSet(address indexed target, bytes4 indexed functionSelector, uint32 delay, uint32 executionWindow);
/**
* @notice Emitted when the pending token withdrawer is set
* @param newPendingTokenWithdrawer The address of the new pending token withdrawer
*/
event PendingTokenWithdrawerSet(address indexed newPendingTokenWithdrawer);
/**
* @notice Emitted when a proposal is executed
* @param proposalId The id of the proposal
*/
event ProposalExecuted(uint256 indexed proposalId);
/**
* @notice Emitted when a proposal is scheduled
* @param proposalId The id of the proposal
* @param proposal The proposal
*/
event ProposalScheduled(uint256 indexed proposalId, Proposal proposal);
/**
* @notice Emitted when a proposal is unscheduled
* @param proposalId The id of the proposal
*/
event ProposalUnscheduled(uint256 indexed proposalId);
/**
* @notice Emitted when a role is updated
* @param role The role updated
* @param account The account updated the role for
* @param grantRole Whether the role is granted or revoked
*/
event RoleUpdated(bytes32 indexed role, address indexed account, bool grantRole);
/**
* @notice Emitted when the token withdrawer is accepted
* @param tokenWithdrawer The address of the new token withdrawer
*/
event TokenWithdrawerAccepted(address indexed tokenWithdrawer);
/**************************************************************************************************************************************/
/*** Role constants ***/
/**************************************************************************************************************************************/
/**
* @notice Returns the bytes32 representation of the canceler role
* @dev Address that has the canceler role can unschedule proposals but can not unschedule role updates
* @return cancelerRole The canceler role
*/
function CANCELLER_ROLE() external view returns (bytes32 cancelerRole);
/**
* @notice Returns the bytes32 representation of the executor role
* @dev Address that has the executor role can execute all proposals including role updates
* @return executorRole The executor role
*/
function EXECUTOR_ROLE() external view returns (bytes32 executorRole);
/**
* @notice Returns the bytes32 representation of the proposer role
* @dev Address that has the proposer role can schedule proposals but can not schedule role updates
* @return proposerRole The proposer role
*/
function PROPOSER_ROLE() external view returns (bytes32 proposerRole);
/**
* @notice Returns the bytes32 representation of the role admin role
* @dev Address that has the role admin role can update roles including the role admin role itself
* @return roleAdmin The role admin role
*/
function ROLE_ADMIN() external view returns (bytes32 roleAdmin);
/**************************************************************************************************************************************/
/*** Timelock constants ***/
/**************************************************************************************************************************************/
/**
* @notice Returns the minimum delay for a proposal
* @return minDelay The minimum delay for a proposal
*/
function MIN_DELAY() external view returns (uint32 minDelay);
/**
* @notice Returns the minimum execution window for a proposal
* @return minExecutionWindow The minimum execution window for a proposal
*/
function MIN_EXECUTION_WINDOW() external view returns (uint32 minExecutionWindow);
/**************************************************************************************************************************************/
/*** View Functions ***/
/**************************************************************************************************************************************/
/**
* @notice Returns the default timelock parameters
* @return delay The delay
* @return executionWindow The execution window
*/
function defaultTimelockParameters() external view returns (uint32 delay, uint32 executionWindow);
/**
* @notice Returns the timelock parameters for a given target and function selector
* @param target The target of the function
* @param functionSelector The function selector
* @return delay The delay
* @return executionWindow The execution window
*/
function functionTimelockParameters(
address target,
bytes4 functionSelector
) external view returns (uint32 delay, uint32 executionWindow);
/**
* @notice Checks if an account has a role
* @param account The account to check
* @param role The role to check
* @return doesHaveRole Whether the account has the role
*/
function hasRole(address account, bytes32 role) external view returns (bool doesHaveRole);
/**
* @notice Checks if a proposal is executable
* @param proposalId The id of the proposal
* @return isExecutable Whether the proposal is executable
*/
function isExecutable(uint256 proposalId) external view returns (bool isExecutable);
/**
* @notice Returns the latest proposal id
* @return latestProposalId The latest proposal id
*/
function latestProposalId() external view returns (uint256 latestProposalId);
/**
* @notice Returns the pending token withdrawer
* @return pendingTokenWithdrawer The address of the pending token withdrawer
*/
function pendingTokenWithdrawer() external view returns (address pendingTokenWithdrawer);
/**
* @notice Returns the proposal for a given proposal id
* @param proposalId The id of the proposal
* @return proposalHash The hash of the proposal
* @return isUnschedulable Whether the proposal is unschedulable
* @return scheduledAt The timestamp when the proposal was scheduled
* @return delayedUntil The timestamp when the proposal was delayed
* @return validUntil The timestamp when the proposal was valid
*/
function proposals(uint256 proposalId) external view returns (
bytes32 proposalHash,
bool isUnschedulable,
uint32 scheduledAt,
uint32 delayedUntil,
uint32 validUntil
);
/**
* @notice Returns the token withdrawer
* @return tokenWithdrawer The address of the token withdrawer
*/
function tokenWithdrawer() external view returns (address tokenWithdrawer);
/**************************************************************************************************************************************/
/*** Token Withdrawer Functions ***/
/**************************************************************************************************************************************/
/**
* @notice Accepts the token withdrawer role and sets pending token withdrawer to zero address
* @dev Only the pending token withdrawer can accept the token withdrawer role
*/
function acceptTokenWithdrawer() external;
/**
* @notice Sets the pending token withdrawer
* @dev Only the token withdrawer can set the pending token withdrawer
* @param newPendingTokenWithdrawer The address of the new pending token withdrawer
*/
function setPendingTokenWithdrawer(address newPendingTokenWithdrawer) external;
/**
* @notice Withdraws tokens from the governor timelock contract
* @dev Only the token withdrawer can withdraw tokens and tokens are sent to the token withdrawer
* @param token The address of the token to withdraw
* @param amount The amount of tokens to withdraw
*/
function withdrawERC20Token(address token, uint256 amount) external;
/**************************************************************************************************************************************/
/*** Timelock Configuration ***/
/**************************************************************************************************************************************/
/**
* @notice Sets the default timelock parameters
* @param delay The new default delay
* @param executionWindow The new default execution window
*/
function setDefaultTimelockParameters(uint32 delay, uint32 executionWindow) external;
/**
* @notice Sets the function timelock parameters
* @param target The target of the function
* @param functionSelector The function selector
* @param delay The new delay
* @param executionWindow The new execution window
*/
function setFunctionTimelockParameters(address target, bytes4 functionSelector, uint32 delay, uint32 executionWindow) external;
/**************************************************************************************************************************************/
/*** Role Management ***/
/**************************************************************************************************************************************/
/**
* @notice Updates a role
* @dev The role updating needs to be done through the timelock contract
* @param role The role to update
* @param account The account to update the role for
* @param grantRole Whether the role is granted or revoked
*/
function updateRole(bytes32 role, address account, bool grantRole) external;
/**************************************************************************************************************************************/
/*** Proposal Management ***/
/**************************************************************************************************************************************/
/**
* @notice Executes proposals
* @dev The proposalIds, targets and data arrays must have the same length
* @param proposalIds The ids of the proposals to execute
* @param targets The targets of the proposals
* @param data The calldata of the proposals that the contract is going to execute
*/
function executeProposals(
uint256[] calldata proposalIds,
address[] calldata targets,
bytes[] calldata data
) external;
/**
* @notice Proposes to update roles
* @dev The role updating needs to be done through the timelock contract
* @param roles The roles to update
* @param accounts The accounts to update the roles for
* @param shouldGrant Whether to grant or revoke the roles
*/
function proposeRoleUpdates(bytes32[] calldata roles, address[] calldata accounts, bool[] calldata shouldGrant) external;
/**
* @notice Schedules proposals
* @dev The targets and data arrays must have the same length
* @param targets The targets of the proposals
* @param data The calldata of the proposals that the contract is going to execute
*/
function scheduleProposals(address[] calldata targets, bytes[] calldata data) external;
/**
* @notice Unschedule proposals
* @dev The proposalIds array must not contain duplicates
* @param proposalIds The ids of the proposals to unschedule
*/
function unscheduleProposals(uint256[] calldata proposalIds) external;
}
"
},
"modules/erc20-helper/src/interfaces/IERC20Like.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.7;
/// @title Interface of the ERC20 standard as needed by ERC20Helper.
interface IERC20Like {
function approve(address spender_, uint256 amount_) external returns (bool success_);
function transfer(address recipient_, uint256 amount_) external returns (bool success_);
function transferFrom(address owner_, address recipient_, uint256 amount_) external returns (bool success_);
}
"
}
},
"settings": {
"remappings": [
"contract-test-utils/=modules/non-transparent-proxy/modules/contract-test-utils/contracts/",
"ds-test/=modules/forge-std/lib/ds-test/src/",
"erc20-helper/=modules/erc20-helper/src/",
"forge-std/=modules/forge-std/src/",
"non-transparent-proxy/=modules/non-transparent-proxy/"
],
"optimizer": {
"enabled": true,
"runs": 200
},
"metadata": {
"useLiteralContent": false,
"bytecodeHash": "ipfs",
"appendCBOR": true
},
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
},
"evmVersion": "cancun",
"viaIR": false
}
}}
Submitted on: 2025-09-22 15:23:45
Comments
Log in to comment.
No comments yet.