472 lines
18 KiB
Solidity
472 lines
18 KiB
Solidity
// SPDX-License-Identifier: PROPRIETARY AND MIT
|
||
// Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||
// All rights reserved.
|
||
//
|
||
// This software is proprietary and confidential.
|
||
// Unauthorized copying, modification, or distribution is prohibited.
|
||
//
|
||
// For licensing inquiries: info@hb3-accelerator.com
|
||
// Website: https://hb3-accelerator.com
|
||
// GitHub: https://github.com/HB3-ACCELERATOR
|
||
|
||
pragma solidity ^0.8.20;
|
||
|
||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||
import "@openzeppelin/contracts/utils/Address.sol";
|
||
|
||
/**
|
||
* @title HierarchicalVotingModule
|
||
* @dev Модуль для иерархического голосования между DLE
|
||
*
|
||
* ОСНОВНЫЕ ФУНКЦИИ:
|
||
* - Владение токенами других DLE
|
||
* - Создание предложений для голосования в других DLE
|
||
* - Внутреннее голосование для внешнего голосования
|
||
* - Выполнение внешнего голосования после достижения кворума
|
||
*
|
||
* БЕЗОПАСНОСТЬ:
|
||
* - Только DLE контракт может выполнять операции
|
||
* - Защита от реентерабельности
|
||
* - Валидация всех входных параметров
|
||
* - Проверка прав через governance
|
||
*/
|
||
contract HierarchicalVotingModule is ReentrancyGuard {
|
||
using SafeERC20 for IERC20;
|
||
using Address for address;
|
||
|
||
// Структура для внешнего голосования
|
||
struct ExternalVotingProposal {
|
||
address targetDLE; // Адрес целевого DLE
|
||
uint256 targetProposalId; // ID предложения в целевом DLE
|
||
bool support; // Поддержка предложения
|
||
string reason; // Причина голосования
|
||
bool executed; // Выполнено ли внешнее голосование
|
||
uint256 internalProposalId; // ID внутреннего предложения в DLE
|
||
uint256 votingPower; // Сила голоса (количество токенов)
|
||
uint256 createdAt; // Время создания
|
||
}
|
||
|
||
// Структура для информации о внешнем DLE
|
||
struct ExternalDLEInfo {
|
||
address dleAddress; // Адрес DLE
|
||
string name; // Название DLE
|
||
string symbol; // Символ токена DLE
|
||
uint256 tokenBalance; // Количество токенов на балансе
|
||
bool isActive; // Активен ли DLE
|
||
uint256 addedAt; // Время добавления
|
||
}
|
||
|
||
// Основные переменные
|
||
address public immutable dleContract; // Адрес основного DLE контракта
|
||
address public treasuryModule; // Адрес TreasuryModule (может быть установлен позже)
|
||
|
||
// Хранение внешних DLE
|
||
mapping(address => ExternalDLEInfo) public externalDLEs;
|
||
address[] public externalDLEList;
|
||
mapping(address => uint256) public externalDLEIndex;
|
||
|
||
// Внешние предложения
|
||
mapping(uint256 => ExternalVotingProposal) public externalVotingProposals;
|
||
uint256 public externalProposalCounter;
|
||
|
||
// Статистика
|
||
uint256 public totalExternalDLEs;
|
||
uint256 public totalExternalProposals;
|
||
uint256 public totalExternalVotes;
|
||
|
||
// События
|
||
event TreasuryModuleSet(address indexed treasuryModule, uint256 timestamp);
|
||
event ExternalDLEAdded(
|
||
address indexed dleAddress,
|
||
string name,
|
||
string symbol,
|
||
uint256 tokenBalance,
|
||
uint256 timestamp
|
||
);
|
||
event ExternalDLERemoved(address indexed dleAddress, uint256 timestamp);
|
||
event ExternalVotingProposalCreated(
|
||
uint256 indexed proposalId,
|
||
address indexed targetDLE,
|
||
uint256 targetProposalId,
|
||
bool support,
|
||
string reason,
|
||
uint256 internalProposalId
|
||
);
|
||
event ExternalVoteExecuted(
|
||
uint256 indexed proposalId,
|
||
address indexed targetDLE,
|
||
uint256 targetProposalId,
|
||
bool support,
|
||
uint256 votingPower
|
||
);
|
||
event ExternalDLEBalanceUpdated(
|
||
address indexed dleAddress,
|
||
uint256 oldBalance,
|
||
uint256 newBalance
|
||
);
|
||
|
||
// Модификаторы
|
||
modifier onlyDLE() {
|
||
require(msg.sender == dleContract, "Only DLE contract can call this");
|
||
_;
|
||
}
|
||
|
||
modifier validExternalDLE(address dleAddress) {
|
||
require(externalDLEs[dleAddress].isActive, "External DLE not active");
|
||
_;
|
||
}
|
||
|
||
constructor(address _dleContract) {
|
||
require(_dleContract != address(0), "DLE contract cannot be zero");
|
||
|
||
dleContract = _dleContract;
|
||
treasuryModule = address(0); // Будет установлен позже через governance
|
||
}
|
||
|
||
/**
|
||
* @dev Установить адрес TreasuryModule (только через DLE governance)
|
||
* @param _treasuryModule Адрес TreasuryModule
|
||
*/
|
||
function setTreasuryModule(address _treasuryModule) external onlyDLE {
|
||
require(_treasuryModule != address(0), "Treasury module cannot be zero");
|
||
require(_treasuryModule.code.length > 0, "Treasury module contract does not exist");
|
||
|
||
treasuryModule = _treasuryModule;
|
||
|
||
emit TreasuryModuleSet(_treasuryModule, block.timestamp);
|
||
}
|
||
|
||
/**
|
||
* @dev Добавить внешний DLE (только через DLE governance)
|
||
* @param dleAddress Адрес внешнего DLE
|
||
* @param name Название DLE
|
||
* @param symbol Символ токена DLE
|
||
*/
|
||
function addExternalDLE(
|
||
address dleAddress,
|
||
string memory name,
|
||
string memory symbol
|
||
) external onlyDLE {
|
||
require(dleAddress != address(0), "DLE address cannot be zero");
|
||
require(!externalDLEs[dleAddress].isActive, "External DLE already added");
|
||
require(bytes(name).length > 0, "Name cannot be empty");
|
||
require(bytes(symbol).length > 0, "Symbol cannot be empty");
|
||
require(treasuryModule != address(0), "Treasury module not set");
|
||
|
||
// Проверяем, что DLE контракт существует
|
||
require(dleAddress.code.length > 0, "DLE contract does not exist");
|
||
|
||
// Получаем баланс токенов этого DLE в TreasuryModule
|
||
uint256 tokenBalance = IERC20(dleAddress).balanceOf(treasuryModule);
|
||
|
||
externalDLEs[dleAddress] = ExternalDLEInfo({
|
||
dleAddress: dleAddress,
|
||
name: name,
|
||
symbol: symbol,
|
||
tokenBalance: tokenBalance,
|
||
isActive: true,
|
||
addedAt: block.timestamp
|
||
});
|
||
|
||
externalDLEList.push(dleAddress);
|
||
externalDLEIndex[dleAddress] = externalDLEList.length - 1;
|
||
totalExternalDLEs++;
|
||
|
||
emit ExternalDLEAdded(dleAddress, name, symbol, tokenBalance, block.timestamp);
|
||
}
|
||
|
||
/**
|
||
* @dev Удалить внешний DLE (только через DLE governance)
|
||
* @param dleAddress Адрес внешнего DLE
|
||
*/
|
||
function removeExternalDLE(address dleAddress) external onlyDLE validExternalDLE(dleAddress) {
|
||
require(externalDLEs[dleAddress].tokenBalance == 0, "Token balance must be zero");
|
||
|
||
// Удаляем из массива
|
||
uint256 index = externalDLEIndex[dleAddress];
|
||
uint256 lastIndex = externalDLEList.length - 1;
|
||
|
||
if (index != lastIndex) {
|
||
address lastDLE = externalDLEList[lastIndex];
|
||
externalDLEList[index] = lastDLE;
|
||
externalDLEIndex[lastDLE] = index;
|
||
}
|
||
|
||
externalDLEList.pop();
|
||
delete externalDLEIndex[dleAddress];
|
||
delete externalDLEs[dleAddress];
|
||
totalExternalDLEs--;
|
||
|
||
emit ExternalDLERemoved(dleAddress, block.timestamp);
|
||
}
|
||
|
||
/**
|
||
* @dev Создать предложение для внешнего голосования
|
||
* @param targetDLE Адрес целевого DLE
|
||
* @param targetProposalId ID предложения в целевом DLE
|
||
* @param support Поддержка предложения
|
||
* @param reason Причина голосования
|
||
* @return proposalId ID созданного предложения
|
||
*/
|
||
function createExternalVotingProposal(
|
||
address targetDLE,
|
||
uint256 targetProposalId,
|
||
bool support,
|
||
string memory reason
|
||
) external onlyDLE validExternalDLE(targetDLE) returns (uint256) {
|
||
require(targetProposalId > 0, "Target proposal ID must be positive");
|
||
require(bytes(reason).length > 0, "Reason cannot be empty");
|
||
|
||
ExternalDLEInfo memory dleInfo = externalDLEs[targetDLE];
|
||
require(dleInfo.tokenBalance > 0, "No tokens in target DLE");
|
||
|
||
// Создаем описание для внутреннего предложения
|
||
string memory description = string(abi.encodePacked(
|
||
"Vote in DLE ", dleInfo.name, " (", dleInfo.symbol, ") on proposal #",
|
||
_toString(targetProposalId), ": ", reason
|
||
));
|
||
|
||
// Создаем внутреннее предложение через DLE
|
||
// Это требует интеграции с DLE контрактом
|
||
uint256 internalProposalId = _createInternalProposal(description);
|
||
|
||
uint256 proposalId = externalProposalCounter++;
|
||
externalVotingProposals[proposalId] = ExternalVotingProposal({
|
||
targetDLE: targetDLE,
|
||
targetProposalId: targetProposalId,
|
||
support: support,
|
||
reason: reason,
|
||
executed: false,
|
||
internalProposalId: internalProposalId,
|
||
votingPower: dleInfo.tokenBalance,
|
||
createdAt: block.timestamp
|
||
});
|
||
|
||
totalExternalProposals++;
|
||
|
||
emit ExternalVotingProposalCreated(
|
||
proposalId,
|
||
targetDLE,
|
||
targetProposalId,
|
||
support,
|
||
reason,
|
||
internalProposalId
|
||
);
|
||
|
||
return proposalId;
|
||
}
|
||
|
||
/**
|
||
* @dev Выполнить внешнее голосование (после прохождения внутреннего предложения)
|
||
* @param proposalId ID внешнего предложения
|
||
*/
|
||
function executeExternalVote(uint256 proposalId) external onlyDLE nonReentrant {
|
||
ExternalVotingProposal storage proposal = externalVotingProposals[proposalId];
|
||
require(proposal.targetDLE != address(0), "Proposal not found");
|
||
require(!proposal.executed, "External vote already executed");
|
||
|
||
// Проверяем, что внутреннее предложение прошло
|
||
require(_isInternalProposalPassed(proposal.internalProposalId), "Internal proposal not passed");
|
||
|
||
// Выполняем голосование в целевом DLE
|
||
_executeVoteInTargetDLE(proposal.targetDLE, proposal.targetProposalId, proposal.support);
|
||
|
||
proposal.executed = true;
|
||
totalExternalVotes++;
|
||
|
||
emit ExternalVoteExecuted(
|
||
proposalId,
|
||
proposal.targetDLE,
|
||
proposal.targetProposalId,
|
||
proposal.support,
|
||
proposal.votingPower
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @dev Обновить баланс токенов внешнего DLE
|
||
* @param dleAddress Адрес внешнего DLE
|
||
*/
|
||
function updateExternalDLEBalance(address dleAddress) external onlyDLE validExternalDLE(dleAddress) {
|
||
uint256 oldBalance = externalDLEs[dleAddress].tokenBalance;
|
||
uint256 newBalance = IERC20(dleAddress).balanceOf(treasuryModule);
|
||
|
||
externalDLEs[dleAddress].tokenBalance = newBalance;
|
||
|
||
emit ExternalDLEBalanceUpdated(dleAddress, oldBalance, newBalance);
|
||
}
|
||
|
||
/**
|
||
* @dev Обновить балансы всех внешних DLE
|
||
*/
|
||
function updateAllExternalDLEBalances() external onlyDLE {
|
||
for (uint256 i = 0; i < externalDLEList.length; i++) {
|
||
address dleAddress = externalDLEList[i];
|
||
if (externalDLEs[dleAddress].isActive) {
|
||
uint256 oldBalance = externalDLEs[dleAddress].tokenBalance;
|
||
uint256 newBalance = IERC20(dleAddress).balanceOf(treasuryModule);
|
||
|
||
externalDLEs[dleAddress].tokenBalance = newBalance;
|
||
|
||
emit ExternalDLEBalanceUpdated(dleAddress, oldBalance, newBalance);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== VIEW ФУНКЦИИ =====
|
||
|
||
/**
|
||
* @dev Получить информацию о внешнем DLE
|
||
*/
|
||
function getExternalDLEInfo(address dleAddress) external view returns (ExternalDLEInfo memory) {
|
||
return externalDLEs[dleAddress];
|
||
}
|
||
|
||
/**
|
||
* @dev Получить список всех внешних DLE
|
||
*/
|
||
function getAllExternalDLEs() external view returns (address[] memory) {
|
||
return externalDLEList;
|
||
}
|
||
|
||
/**
|
||
* @dev Получить активные внешние DLE
|
||
*/
|
||
function getActiveExternalDLEs() external view returns (address[] memory) {
|
||
uint256 activeCount = 0;
|
||
|
||
for (uint256 i = 0; i < externalDLEList.length; i++) {
|
||
if (externalDLEs[externalDLEList[i]].isActive) {
|
||
activeCount++;
|
||
}
|
||
}
|
||
|
||
address[] memory activeDLEs = new address[](activeCount);
|
||
uint256 index = 0;
|
||
|
||
for (uint256 i = 0; i < externalDLEList.length; i++) {
|
||
if (externalDLEs[externalDLEList[i]].isActive) {
|
||
activeDLEs[index] = externalDLEList[i];
|
||
index++;
|
||
}
|
||
}
|
||
|
||
return activeDLEs;
|
||
}
|
||
|
||
/**
|
||
* @dev Получить информацию о внешнем предложении
|
||
*/
|
||
function getExternalVotingProposal(uint256 proposalId) external view returns (ExternalVotingProposal memory) {
|
||
return externalVotingProposals[proposalId];
|
||
}
|
||
|
||
/**
|
||
* @dev Получить статистику модуля
|
||
*/
|
||
function getModuleStats() external view returns (
|
||
uint256 totalDLEs,
|
||
uint256 totalProposals,
|
||
uint256 totalVotes,
|
||
uint256 activeDLEs
|
||
) {
|
||
uint256 activeCount = 0;
|
||
for (uint256 i = 0; i < externalDLEList.length; i++) {
|
||
if (externalDLEs[externalDLEList[i]].isActive) {
|
||
activeCount++;
|
||
}
|
||
}
|
||
|
||
return (
|
||
totalExternalDLEs,
|
||
totalExternalProposals,
|
||
totalExternalVotes,
|
||
activeCount
|
||
);
|
||
}
|
||
|
||
// ===== ВНУТРЕННИЕ ФУНКЦИИ =====
|
||
|
||
/**
|
||
* @dev Создать внутреннее предложение в DLE
|
||
* @param description Описание предложения
|
||
* @return proposalId ID созданного предложения
|
||
*/
|
||
function _createInternalProposal(string memory description) internal returns (uint256) {
|
||
// Создаем предложение через стандартный интерфейс DLE
|
||
(bool success, bytes memory data) = dleContract.call(
|
||
abi.encodeWithSignature(
|
||
"createProposal(string,uint256,bytes,uint256,uint256[],uint256)",
|
||
description,
|
||
7 days, // 7 дней голосования
|
||
"", // Пустая операция
|
||
block.chainid, // Текущая сеть
|
||
new uint256[](0), // Пустой массив целевых цепочек
|
||
0 // Без timelock
|
||
)
|
||
);
|
||
|
||
require(success, "Failed to create internal proposal");
|
||
return abi.decode(data, (uint256));
|
||
}
|
||
|
||
/**
|
||
* @dev Проверить, прошло ли внутреннее предложение
|
||
* @param proposalId ID внутреннего предложения
|
||
* @return passed Прошло ли предложение
|
||
*/
|
||
function _isInternalProposalPassed(uint256 proposalId) internal view returns (bool) {
|
||
(bool success, bytes memory data) = dleContract.staticcall(
|
||
abi.encodeWithSignature("checkProposalResult(uint256)", proposalId)
|
||
);
|
||
|
||
if (!success) return false;
|
||
(bool passed, bool quorumReached) = abi.decode(data, (bool, bool));
|
||
return passed && quorumReached;
|
||
}
|
||
|
||
/**
|
||
* @dev Выполнить голосование в целевом DLE
|
||
* @param targetDLE Адрес целевого DLE
|
||
* @param proposalId ID предложения
|
||
* @param support Поддержка предложения
|
||
*/
|
||
function _executeVoteInTargetDLE(
|
||
address targetDLE,
|
||
uint256 proposalId,
|
||
bool support
|
||
) internal {
|
||
// Выполняем голосование напрямую в целевом DLE
|
||
// Это требует, чтобы целевой DLE имел функцию vote
|
||
(bool success, ) = targetDLE.call(
|
||
abi.encodeWithSignature("vote(uint256,bool)", proposalId, support)
|
||
);
|
||
|
||
require(success, "Failed to execute vote in target DLE");
|
||
}
|
||
|
||
/**
|
||
* @dev Конвертировать uint256 в string
|
||
*/
|
||
function _toString(uint256 value) internal pure returns (string memory) {
|
||
if (value == 0) {
|
||
return "0";
|
||
}
|
||
uint256 temp = value;
|
||
uint256 digits;
|
||
while (temp != 0) {
|
||
digits++;
|
||
temp /= 10;
|
||
}
|
||
bytes memory buffer = new bytes(digits);
|
||
while (value != 0) {
|
||
digits -= 1;
|
||
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
|
||
value /= 10;
|
||
}
|
||
return string(buffer);
|
||
}
|
||
}
|