Files
DLE/backend/contracts/HierarchicalVotingModule.sol
2026-03-01 22:03:48 +03:00

472 lines
18 KiB
Solidity
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: PROPRIETARY AND MIT
// Copyright (c) 2024-2026 Тарабанов Александр Викторович
// 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/VC-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);
}
}