ваше сообщение коммита
This commit is contained in:
471
backend/contracts/HierarchicalVotingModule.sol
Normal file
471
backend/contracts/HierarchicalVotingModule.sol
Normal file
@@ -0,0 +1,471 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user