ChainScore Labs
LABS
Guides

How Time Locks Improve Smart Contract Safety

Chainscore © 2025
core-concepts

Core Concepts of Time Locks

Time locks are a critical security primitive that enforces a mandatory waiting period for executing privileged actions, preventing hasty or malicious changes.

01

Delay Period

The delay period is the mandatory waiting time between a transaction's proposal and its execution.

  • It is a fixed or configurable duration (e.g., 24-72 hours).
  • This creates a "cooling-off" period for the community to review changes.
  • It matters because it is the primary defense against rushed governance attacks or admin key compromises, allowing time for users to react.
02

Proposal & Execution

A two-step process where an action is first scheduled and later finalized after the delay.

  • A proposal transaction is broadcast, starting the timer.
  • A separate execution transaction is required after the delay elapses.
  • This separation matters as it decouples authorization from execution, enabling on-chain verification and potential cancellation of malicious proposals.
03

Timelock Controller

A smart contract that acts as the intermediary executor, holding assets and enforcing delays.

  • It is often the owner or admin of other core protocol contracts.
  • All upgrade or parameter-change calls must route through it.
  • This matters because it centralizes security, providing a transparent and auditable log of all scheduled operations.
04

Role-Based Access

Defines permissions for proposing and executing time-locked actions, separating powers.

  • Proposers can schedule actions but cannot execute them immediately.
  • Executors can trigger the action only after the delay.
  • This separation of duties matters for minimizing trust, as compromising a single role is insufficient to bypass the security model.
05

Cancellation Mechanism

The ability for authorized parties to cancel a proposed action before its execution.

  • Typically reserved for a guardian role or decentralized governance.
  • Allows the community to veto a proposal deemed harmful.
  • This matters as it provides a last-resort safety net if a malicious proposal slips through, enhancing overall resilience.
06

Minimum Delay

A security parameter defining the shortest possible waiting period for any action.

  • It is a fundamental constant set at the Timelock's deployment.
  • Prevents administrators from reducing delays to dangerously short periods.
  • This matters because it establishes a predictable lower bound for security, ensuring users always have a guaranteed minimum time to respond.

Common Implementation Patterns

Process overview

1

Define the Timelock Contract

Establish the core contract that will hold and schedule privileged actions.

Detailed Instructions

The first step is to deploy a dedicated TimelockController contract, often using a battle-tested implementation like OpenZeppelin's. This contract acts as the central scheduler and executor for all delayed actions. During deployment, you must configure several critical parameters: the minimum delay (e.g., 2 days for a DAO treasury), the list of proposers (addresses allowed to queue transactions), and the list of executors (addresses allowed to execute them after the delay). It is a security best practice to set the admin role to a multi-signature wallet or a DAO governance contract, not an EOA.

  • Sub-step 1: Import and inherit from TimelockController in your contract.
  • Sub-step 2: In the constructor, set the minDelay, proposers array, and executors array.
  • Sub-step 3: Deploy the contract and verify its source code on a block explorer.
solidity
// Example deployment script snippet using OpenZeppelin import "@openzeppelin/contracts/governance/TimelockController.sol"; contract MyTimelock is TimelockController { constructor( uint256 minDelay, address[] memory proposers, address[] memory executors, address admin ) TimelockController(minDelay, proposers, executors, admin) {} }

Tip: Use a deterministic deployment proxy like create2 for the Timelock address to simplify future contract integrations.

2

Transfer Contract Ownership to the Timelock

Relinquish admin control of your protocol's core contracts to the timelock.

Detailed Instructions

After the Timelock contract is live, you must transfer the privileged roles of your protocol's smart contracts to its address. This is a critical and irreversible step that enforces the delay on all future administrative actions. Common functions to call include transferOwnership(), grantRole() for specific access control roles, or setting the Timelock as the owner or admin via initialization functions. For each core contract (e.g., Treasury, Token, Staking), you must execute a transaction from the current owner's address to perform the transfer.

  • Sub-step 1: Compile a list of all contracts with privileged functions (minting, pausing, upgrading).
  • Sub-step 2: For each contract, call the relevant ownership transfer function with the Timelock address as the argument.
  • Sub-step 3: Verify the role change by checking the contract's state on a block explorer or by calling view functions.
