// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {CheckpointExtensionBase} from "./CheckpointExtensionBase.sol"; import {CheckpointStorage} from "../storage/CheckpointStorage.sol"; import {CheckpointFlags} from "../libraries/CheckpointFlags.sol"; /// @notice Queues non-emergency checkpoints until delay elapses (governance timelock-lite). contract TimelockSubmitExtension is CheckpointExtensionBase { uint256 public delaySeconds = 48 hours; mapping(bytes32 => uint256) public readyAt; event CheckpointQueued(uint64 indexed batchId, bytes32 queueId, uint256 readyAt); function HOOK_BEFORE_SUBMIT() external pure override returns (uint32) { return 1 << 0; } function HOOK_AFTER_SUBMIT() external pure override returns (uint32) { return 0; } function HOOK_ON_CCIP() external pure override returns (uint32) { return 0; } function setDelay(uint256 seconds_) external { delaySeconds = seconds_; } function queueCheckpoint(CheckpointStorage.CheckpointHeader calldata header) external returns (bytes32 queueId) { require(!CheckpointFlags.has(header.flags, CheckpointFlags.EMERGENCY), "emergency"); queueId = keccak256(abi.encode(header.batchId, header.paymentsRoot, header.checkpointBlock)); readyAt[queueId] = block.timestamp + delaySeconds; emit CheckpointQueued(header.batchId, queueId, readyAt[queueId]); } function beforeSubmit(CheckpointStorage.CheckpointHeader calldata header, bytes calldata) external view { if (CheckpointFlags.has(header.flags, CheckpointFlags.EMERGENCY)) return; bytes32 queueId = keccak256(abi.encode(header.batchId, header.paymentsRoot, header.checkpointBlock)); uint256 ready = readyAt[queueId]; if (ready == 0) return; require(block.timestamp >= ready, "timelock"); } function afterSubmit(CheckpointStorage.CheckpointHeader calldata header, bytes calldata) external { bytes32 queueId = keccak256(abi.encode(header.batchId, header.paymentsRoot, header.checkpointBlock)); delete readyAt[queueId]; } function onCCIPReceive(bytes calldata) external pure override {} }