TimelockController

Description:

Smart contract deployed on Ethereum with Factory features.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/security/TimelockController.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

/**
 * @title TimelockController
 * @notice Enhanced timelock with guardian support for emergency response
 * @dev Guardian can pause but cannot unpause, providing defense without griefing risk
 */
contract TimelockController {
    // ============ Constants ============
    
    uint256 public constant TIMELOCK_DELAY = 6 hours;
    uint256 public constant GRACE_PERIOD = 7 days;
    
    // Function selectors that require timelock
    bytes4 private constant SELECTOR_ADD_STRATEGY = bytes4(keccak256("addStrategy(string,address)"));
    bytes4 private constant SELECTOR_REMOVE_STRATEGY = bytes4(keccak256("removeStrategy(string)"));
    bytes4 private constant SELECTOR_SET_CONTROLLER = bytes4(keccak256("setController(address)"));
    bytes4 private constant SELECTOR_SET_PERFORMANCE_FEE = bytes4(keccak256("setPerformanceFee(uint256)"));
    bytes4 private constant SELECTOR_SET_VAULT = bytes4(keccak256("setVault(address)"));
    bytes4 private constant SELECTOR_SET_APY_ORACLE = bytes4(keccak256("setAPYOracle(address)"));
    bytes4 private constant SELECTOR_SET_REBALANCE_THRESHOLD = bytes4(keccak256("setRebalanceThreshold(uint256)"));
    
    // Function selectors that can be executed immediately (emergencies and operations)
    bytes4 private constant SELECTOR_PAUSE = bytes4(keccak256("pause()"));
    bytes4 private constant SELECTOR_UNPAUSE = bytes4(keccak256("unpause()"));
    bytes4 private constant SELECTOR_REBALANCE = bytes4(keccak256("rebalance()"));
    bytes4 private constant SELECTOR_CLAIM_FEES = bytes4(keccak256("claimFees()"));
    bytes4 private constant SELECTOR_EMERGENCY_WITHDRAW = bytes4(keccak256("emergencyWithdraw(string)"));
    bytes4 private constant SELECTOR_DEPLOY_TO_STRATEGY = bytes4(keccak256("deployToStrategy(string,uint256)"));
    
    // ============ Storage ============
    
    address public admin;
    address public pendingAdmin;
    address public guardian;  // Can pause but not unpause
    
    bool public globalPaused;  // Guardian-triggered pause state
    
    mapping(bytes32 => uint256) public queuedTransactions;
    mapping(bytes4 => bool) public requiresTimelock;
    mapping(bytes4 => bool) public isEmergencyFunction;
    
    // ============ Events ============
    
    event TransactionQueued(
        bytes32 indexed txHash,
        address indexed target,
        bytes data,
        uint256 executeTime
    );
    
    event TransactionExecuted(
        bytes32 indexed txHash,
        address indexed target,
        bytes data
    );
    
    event TransactionCancelled(
        bytes32 indexed txHash,
        address indexed target,
        bytes data
    );
    
    event AdminChanged(address indexed previousAdmin, address indexed newAdmin);
    event AdminChangeInitiated(address indexed currentAdmin, address indexed pendingAdmin);
    event GuardianSet(address indexed previousGuardian, address indexed newGuardian);
    event EmergencyPause(address indexed triggeredBy);
    event EmergencyUnpause(address indexed triggeredBy);
    
    // ============ Errors ============
    
    error NotAdmin();
    error NotAdminOrGuardian();
    error NotPendingAdmin();
    error InvalidDelay();
    error TransactionNotQueued();
    error TimelockNotExpired();
    error TransactionExpired();
    error ExecutionFailed();
    error ZeroAddress();
    error AlreadyQueued();
    error GloballyPaused();
    error NotPaused();
    
    // ============ Modifiers ============
    
    modifier onlyAdmin() {
        if (msg.sender != admin) revert NotAdmin();
        _;
    }
    
    modifier onlyAdminOrGuardian() {
        if (msg.sender != admin && msg.sender != guardian) revert NotAdminOrGuardian();
        _;
    }
    
    modifier whenNotPaused() {
        if (globalPaused) revert GloballyPaused();
        _;
    }
    
    // ============ Constructor ============
    
    constructor(address _admin, address _guardian) {
        if (_admin == address(0)) revert ZeroAddress();
        admin = _admin;
        guardian = _guardian; // Can be address(0) initially
        
        // Initialize functions that require timelock
        requiresTimelock[SELECTOR_ADD_STRATEGY] = true;
        requiresTimelock[SELECTOR_REMOVE_STRATEGY] = true;
        requiresTimelock[SELECTOR_SET_CONTROLLER] = true;
        requiresTimelock[SELECTOR_SET_PERFORMANCE_FEE] = true;
        requiresTimelock[SELECTOR_SET_VAULT] = true;
        requiresTimelock[SELECTOR_SET_APY_ORACLE] = true;
        // NOTE: setRebalanceThreshold NOT in requiresTimelock - it's an emergency function
        
        // Initialize emergency functions (no timelock needed)
        isEmergencyFunction[SELECTOR_PAUSE] = true;
        isEmergencyFunction[SELECTOR_UNPAUSE] = true;
        // CRITICAL FIX: Rebalance is NOT an emergency function - only keeper should rebalance immediately
        // isEmergencyFunction[SELECTOR_REBALANCE] = true;  // REMOVED - keeper handles this
        isEmergencyFunction[SELECTOR_SET_REBALANCE_THRESHOLD] = true;  // Needs to be immediate for agent
        isEmergencyFunction[SELECTOR_CLAIM_FEES] = true;
        isEmergencyFunction[SELECTOR_EMERGENCY_WITHDRAW] = true;  // Immediate for emergency response
        isEmergencyFunction[SELECTOR_DEPLOY_TO_STRATEGY] = true;  // Immediate for market opportunities
        
        emit AdminChanged(address(0), _admin);
        if (_guardian != address(0)) {
            emit GuardianSet(address(0), _guardian);
        }
    }
    
    // ============ Guardian Functions ============
    
    /**
     * @notice Emergency pause - can be called by admin or guardian
     * @dev Guardian can pause to stop attacks but cannot unpause to prevent griefing
     */
    function emergencyPause() external onlyAdminOrGuardian {
        if (globalPaused) revert AlreadyQueued(); // Reusing error for "already paused"
        globalPaused = true;
        emit EmergencyPause(msg.sender);
    }
    
    /**
     * @notice Unpause - only admin can unpause
     * @dev Prevents guardian from griefing by pausing/unpausing repeatedly
     */
    function emergencyUnpause() external onlyAdmin {
        if (!globalPaused) revert NotPaused();
        globalPaused = false;
        emit EmergencyUnpause(msg.sender);
    }
    
    /**
     * @notice Set or update guardian address
     * @param newGuardian The new guardian address (can be address(0) to remove)
     */
    function setGuardian(address newGuardian) external onlyAdmin {
        address oldGuardian = guardian;
        guardian = newGuardian;
        emit GuardianSet(oldGuardian, newGuardian);
    }
    
    // ============ Timelock Functions ============
    
    /**
     * @notice Queue a transaction for timelock execution
     * @param target The contract to call
     * @param data The encoded function call
     * @return txHash The hash of the queued transaction
     */
    function queueTransaction(
        address target,
        bytes calldata data
    ) external onlyAdmin whenNotPaused returns (bytes32 txHash) {
        // Check if this function requires timelock
        bytes4 selector = bytes4(data);
        
        // If it's an emergency function, execute immediately
        if (isEmergencyFunction[selector]) {
            (bool success, bytes memory returnData) = target.call(data);
            if (!success) {
                if (returnData.length > 0) {
                    assembly {
                        revert(add(32, returnData), mload(returnData))
                    }
                }
                revert ExecutionFailed();
            }
            emit TransactionExecuted(0, target, data);
            return 0;
        }
        
        // Calculate execution time
        uint256 executeTime = block.timestamp + TIMELOCK_DELAY;
        
        // Generate transaction hash
        txHash = keccak256(abi.encode(target, data, executeTime));
        
        // Check if already queued
        if (queuedTransactions[txHash] != 0) revert AlreadyQueued();
        
        // Queue the transaction
        queuedTransactions[txHash] = executeTime;
        
        emit TransactionQueued(txHash, target, data, executeTime);
    }
    
    /**
     * @notice Execute a queued transaction after timelock expires
     * @param target The contract to call
     * @param data The encoded function call
     * @param executeTime The timestamp when execution is allowed
     */
    function executeTransaction(
        address target,
        bytes calldata data,
        uint256 executeTime
    ) external onlyAdmin whenNotPaused {
        // Generate transaction hash
        bytes32 txHash = keccak256(abi.encode(target, data, executeTime));
        
        // Verify transaction is queued
        if (queuedTransactions[txHash] == 0) revert TransactionNotQueued();
        
        // Verify timelock has expired
        if (block.timestamp < executeTime) revert TimelockNotExpired();
        
        // Verify within grace period
        if (block.timestamp > executeTime + GRACE_PERIOD) revert TransactionExpired();
        
        // Delete from queue
        delete queuedTransactions[txHash];
        
        // Execute transaction
        (bool success, bytes memory returnData) = target.call(data);
        if (!success) {
            if (returnData.length > 0) {
                assembly {
                    revert(add(32, returnData), mload(returnData))
                }
            }
            revert ExecutionFailed();
        }
        
        emit TransactionExecuted(txHash, target, data);
    }
    
    /**
     * @notice Cancel a queued transaction
     * @param target The contract to call
     * @param data The encoded function call
     * @param executeTime The timestamp when execution is allowed
     */
    function cancelTransaction(
        address target,
        bytes calldata data,
        uint256 executeTime
    ) external onlyAdmin {
        // Generate transaction hash
        bytes32 txHash = keccak256(abi.encode(target, data, executeTime));
        
        // Verify transaction is queued
        if (queuedTransactions[txHash] == 0) revert TransactionNotQueued();
        
        // Delete from queue
        delete queuedTransactions[txHash];
        
        emit TransactionCancelled(txHash, target, data);
    }
    
    /**
     * @notice Queue admin change (now goes through timelock)
     * @param newAdmin The new admin address
     */
    function queueAdminChange(address newAdmin) external onlyAdmin whenNotPaused returns (bytes32) {
        if (newAdmin == address(0)) revert ZeroAddress();
        
        // Create the data for the internal admin change
        bytes memory data = abi.encodeWithSignature("initiateAdminChange(address)", newAdmin);
        
        // Queue it through timelock
        uint256 executeTime = block.timestamp + TIMELOCK_DELAY;
        bytes32 txHash = keccak256(abi.encode(address(this), data, executeTime));
        
        if (queuedTransactions[txHash] != 0) revert AlreadyQueued();
        queuedTransactions[txHash] = executeTime;
        
        emit TransactionQueued(txHash, address(this), data, executeTime);
        return txHash;
    }
    
    /**
     * @notice Internal function to initiate admin change
     * @dev This can only be called through executeTransaction after timelock
     */
    function initiateAdminChange(address newAdmin) external {
        // Can only be called by this contract (through executeTransaction)
        require(msg.sender == address(this), "Only through timelock");
        pendingAdmin = newAdmin;
        emit AdminChangeInitiated(admin, newAdmin);
    }
    
    /**
     * @notice Accept admin role (must be called by pending admin)
     */
    function acceptAdmin() external {
        if (msg.sender != pendingAdmin) revert NotPendingAdmin();
        emit AdminChanged(admin, pendingAdmin);
        admin = pendingAdmin;
        pendingAdmin = address(0);
    }
    
    // ============ View Functions ============
    
    /**
     * @notice Check if a function selector requires timelock
     * @param selector The function selector to check
     * @return Whether the function requires timelock
     */
    function functionRequiresTimelock(bytes4 selector) external view returns (bool) {
        return requiresTimelock[selector];
    }
    
    /**
     * @notice Check if a function selector is an emergency function
     * @param selector The function selector to check
     * @return Whether the function is an emergency function
     */
    function isFunctionEmergency(bytes4 selector) external view returns (bool) {
        return isEmergencyFunction[selector];
    }
    
    /**
     * @notice Get the execution time for a queued transaction
     * @param target The contract to call
     * @param data The encoded function call
     * @param executeTime The timestamp when execution is allowed
     * @return The execution timestamp (0 if not queued)
     */
    function getTransactionExecuteTime(
        address target,
        bytes calldata data,
        uint256 executeTime
    ) external view returns (uint256) {
        bytes32 txHash = keccak256(abi.encode(target, data, executeTime));
        return queuedTransactions[txHash];
    }
    
    /**
     * @notice Check if the protocol is paused
     * @return Whether the protocol is paused
     */
    function isPaused() external view returns (bool) {
        return globalPaused;
    }
}"
    }
  },
  "settings": {
    "remappings": [
      "forge-std/=lib/forge-std/src/",
      "@openzeppelin/=lib/openzeppelin-contracts/",
      "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
      "erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/",
      "halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/",
      "openzeppelin-contracts/=lib/openzeppelin-contracts/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "evmVersion": "cancun",
    "viaIR": true
  }
}}

Tags:
Factory|addr:0x1ab9e1f56b7fec72680d39941ba48ce8f6ade6f2|verified:true|block:23604929|tx:0x265c9d3ddbff47b4736c7d3c51562ff86e63c856a04c336d79aa0e6662272697|first_check:1760798968

Submitted on: 2025-10-18 16:49:31

Comments

Log in to comment.

No comments yet.