398 lines
17 KiB
Solidity
398 lines
17 KiB
Solidity
// SPDX-License-Identifier: PROPRIETARY AND MIT
|
||
// Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||
// All rights reserved.
|
||
|
||
pragma solidity ^0.8.20;
|
||
|
||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||
|
||
/**
|
||
* @title TimelockModule
|
||
* @dev Модуль временной задержки для критических операций DLE
|
||
*
|
||
* НАЗНАЧЕНИЕ:
|
||
* - Добавляет обязательную задержку между принятием и исполнением решений
|
||
* - Даёт время сообществу на обнаружение и отмену вредоносных предложений
|
||
* - Повышает безопасность критических операций (смена кворума, добавление модулей)
|
||
*
|
||
* ПРИНЦИП РАБОТЫ:
|
||
* 1. DLE исполняет операцию не напрямую, а через Timelock
|
||
* 2. Timelock ставит операцию в очередь с задержкой
|
||
* 3. После истечения задержки любой может исполнить операцию
|
||
* 4. В период задержки операцию можно отменить через экстренное голосование
|
||
*/
|
||
contract TimelockModule is ReentrancyGuard {
|
||
|
||
// Структура отложенной операции
|
||
struct QueuedOperation {
|
||
bytes32 id; // Уникальный ID операции
|
||
address target; // Целевой контракт (обычно DLE)
|
||
bytes data; // Данные для вызова
|
||
uint256 executeAfter; // Время, после которого можно исполнить
|
||
uint256 queuedAt; // Время постановки в очередь
|
||
bool executed; // Исполнена ли операция
|
||
bool cancelled; // Отменена ли операция
|
||
address proposer; // Кто поставил в очередь
|
||
string description; // Описание операции
|
||
uint256 delay; // Задержка для этой операции
|
||
}
|
||
|
||
// Основные настройки
|
||
address public immutable dleContract; // Адрес DLE контракта
|
||
uint256 public defaultDelay = 2 days; // Стандартная задержка
|
||
uint256 public emergencyDelay = 30 minutes; // Экстренная задержка
|
||
uint256 public maxDelay = 30 days; // Максимальная задержка
|
||
uint256 public minDelay = 1 hours; // Минимальная задержка
|
||
|
||
// Хранение операций
|
||
mapping(bytes32 => QueuedOperation) public queuedOperations;
|
||
bytes32[] public operationQueue; // Список всех операций
|
||
mapping(bytes32 => uint256) public operationIndex; // ID => индекс в очереди
|
||
|
||
// Категории операций с разными задержками
|
||
mapping(bytes4 => uint256) public operationDelays; // selector => delay
|
||
mapping(bytes4 => bool) public criticalOperations; // критические операции
|
||
mapping(bytes4 => bool) public emergencyOperations; // экстренные операции
|
||
|
||
// Статистика
|
||
uint256 public totalOperations;
|
||
uint256 public executedOperations;
|
||
uint256 public cancelledOperations;
|
||
|
||
// События
|
||
event OperationQueued(
|
||
bytes32 indexed operationId,
|
||
address indexed target,
|
||
bytes data,
|
||
uint256 executeAfter,
|
||
uint256 delay,
|
||
string description
|
||
);
|
||
event OperationExecuted(bytes32 indexed operationId, address indexed executor);
|
||
event OperationCancelled(bytes32 indexed operationId, address indexed canceller, string reason);
|
||
event DelayUpdated(bytes4 indexed selector, uint256 oldDelay, uint256 newDelay);
|
||
event DefaultDelayUpdated(uint256 oldDelay, uint256 newDelay);
|
||
event EmergencyExecution(bytes32 indexed operationId, string reason);
|
||
|
||
// Модификаторы
|
||
modifier onlyDLE() {
|
||
require(msg.sender == dleContract, "Only DLE can call");
|
||
_;
|
||
}
|
||
|
||
modifier validOperation(bytes32 operationId) {
|
||
require(queuedOperations[operationId].id == operationId, "Operation not found");
|
||
require(!queuedOperations[operationId].executed, "Already executed");
|
||
require(!queuedOperations[operationId].cancelled, "Operation cancelled");
|
||
_;
|
||
}
|
||
|
||
constructor(address _dleContract) {
|
||
require(_dleContract != address(0), "DLE contract cannot be zero");
|
||
require(_dleContract.code.length > 0, "DLE contract must exist");
|
||
|
||
dleContract = _dleContract;
|
||
totalOperations = 0;
|
||
|
||
// Настраиваем задержки для разных типов операций
|
||
_setupOperationDelays();
|
||
}
|
||
|
||
/**
|
||
* @dev Поставить операцию в очередь (вызывается из DLE)
|
||
* @param target Целевой контракт
|
||
* @param data Данные операции
|
||
* @param description Описание операции
|
||
*/
|
||
function queueOperation(
|
||
address target,
|
||
bytes memory data,
|
||
string memory description
|
||
) external onlyDLE returns (bytes32) {
|
||
require(target != address(0), "Target cannot be zero");
|
||
require(data.length >= 4, "Invalid operation data");
|
||
|
||
// Определяем задержку для операции
|
||
bytes4 selector;
|
||
assembly {
|
||
selector := mload(add(data, 0x20))
|
||
}
|
||
uint256 delay = _getOperationDelay(selector);
|
||
|
||
// Создаём уникальный ID операции
|
||
bytes32 operationId = keccak256(abi.encodePacked(
|
||
target,
|
||
data,
|
||
block.timestamp,
|
||
totalOperations
|
||
));
|
||
|
||
// Проверяем что операция ещё не существует
|
||
require(queuedOperations[operationId].id == bytes32(0), "Operation already exists");
|
||
|
||
// Создаём операцию
|
||
queuedOperations[operationId] = QueuedOperation({
|
||
id: operationId,
|
||
target: target,
|
||
data: data,
|
||
executeAfter: block.timestamp + delay,
|
||
queuedAt: block.timestamp,
|
||
executed: false,
|
||
cancelled: false,
|
||
proposer: msg.sender, // Адрес вызывающего (обычно DLE контракт)
|
||
description: description,
|
||
delay: delay
|
||
});
|
||
|
||
// Добавляем в очередь
|
||
operationQueue.push(operationId);
|
||
operationIndex[operationId] = operationQueue.length - 1;
|
||
totalOperations++;
|
||
|
||
emit OperationQueued(operationId, target, data, block.timestamp + delay, delay, description);
|
||
|
||
return operationId;
|
||
}
|
||
|
||
/**
|
||
* @dev Исполнить операцию после истечения задержки (может любой)
|
||
* @param operationId ID операции
|
||
*/
|
||
function executeOperation(bytes32 operationId) external nonReentrant validOperation(operationId) {
|
||
QueuedOperation storage operation = queuedOperations[operationId];
|
||
|
||
require(block.timestamp >= operation.executeAfter, "Timelock not expired");
|
||
require(block.timestamp <= operation.executeAfter + 7 days, "Operation expired"); // Операции истекают через неделю
|
||
|
||
operation.executed = true;
|
||
executedOperations++;
|
||
|
||
// Исполняем операцию
|
||
(bool success, bytes memory result) = operation.target.call(operation.data);
|
||
require(success, string(abi.encodePacked("Execution failed: ", result)));
|
||
|
||
emit OperationExecuted(operationId, msg.sender);
|
||
}
|
||
|
||
/**
|
||
* @dev Отменить операцию (только через DLE governance)
|
||
* @param operationId ID операции
|
||
* @param reason Причина отмены
|
||
*/
|
||
function cancelOperation(
|
||
bytes32 operationId,
|
||
string memory reason
|
||
) external onlyDLE validOperation(operationId) {
|
||
QueuedOperation storage operation = queuedOperations[operationId];
|
||
|
||
operation.cancelled = true;
|
||
cancelledOperations++;
|
||
|
||
emit OperationCancelled(operationId, msg.sender, reason);
|
||
}
|
||
|
||
/**
|
||
* @dev Экстренное исполнение без задержки (только для критических ситуаций)
|
||
* @param operationId ID операции
|
||
* @param reason Причина экстренного исполнения
|
||
*/
|
||
function emergencyExecute(
|
||
bytes32 operationId,
|
||
string memory reason
|
||
) external onlyDLE nonReentrant validOperation(operationId) {
|
||
QueuedOperation storage operation = queuedOperations[operationId];
|
||
|
||
// Проверяем что операция помечена как экстренная
|
||
bytes memory opData = operation.data;
|
||
bytes4 selector;
|
||
assembly {
|
||
selector := mload(add(opData, 0x20))
|
||
}
|
||
require(emergencyOperations[selector], "Not emergency operation");
|
||
|
||
operation.executed = true;
|
||
executedOperations++;
|
||
|
||
// Исполняем операцию
|
||
(bool success, bytes memory result) = operation.target.call(operation.data);
|
||
require(success, string(abi.encodePacked("Emergency execution failed: ", result)));
|
||
|
||
emit OperationExecuted(operationId, msg.sender);
|
||
emit EmergencyExecution(operationId, reason);
|
||
}
|
||
|
||
/**
|
||
* @dev Обновить задержку для типа операции (только через governance)
|
||
* @param selector Селектор функции
|
||
* @param newDelay Новая задержка
|
||
* @param isCritical Является ли операция критической
|
||
* @param isEmergency Может ли исполняться экстренно
|
||
*/
|
||
function updateOperationDelay(
|
||
bytes4 selector,
|
||
uint256 newDelay,
|
||
bool isCritical,
|
||
bool isEmergency
|
||
) external onlyDLE {
|
||
require(newDelay >= minDelay, "Delay too short");
|
||
require(newDelay <= maxDelay, "Delay too long");
|
||
|
||
uint256 oldDelay = operationDelays[selector];
|
||
operationDelays[selector] = newDelay;
|
||
criticalOperations[selector] = isCritical;
|
||
emergencyOperations[selector] = isEmergency;
|
||
|
||
emit DelayUpdated(selector, oldDelay, newDelay);
|
||
}
|
||
|
||
/**
|
||
* @dev Обновить стандартную задержку (только через governance)
|
||
* @param newDelay Новая стандартная задержка
|
||
*/
|
||
function updateDefaultDelay(uint256 newDelay) external onlyDLE {
|
||
require(newDelay >= minDelay, "Delay too short");
|
||
require(newDelay <= maxDelay, "Delay too long");
|
||
|
||
uint256 oldDelay = defaultDelay;
|
||
defaultDelay = newDelay;
|
||
|
||
emit DefaultDelayUpdated(oldDelay, newDelay);
|
||
}
|
||
|
||
// ===== VIEW ФУНКЦИИ =====
|
||
|
||
/**
|
||
* @dev Получить информацию об операции
|
||
*/
|
||
function getOperation(bytes32 operationId) external view returns (QueuedOperation memory) {
|
||
return queuedOperations[operationId];
|
||
}
|
||
|
||
/**
|
||
* @dev Проверить, готова ли операция к исполнению
|
||
*/
|
||
function isReady(bytes32 operationId) external view returns (bool) {
|
||
QueuedOperation storage operation = queuedOperations[operationId];
|
||
return operation.id != bytes32(0) &&
|
||
!operation.executed &&
|
||
!operation.cancelled &&
|
||
block.timestamp >= operation.executeAfter;
|
||
}
|
||
|
||
/**
|
||
* @dev Получить время до исполнения операции
|
||
*/
|
||
function getTimeToExecution(bytes32 operationId) external view returns (uint256) {
|
||
QueuedOperation storage operation = queuedOperations[operationId];
|
||
if (operation.executeAfter <= block.timestamp) {
|
||
return 0;
|
||
}
|
||
return operation.executeAfter - block.timestamp;
|
||
}
|
||
|
||
/**
|
||
* @dev Получить список активных операций
|
||
*/
|
||
function getActiveOperations() external view returns (bytes32[] memory) {
|
||
uint256 activeCount = 0;
|
||
|
||
// Считаем активные операции
|
||
for (uint256 i = 0; i < operationQueue.length; i++) {
|
||
QueuedOperation storage op = queuedOperations[operationQueue[i]];
|
||
if (!op.executed && !op.cancelled) {
|
||
activeCount++;
|
||
}
|
||
}
|
||
|
||
// Заполняем массив
|
||
bytes32[] memory activeOps = new bytes32[](activeCount);
|
||
uint256 index = 0;
|
||
|
||
for (uint256 i = 0; i < operationQueue.length; i++) {
|
||
QueuedOperation storage op = queuedOperations[operationQueue[i]];
|
||
if (!op.executed && !op.cancelled) {
|
||
activeOps[index] = operationQueue[i];
|
||
index++;
|
||
}
|
||
}
|
||
|
||
return activeOps;
|
||
}
|
||
|
||
/**
|
||
* @dev Получить статистику Timelock
|
||
*/
|
||
function getTimelockStats() external view returns (
|
||
uint256 total,
|
||
uint256 executed,
|
||
uint256 cancelled,
|
||
uint256 pending,
|
||
uint256 currentDelay
|
||
) {
|
||
return (
|
||
totalOperations,
|
||
executedOperations,
|
||
cancelledOperations,
|
||
totalOperations - executedOperations - cancelledOperations,
|
||
defaultDelay
|
||
);
|
||
}
|
||
|
||
// ===== ВНУТРЕННИЕ ФУНКЦИИ =====
|
||
|
||
/**
|
||
* @dev Определить задержку для операции
|
||
*/
|
||
function _getOperationDelay(bytes4 selector) internal view returns (uint256) {
|
||
uint256 customDelay = operationDelays[selector];
|
||
if (customDelay > 0) {
|
||
return customDelay;
|
||
}
|
||
|
||
// Используем стандартную задержку
|
||
return defaultDelay;
|
||
}
|
||
|
||
/**
|
||
* @dev Настроить стандартные задержки для операций
|
||
*/
|
||
function _setupOperationDelays() internal {
|
||
// Критические операции - длинная задержка (7 дней)
|
||
bytes4 updateQuorum = bytes4(keccak256("updateQuorumPercentage(uint256)"));
|
||
bytes4 addModule = bytes4(keccak256("_addModule(bytes32,address)"));
|
||
bytes4 removeModule = bytes4(keccak256("_removeModule(bytes32)"));
|
||
bytes4 addChain = bytes4(keccak256("_addSupportedChain(uint256)"));
|
||
bytes4 removeChain = bytes4(keccak256("_removeSupportedChain(uint256)"));
|
||
|
||
operationDelays[updateQuorum] = 7 days;
|
||
operationDelays[addModule] = 7 days;
|
||
operationDelays[removeModule] = 7 days;
|
||
operationDelays[addChain] = 5 days;
|
||
operationDelays[removeChain] = 5 days;
|
||
|
||
criticalOperations[updateQuorum] = true;
|
||
criticalOperations[addModule] = true;
|
||
criticalOperations[removeModule] = true;
|
||
criticalOperations[addChain] = true;
|
||
criticalOperations[removeChain] = true;
|
||
|
||
// Обычные операции - стандартная задержка (2 дня)
|
||
bytes4 updateDLEInfo = bytes4(keccak256("updateDLEInfo(string,string,string,string,uint256,string[],uint256)"));
|
||
bytes4 updateVotingDurations = bytes4(keccak256("_updateVotingDurations(uint256,uint256)"));
|
||
|
||
operationDelays[updateDLEInfo] = 2 days;
|
||
operationDelays[updateVotingDurations] = 1 days;
|
||
|
||
// Treasury операции - короткая задержка (1 день)
|
||
bytes4 treasuryTransfer = bytes4(keccak256("treasuryTransfer(address,address,uint256)"));
|
||
bytes4 treasuryAddToken = bytes4(keccak256("treasuryAddToken(address,string,uint8)"));
|
||
|
||
operationDelays[treasuryTransfer] = 1 days;
|
||
operationDelays[treasuryAddToken] = 1 days;
|
||
|
||
// Экстренные операции (могут исполняться немедленно при необходимости)
|
||
emergencyOperations[removeModule] = true; // Удаление вредоносного модуля
|
||
emergencyOperations[removeChain] = true; // Отключение скомпрометированной сети
|
||
}
|
||
}
|