solidity
// Example: Transferring ownership of an Ownable contract MyToken token = MyToken(0x1234...); token.transferOwnership(timelockAddress); // Example: Granting the DEFAULT_ADMIN_ROLE to the timelock MyGovernor governor = MyGovernor(0x5678...); bytes32 role = governor.DEFAULT_ADMIN_ROLE(); governor.grantRole(role, timelockAddress);

Tip: Perform this step in a controlled environment (testnet first) and have a multi-sig sign the transactions as a final safety check before mainnet.

3

Queue a Governance Proposal

Schedule a delayed action by submitting a transaction to the timelock queue.

Detailed Instructions

To execute any privileged action, a proposer must first queue it. This involves calling the Timelock's schedule function with the exact transaction details: the target contract address, the value (ETH) to send, the calldata (encoded function call), the predecessor (for dependency, often bytes32(0)), and a unique salt (for ID). The timelock will compute and emit a unique operationId based on these parameters. Crucially, the transaction will only be executable after the block timestamp exceeds the scheduled time by at least the minimum delay. This creates the mandatory review period.

  • Sub-step 1: Encode the function call (calldata) for the desired action (e.g., upgradeTo(address)).
  • Sub-step 2: Call timelock.schedule(target, value, data, predecessor, salt, delay). Use getMinDelay() for the delay.
  • Sub-step 3: Record the emitted OperationScheduled event log to get the operationId for tracking.
solidity
// Example: Queuing a proposal to upgrade a proxy contract address target = 0xabcd...; uint256 value = 0; bytes memory data = abi.encodeWithSignature("upgradeTo(address)", newImplementation); bytes32 predecessor = bytes32(0); bytes32 salt = keccak256(abi.encodePacked("Upgrade V1.2")); uint256 delay = timelock.getMinDelay(); timelock.schedule(target, value, data, predecessor, salt, delay);

Tip: The salt allows for proposal cancellation; use a descriptive hash to avoid collisions and for clear audit trails.

4

Execute the Queued Action

Carry out the scheduled transaction after the delay period has elapsed.

Detailed Instructions

After the enforced delay window has passed (check via getTimestamp(id)), any address designated as an executor can call the execute function. This function requires the same parameters used in the schedule call. The timelock will verify three conditions: the operation is ready (timestamp passed), not executed, and not canceled. Upon successful execution, the timelock forwards the call to the target contract with the specified value and calldata. This two-step process ensures a cooling-off period where the community can scrutinize the action and, if necessary, a guardian can cancel it before it takes effect.

  • Sub-step 1: Verify the operation's state by calling timelock.isOperationReady(operationId).
  • Sub-step 2: Prepare the identical parameters (target, value, data, predecessor, salt) used during scheduling.
  • Sub-step 3: Call timelock.execute(target, value, data, predecessor, salt) to trigger the final action.
solidity
// Example: Executing the previously queued upgrade bytes32 operationId = timelock.hashOperation(target, value, data, predecessor, salt); require(timelock.isOperationReady(operationId), "Timelock: operation not ready"); timelock.execute(target, value, data, predecessor, salt);

Tip: Use a keeper network or bot to monitor isOperationReady and execute transactions automatically when the delay expires, ensuring timely execution.

5

Implement Emergency Safeguards

Integrate mechanisms to handle critical failures or malicious proposals.

Detailed Instructions

While timelocks enforce a delay, you must also plan for emergency scenarios. The primary safeguard is the cancel function, which allows a privileged role (often a guardian multi-sig) to halt any queued operation before it becomes executable. This is vital for responding to a discovered vulnerability in a pending upgrade. Additionally, consider implementing a grace period by setting a maximum delay alongside the minimum, preventing proposals from being queued too far in the future. For extreme cases where the timelock itself is compromised, some designs include an escape hatch: a separate, longer-timelocked function that can change the timelock's proposers/executors.

  • Sub-step 1: Assign a trusted guardian address (e.g., a 4/7 multi-sig) the CANCELLER_ROLE on the timelock.
  • Sub-step 2: Monitor all queued operations for suspicious activity during the delay period.
  • Sub-step 3: If necessary, the guardian calls timelock.cancel(operationId) to invalidate the proposal.
solidity
// Example: Guardian canceling a malicious proposal // Only an address with the CANCELLER_ROLE can call this. timelock.cancel(operationId); // Example: View function to check remaining time function getRemainingDelay(bytes32 id) public view returns (uint256) { uint256 timestamp = timelock.getTimestamp(id); if (timestamp == 0 || timestamp > block.timestamp) return 0; return timestamp - block.timestamp; }

