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
}
}}
Submitted on: 2025-10-18 16:49:31
Comments
Log in to comment.
No comments yet.