乐闻世界logo
搜索文章和话题

How to handle Timelock mechanism in Solidity?

3月6日 21:47

Timelock is a security mechanism that requires transactions to go through a delay period before execution. It is widely used in DeFi protocols, governance contracts, and upgrade mechanisms, providing users with reaction time to prevent malicious operations.

1. Core Concepts of Timelock

solidity
/* Timelock principles: - Transactions enter queue after submission - Must go through minimum delay time before execution - Can cancel transactions during delay period - Transactions expire after maximum delay time Application scenarios: - Protocol parameter changes - Contract upgrades - Large fund transfers - Governance proposal execution - Emergency function pause */

2. Basic Timelock Implementation

solidity
contract BasicTimelock { // Events event TransactionScheduled( bytes32 indexed txHash, address indexed target, uint256 value, bytes data, uint256 eta ); event TransactionExecuted( bytes32 indexed txHash, address indexed target, uint256 value, bytes data ); event TransactionCancelled(bytes32 indexed txHash); event DelayChanged(uint256 oldDelay, uint256 newDelay); // State variables address public admin; uint256 public delay; // Minimum delay time uint256 public constant GRACE_PERIOD = 14 days; // Grace period uint256 public constant MINIMUM_DELAY = 2 days; // Minimum delay uint256 public constant MAXIMUM_DELAY = 30 days; // Maximum delay // Transaction hash => scheduled execution time mapping(bytes32 => uint256) public queuedTransactions; modifier onlyAdmin() { require(msg.sender == admin, "Timelock: caller is not admin"); _; } constructor(address _admin, uint256 _delay) { require(_delay >= MINIMUM_DELAY, "Delay must exceed minimum"); require(_delay <= MAXIMUM_DELAY, "Delay must not exceed maximum"); admin = _admin; delay = _delay; } // Receive ETH receive() external payable {} // Set delay time function setDelay(uint256 _delay) public onlyAdmin { require(_delay >= MINIMUM_DELAY, "Delay must exceed minimum"); require(_delay <= MAXIMUM_DELAY, "Delay must not exceed maximum"); emit DelayChanged(delay, _delay); delay = _delay; } // Calculate transaction hash function hashTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) public pure returns (bytes32) { return keccak256(abi.encode(target, value, signature, data, eta)); } // Schedule transaction function queueTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) public onlyAdmin returns (bytes32 txHash) { require( eta >= block.timestamp + delay, "Timelock: estimated execution block must satisfy delay" ); txHash = hashTransaction(target, value, signature, data, eta); require( queuedTransactions[txHash] == 0, "Timelock: transaction already queued" ); queuedTransactions[txHash] = eta; emit TransactionScheduled(txHash, target, value, data, eta); } // Cancel transaction function cancelTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) public onlyAdmin { bytes32 txHash = hashTransaction(target, value, signature, data, eta); require( queuedTransactions[txHash] != 0, "Timelock: transaction not queued" ); delete queuedTransactions[txHash]; emit TransactionCancelled(txHash); } // Execute transaction function executeTransaction( address target, uint256 value, string memory signature, bytes memory data, uint256 eta ) public payable onlyAdmin { bytes32 txHash = hashTransaction(target, value, signature, data, eta); require( queuedTransactions[txHash] != 0, "Timelock: transaction not queued" ); require( block.timestamp >= eta, "Timelock: transaction hasn't surpassed time lock" ); require( block.timestamp <= eta + GRACE_PERIOD, "Timelock: transaction is stale" ); delete queuedTransactions[txHash]; bytes memory callData; if (bytes(signature).length == 0) { callData = data; } else { callData = abi.encodePacked( bytes4(keccak256(bytes(signature))), data ); } (bool success, ) = target.call{value: value}(callData); require(success, "Timelock: transaction execution reverted"); emit TransactionExecuted(txHash, target, value, data); } }

3. Advanced Timelock (Supports Batch Operations)

solidity
contract AdvancedTimelock { // Events event OperationScheduled( bytes32 indexed id, uint256 indexed delay, address indexed target, uint256 value, bytes data ); event OperationExecuted(bytes32 indexed id); event OperationCancelled(bytes32 indexed id); event MinDelayChange(uint256 oldDuration, uint256 newDuration); event RoleGranted(bytes32 indexed role, address indexed account); event RoleRevoked(bytes32 indexed role, address indexed account); // Role definitions bytes32 public constant TIMELOCK_ADMIN_ROLE = keccak256("TIMELOCK_ADMIN_ROLE"); bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE"); bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE"); bytes32 public constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE"); // Operation states enum OperationState { Unset, // Not set Pending, // Pending Ready, // Ready Done, // Done Expired // Expired } struct Operation { uint256 delay; // Delay time uint256 scheduledAt; // Scheduled time bool executed; // Is executed } // State variables mapping(bytes32 => Operation) public operations; mapping(bytes32 => mapping(address => bool)) public hasRole; uint256 public minDelay; // Minimum delay uint256 public maxDelay; // Maximum delay uint256 public gracePeriod; // Grace period bytes32[] public operationIds; // Operation ID list modifier onlyRole(bytes32 role) { require(hasRole[role][msg.sender], "Missing role"); _; } constructor( uint256 _minDelay, address[] memory proposers, address[] memory executors ) { require(_minDelay > 0, "Min delay must be > 0"); minDelay = _minDelay; maxDelay = _minDelay * 10; gracePeriod = 14 days; // Set admin _grantRole(TIMELOCK_ADMIN_ROLE, msg.sender); // Set proposers for (uint i = 0; i < proposers.length; i++) { _grantRole(PROPOSER_ROLE, proposers[i]); } // Set executors for (uint i = 0; i < executors.length; i++) { _grantRole(EXECUTOR_ROLE, executors[i]); } // Cancellers can be anyone _grantRole(CANCELLER_ROLE, msg.sender); } function _grantRole(bytes32 role, address account) internal { hasRole[role][account] = true; emit RoleGranted(role, account); } function grantRole(bytes32 role, address account) external onlyRole(TIMELOCK_ADMIN_ROLE) { _grantRole(role, account); } function revokeRole(bytes32 role, address account) external onlyRole(TIMELOCK_ADMIN_ROLE) { hasRole[role][account] = false; emit RoleRevoked(role, account); } // Receive ETH receive() external payable {} // Calculate operation ID function hashOperation( address target, uint256 value, bytes calldata data, bytes32 predecessor, bytes32 salt ) public pure returns (bytes32) { return keccak256(abi.encode(target, value, data, predecessor, salt)); } // Batch operation hash function hashOperationBatch( address[] calldata targets, uint256[] calldata values, bytes[] calldata datas, bytes32 predecessor, bytes32 salt ) public pure returns (bytes32) { return keccak256(abi.encode(targets, values, datas, predecessor, salt)); } // Get operation state function getOperationState(bytes32 id) public view returns (OperationState) { Operation memory op = operations[id]; if (op.scheduledAt == 0) { return OperationState.Unset; } else if (op.executed) { return OperationState.Done; } else if (block.timestamp < op.scheduledAt + op.delay) { return OperationState.Pending; } else if (block.timestamp > op.scheduledAt + op.delay + gracePeriod) { return OperationState.Expired; } else { return OperationState.Ready; } } // Check if ready to execute function isOperationReady(bytes32 id) public view returns (bool) { return getOperationState(id) == OperationState.Ready; } // Check if done function isOperationDone(bytes32 id) public view returns (bool) { return getOperationState(id) == OperationState.Done; } // Schedule operation function schedule( address target, uint256 value, bytes calldata data, bytes32 predecessor, bytes32 salt, uint256 delay ) public onlyRole(PROPOSER_ROLE) { bytes32 id = hashOperation(target, value, data, predecessor, salt); require( getOperationState(id) == OperationState.Unset, "Operation already scheduled" ); require(delay >= minDelay, "Delay too short"); require(delay <= maxDelay, "Delay too long"); operations[id] = Operation({ delay: delay, scheduledAt: block.timestamp, executed: false }); operationIds.push(id); emit OperationScheduled(id, delay, target, value, data); } // Batch schedule function scheduleBatch( address[] calldata targets, uint256[] calldata values, bytes[] calldata datas, bytes32 predecessor, bytes32 salt, uint256 delay ) public onlyRole(PROPOSER_ROLE) { require( targets.length == values.length && targets.length == datas.length, "Length mismatch" ); bytes32 id = hashOperationBatch(targets, values, datas, predecessor, salt); require( getOperationState(id) == OperationState.Unset, "Operation already scheduled" ); require(delay >= minDelay, "Delay too short"); operations[id] = Operation({ delay: delay, scheduledAt: block.timestamp, executed: false }); operationIds.push(id); for (uint i = 0; i < targets.length; i++) { emit OperationScheduled(id, delay, targets[i], values[i], datas[i]); } } // Cancel operation function cancel(bytes32 id) external onlyRole(CANCELLER_ROLE) { require( getOperationState(id) != OperationState.Unset, "Operation not scheduled" ); require( getOperationState(id) != OperationState.Done, "Operation already done" ); delete operations[id]; emit OperationCancelled(id); } // Execute operation function execute( address target, uint256 value, bytes calldata data, bytes32 predecessor, bytes32 salt ) public payable onlyRole(EXECUTOR_ROLE) { bytes32 id = hashOperation(target, value, data, predecessor, salt); require( getOperationState(id) == OperationState.Ready, "Operation not ready" ); operations[id].executed = true; (bool success, ) = target.call{value: value}(data); require(success, "Execution failed"); emit OperationExecuted(id); } // Batch execute function executeBatch( address[] calldata targets, uint256[] calldata values, bytes[] calldata datas, bytes32 predecessor, bytes32 salt ) public payable onlyRole(EXECUTOR_ROLE) { require( targets.length == values.length && targets.length == datas.length, "Length mismatch" ); bytes32 id = hashOperationBatch(targets, values, datas, predecessor, salt); require( getOperationState(id) == OperationState.Ready, "Operation not ready" ); operations[id].executed = true; for (uint i = 0; i < targets.length; i++) { (bool success, ) = targets[i].call{value: values[i]}(datas[i]); require(success, "Batch execution failed"); } emit OperationExecuted(id); } // Update delay function updateDelay(uint256 newDelay) external onlyRole(TIMELOCK_ADMIN_ROLE) { require(newDelay >= minDelay, "Delay too short"); require(newDelay <= maxDelay, "Delay too long"); emit MinDelayChange(minDelay, newDelay); minDelay = newDelay; } // Get pending operations function getPendingOperations() external view returns (bytes32[] memory) { uint256 count = 0; for (uint i = 0; i < operationIds.length; i++) { if (getOperationState(operationIds[i]) == OperationState.Pending) { count++; } } bytes32[] memory pending = new bytes32[](count); uint256 index = 0; for (uint i = 0; i < operationIds.length; i++) { if (getOperationState(operationIds[i]) == OperationState.Pending) { pending[index] = operationIds[i]; index++; } } return pending; } // Get ready operations function getReadyOperations() external view returns (bytes32[] memory) { uint256 count = 0; for (uint i = 0; i < operationIds.length; i++) { if (getOperationState(operationIds[i]) == OperationState.Ready) { count++; } } bytes32[] memory ready = new bytes32[](count); uint256 index = 0; for (uint i = 0; i < operationIds.length; i++) { if (getOperationState(operationIds[i]) == OperationState.Ready) { ready[index] = operationIds[i]; index++; } } return ready; } }

4. Timelock in Governance

solidity
contract GovernanceWithTimelock { // Events event ProposalCreated( uint256 indexed id, address proposer, address[] targets, uint256[] values, bytes[] calldatas, uint256 eta ); event ProposalExecuted(uint256 indexed id); event ProposalCancelled(uint256 indexed id); event VoteCast( address indexed voter, uint256 indexed proposalId, bool support, uint256 votes ); // Proposal structure struct Proposal { address proposer; address[] targets; uint256[] values; bytes[] calldatas; uint256 forVotes; uint256 againstVotes; bool executed; bool canceled; uint256 eta; uint256 startBlock; uint256 endBlock; } // State variables address public timelock; address public governanceToken; uint256 public votingDelay; // Voting delay (block count) uint256 public votingPeriod; // Voting period (block count) uint256 public proposalThreshold; // Proposal threshold uint256 public quorumVotes; // Quorum votes uint256 public gracePeriod; // Grace period mapping(uint256 => Proposal) public proposals; mapping(uint256 => mapping(address => bool)) public hasVoted; uint256 public proposalCount; constructor( address _timelock, address _governanceToken, uint256 _votingDelay, uint256 _votingPeriod, uint256 _proposalThreshold, uint256 _quorumVotes ) { timelock = _timelock; governanceToken = _governanceToken; votingDelay = _votingDelay; votingPeriod = _votingPeriod; proposalThreshold = _proposalThreshold; quorumVotes = _quorumVotes; gracePeriod = 14 days; } // Create proposal function propose( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description ) public returns (uint256) { require( getVotes(msg.sender, block.number - 1) >= proposalThreshold, "Below proposal threshold" ); require( targets.length == values.length && targets.length == calldatas.length, "Length mismatch" ); require(targets.length > 0, "Must provide actions"); proposalCount++; uint256 proposalId = proposalCount; Proposal storage newProposal = proposals[proposalId]; newProposal.proposer = msg.sender; newProposal.targets = targets; newProposal.values = values; newProposal.calldatas = calldatas; newProposal.startBlock = block.number + votingDelay; newProposal.endBlock = block.number + votingDelay + votingPeriod; emit ProposalCreated( proposalId, msg.sender, targets, values, calldatas, 0 ); return proposalId; } // Vote function castVote(uint256 proposalId, bool support) external { require(state(proposalId) == ProposalState.Active, "Voting closed"); Proposal storage proposal = proposals[proposalId]; require(!hasVoted[proposalId][msg.sender], "Already voted"); uint256 votes = getVotes(msg.sender, proposal.startBlock); require(votes > 0, "No voting power"); hasVoted[proposalId][msg.sender] = true; if (support) { proposal.forVotes += votes; } else { proposal.againstVotes += votes; } emit VoteCast(msg.sender, proposalId, support, votes); } // Queue for execution (through timelock) function queue(uint256 proposalId) external { require( state(proposalId) == ProposalState.Succeeded, "Proposal not succeeded" ); Proposal storage proposal = proposals[proposalId]; uint256 eta = block.timestamp + AdvancedTimelock(timelock).minDelay(); proposal.eta = eta; // Queue through timelock for (uint i = 0; i < proposal.targets.length; i++) { AdvancedTimelock(timelock).schedule( proposal.targets[i], proposal.values[i], proposal.calldatas[i], bytes32(0), bytes32(proposalId), AdvancedTimelock(timelock).minDelay() ); } } // Execute proposal function execute(uint256 proposalId) external payable { require( state(proposalId) == ProposalState.Queued, "Proposal not queued" ); Proposal storage proposal = proposals[proposalId]; require( block.timestamp >= proposal.eta, "Timelock not passed" ); require( block.timestamp <= proposal.eta + gracePeriod, "Proposal expired" ); proposal.executed = true; // Execute through timelock for (uint i = 0; i < proposal.targets.length; i++) { AdvancedTimelock(timelock).execute( proposal.targets[i], proposal.values[i], proposal.calldatas[i], bytes32(0), bytes32(proposalId) ); } emit ProposalExecuted(proposalId); } // Cancel proposal function cancel(uint256 proposalId) external { require( state(proposalId) != ProposalState.Executed, "Already executed" ); Proposal storage proposal = proposals[proposalId]; require( msg.sender == proposal.proposer || getVotes(proposal.proposer, block.number - 1) < proposalThreshold, "Not authorized" ); proposal.canceled = true; // Cancel operations in timelock for (uint i = 0; i < proposal.targets.length; i++) { bytes32 id = keccak256(abi.encode( proposal.targets[i], proposal.values[i], proposal.calldatas[i], bytes32(0), bytes32(proposalId) )); if (AdvancedTimelock(timelock).getOperationState(id) != AdvancedTimelock.OperationState.Unset) { AdvancedTimelock(timelock).cancel(id); } } emit ProposalCancelled(proposalId); } // Proposal state enum enum ProposalState { Pending, Active, Canceled, Defeated, Succeeded, Queued, Expired, Executed } // Get proposal state function state(uint256 proposalId) public view returns (ProposalState) { require(proposalCount >= proposalId && proposalId > 0, "Invalid id"); Proposal storage proposal = proposals[proposalId]; if (proposal.canceled) { return ProposalState.Canceled; } else if (proposal.executed) { return ProposalState.Executed; } else if (block.number <= proposal.startBlock) { return ProposalState.Pending; } else if (block.number <= proposal.endBlock) { return ProposalState.Active; } else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes) { return ProposalState.Defeated; } else if (proposal.eta == 0) { return ProposalState.Succeeded; } else if (block.timestamp >= proposal.eta + gracePeriod) { return ProposalState.Expired; } else { return ProposalState.Queued; } } // Get voting power function getVotes(address account, uint256 blockNumber) public view returns (uint256) { // Simplified implementation, actual should query governance token return 1000; } }

5. Timelock Security Best Practices

solidity
contract TimelockSecurity { /* Security best practices: 1. Delay time setting - Minimum delay: 2-3 days (give users reaction time) - Maximum delay: 30 days (prevent indefinite delay) - Grace period: 14 days (cannot execute after expiration) 2. Permission separation - Proposer: can submit operations - Executor: can execute operations - Canceller: can cancel operations - Admin: can modify parameters 3. Monitoring and alerting - Listen to all timelock events - Set up alerts for abnormal operations - Establish emergency response mechanism 4. Multiple verification - Operation hash verification - Parameter boundary check - State pre-check */ } // Timelock with emergency pause contract PausableTimelock { bool public paused; address public guardian; modifier whenNotPaused() { require(!paused, "Timelock paused"); _; } modifier onlyGuardian() { require(msg.sender == guardian, "Not guardian"); _; } function pause() external onlyGuardian { paused = true; } function unpause() external onlyGuardian { paused = false; } function execute( address target, uint256 value, bytes calldata data, bytes32 predecessor, bytes32 salt ) external payable whenNotPaused { // Execution logic... } }

6. Test Example

javascript
// Hardhat test timelock const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("Timelock", function () { let timelock; let admin, proposer, executor, canceller; const minDelay = 2 * 24 * 60 * 60; // 2 days beforeEach(async function () { [admin, proposer, executor, canceller] = await ethers.getSigners(); const Timelock = await ethers.getContractFactory("AdvancedTimelock"); timelock = await Timelock.deploy( minDelay, [proposer.address], [executor.address] ); await timelock.deployed(); // Send ETH to timelock await admin.sendTransaction({ to: timelock.address, value: ethers.utils.parseEther("10") }); }); it("Should schedule and execute operation after delay", async function () { const target = admin.address; const value = ethers.utils.parseEther("1"); const data = "0x"; const predecessor = ethers.constants.HashZero; const salt = ethers.utils.randomBytes(32); // Schedule operation await timelock.connect(proposer).schedule( target, value, data, predecessor, salt, minDelay ); const id = await timelock.hashOperation(target, value, data, predecessor, salt); expect(await timelock.getOperationState(id)).to.equal(1); // Pending // Advance time await network.provider.send("evm_increaseTime", [minDelay]); await network.provider.send("evm_mine"); expect(await timelock.getOperationState(id)).to.equal(2); // Ready // Execute operation await timelock.connect(executor).execute(target, value, data, predecessor, salt); expect(await timelock.getOperationState(id)).to.equal(3); // Done }); it("Should not execute before delay", async function () { const target = admin.address; const value = ethers.utils.parseEther("1"); const data = "0x"; const predecessor = ethers.constants.HashZero; const salt = ethers.utils.randomBytes(32); await timelock.connect(proposer).schedule( target, value, data, predecessor, salt, minDelay ); // Try to execute early await expect( timelock.connect(executor).execute(target, value, data, predecessor, salt) ).to.be.revertedWith("Operation not ready"); }); });

7. Summary

Timelock is an important security mechanism:

  1. Core Functions:

    • Delayed execution
    • Cancellable
    • Expiration mechanism
    • Access control
  2. Application Scenarios:

    • Governance contracts
    • Protocol upgrades
    • Parameter adjustments
    • Fund transfers
  3. Security Points:

    • Reasonable delay time
    • Permission separation
    • Monitoring alerts
    • Emergency mechanisms
  4. Best Practices:

    • Use OpenZeppelin's TimelockController
    • Test time boundaries thoroughly
    • Establish monitoring system
    • Develop emergency plans
标签:Solidity