Tip: The guardian role should be distinctly separate from the proposer role to maintain checks and balances within the system.

Time Lock Use Cases and Configurations

Comparison of common time lock parameters and their security trade-offs for protocol upgrades.

Configuration ParameterConservative (High Security)Balanced (Common)Aggressive (Low Latency)

Delay Duration

14 days

2-7 days

24-48 hours

Minimum Quorum (Governance)

40% of total supply

20-30% of total supply

10-15% of total supply

Execution Grace Period

7 days

3 days

24 hours

Proposal Threshold

1% of total supply

0.5% of total supply

0.1% of total supply

Multisig Signers Required

7 of 9

4 of 7

2 of 5

Emergency Bypass

None

48-hour timelock with 5/7 multisig

24-hour timelock with 3/5 multisig

Veto Power

Security Council (5/7)

Time delay only

No veto mechanism

Time Locks for Developers and Users

Understanding the Safety Mechanism

A time lock is a security feature that enforces a mandatory waiting period before a proposed change to a protocol can be executed. This delay gives users time to review the change and take action, such as withdrawing funds, if they disagree with it.

Key Points

  • Transparency and Review: All governance proposals, like a parameter change in Compound or a new pool addition in Curve, are made public during the lock period. This prevents immediate, unilateral changes.
  • User Protection: If a malicious proposal passes, the delay acts as an escape hatch. Users can exit the protocol before the change takes effect, protecting their assets.
  • Trust Minimization: It reduces reliance on blind trust in developers or token holders by introducing a verifiable, time-based checkpoint for all major actions.

Example

When a DAO like Uniswap proposes to upgrade its smart contracts, a time lock (often 2-7 days) is enforced. During this period, you can see the exact code that will be deployed. If you are uncomfortable, you can remove your liquidity from the protocol before the upgrade executes.

Auditing Time Lock Security

Process for systematically reviewing time lock implementations to identify vulnerabilities and ensure correct configuration.

1

Review Time Lock Configuration

Examine the core parameters and governance structure of the time lock contract.

Detailed Instructions

Begin by auditing the time lock duration and access control roles. The delay period must be long enough for the community to react to malicious proposals but not so long it hinders protocol agility. Verify the proposer and executor roles are correctly assigned, typically to a governance contract or a multi-signature wallet, not a single EOA.

  • Sub-step 1: Locate and verify the delay state variable. For a mainnet protocol, a common range is 2 to 7 days (e.g., 172800 to 604800 seconds).
  • Sub-step 2: Check the GRACE_PERIOD. This defines how long a queued transaction remains executable; 14 days is a standard value.
  • Sub-step 3: Inspect the role management functions (grantRole, revokeRole) to ensure they are themselves behind a time lock or have robust multisig protection.
solidity
// Example: Checking key parameters in an OpenZeppelin style timelock uint256 public constant MIN_DELAY = 2 days; uint256 public constant MAX_DELAY = 7 days; address public immutable proposer; address public immutable executor;

Tip: Use a block explorer to verify the live contract's configured delay and confirm it matches the documented governance process.

2

Analyze Transaction Queue and Execution Logic

Verify the integrity of the queuing, delaying, and execution mechanisms.

Detailed Instructions

Audit the flow from proposal to execution. The core security property is that a transaction cannot be executed before its timestamp + delay. Scrutinize the queueTransaction and executeTransaction functions for logic flaws. Ensure the eta (estimated time of arrival) is calculated correctly and immutable once queued.

  • Sub-step 1: Trace the queueTransaction function. Confirm it calculates eta = block.timestamp + delay and stores it in a public mapping.
  • Sub-step 2: In executeTransaction, verify the require statement: require(block.timestamp >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock.").
  • Sub-step 3: Check for the require(block.timestamp <= eta + GRACE_PERIOD) condition to prevent execution of stale, potentially dangerous transactions.
solidity
// Critical execution validation function executeTransaction( address target, uint256 value, bytes calldata data, bytes32 predecessor, bytes32 salt ) public payable onlyRole(EXECUTOR_ROLE) { require(block.timestamp >= eta[salt], "Too early"); require(block.timestamp <= eta[salt] + GRACE_PERIOD, "Transaction stale"); // ... execution logic }

Tip: Look for any function that might allow the eta to be modified after queuing or a way to bypass the timestamp check, which would break the core security guarantee.

3

Inspect Privileged Function Exposure

Identify all functions that can modify the time lock's state or bypass its protections.

Detailed Instructions

A critical vulnerability is a function that allows an admin to update the delay without going through the time lock itself. This creates a centralization risk. Similarly, functions that can cancel transactions or change roles must be scrutinized.

  • Sub-step 1: Search for any updateDelay or setDelay function. It must be callable only by the timelock itself (using require(msg.sender == address(this))), ensuring delay changes are also delayed.
  • Sub-step 2: Examine the cancelTransaction function. It should be restricted to the proposer role and should not allow cancellation of transactions that are already executable (eta has passed).
  • Sub-step 3: Verify that the renounceRole function for critical roles (like TIMELOCK_ADMIN_ROLE) is implemented and encouraged to decentralize control post-setup.
solidity
// A secure delay update function function updateDelay(uint256 newDelay) external { require(msg.sender == address(this), "Caller must be timelock itself"); require(newDelay >= MIN_DELAY && newDelay <= MAX_DELAY, "Delay out of range"); delay = newDelay; emit DelayUpdated(newDelay); }

Tip: Use static analysis tools like Slither to automatically detect functions that are not protected by the timelock but can affect critical parameters.

4

Test Integration with Target Contracts

Ensure the timelock is correctly set as the owner or admin of the core protocol contracts.

Detailed Instructions

The timelock's security is irrelevant if it isn't the ultimate owner of the system's privileged functions. Audit the initialization and ownership transfer steps of all contracts under governance. A common finding is a contract where the deployer retains a DEFAULT_ADMIN_ROLE or ownership that bypasses the timelock.

  • Sub-step 1: For each core contract (e.g., Treasury, Governor, Staking), check the owner() or hasRole(DEFAULT_ADMIN_ROLE, ...).
  • Sub-step 2: Verify that the assigned address is the timelock contract address (e.g., 0x123...), not an EOA or a multisig that isn't the timelock executor.
  • Sub-step 3: Review the contract's constructor and initialization functions to ensure the timelock was set correctly at deployment and no initial admin keys were left active.
solidity
// Example: A secure contract constructor constructor(address _timelock) { _grantRole(DEFAULT_ADMIN_ROLE, _timelock); // Timelock gets admin _grantRole(MINTER_ROLE, _timelock); // Timelock can mint _revokeRole(DEFAULT_ADMIN_ROLE, msg.sender); // Deployer renounces }

Tip: Create an access control matrix spreadsheet mapping each privileged function across all contracts to the entity (Timelock, Multisig, EOA) that can call it. Any column not pointing to the Timelock represents a potential vulnerability.

5

Simulate Attack Vectors and Edge Cases

Perform scenario analysis to test the resilience of the timelock system.

Detailed Instructions

Use a forked mainnet environment or write comprehensive unit tests to model adversarial scenarios. Focus on front-running, gas griefing, and timestamp manipulation. The goal is to ensure the timelock enforces its guarantees under non-ideal conditions.

  • Sub-step 1: Test a front-running attack where a malicious actor tries to execute a transaction the moment it becomes valid (block.timestamp == eta). The system should handle this correctly.
  • Sub-step 2: Simulate a gas griefing attack by queuing a transaction with a very low gas limit for execution, causing it to fail during the grace period and expire.
  • Sub-step 3: Analyze the impact of block timestamp manipulation by validators. While limited to ~900 seconds, test if a 1-2 hour variance could allow early execution if the delay is set very short (e.g., 1 hour).
solidity
// Pseudo-test for execution at exact timestamp function testExecuteAtExactETA() public { vm.warp(queuedTransaction.eta); // Set block.timestamp to exact ETA vm.expectEmit(true, true, true, true); emit TransactionExecuted(txHash); timelock.executeTransaction(...); // This should succeed }

Tip: Consider the "cancel and re-queue" attack. If a proposer can cancel a queued transaction and immediately re-queue it with a later ETA, they could potentially delay execution indefinitely. Ensure cancellation has appropriate cooldowns or restrictions.

Frequently Asked Questions

The primary benefit is introducing a mandatory delay period between a governance decision's proposal and its execution. This delay acts as a critical security circuit breaker, allowing users and the community to review the proposed changes. During this window, stakeholders can analyze the code for vulnerabilities, assess the impact on the protocol, and, if necessary, exit their positions or coordinate a response. For example, a 48-hour timelock on a major upgrade to a lending protocol gives depositors time to withdraw funds if they disagree with the new risk parameters.