ваше сообщение коммита
This commit is contained in:
@@ -77,7 +77,7 @@ const tokensRouter = require('./routes/tokens');
|
||||
const isicRoutes = require('./routes/isic'); // Добавленный импорт
|
||||
const kppRoutes = require('./routes/kpp'); // Добавленный импорт КПП кодов
|
||||
const geocodingRoutes = require('./routes/geocoding'); // Добавленный импорт
|
||||
const dleRoutes = require('./routes/dle'); // Добавляем импорт DLE маршрутов
|
||||
|
||||
const dleV2Routes = require('./routes/dleV2'); // Добавляем импорт DLE v2 маршрутов
|
||||
const settingsRoutes = require('./routes/settings'); // Добавляем импорт маршрутов настроек
|
||||
const tablesRoutes = require('./routes/tables'); // Добавляем импорт таблиц
|
||||
@@ -200,7 +200,7 @@ app.use('/api/tokens', tokensRouter);
|
||||
app.use('/api/isic', isicRoutes); // Добавленное использование роута
|
||||
app.use('/api/kpp', kppRoutes); // Добавленное использование роута КПП кодов
|
||||
app.use('/api/geocoding', geocodingRoutes); // Добавленное использование роута
|
||||
app.use('/api/dle', dleRoutes); // Добавляем маршрут DLE
|
||||
|
||||
app.use('/api/dle-v2', dleV2Routes); // Добавляем маршрут DLE v2
|
||||
app.use('/api/settings', settingsRoutes); // Добавляем маршрут настроек
|
||||
app.use('/api/countries', countriesRoutes); // Добавляем маршрут стран
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../../../build-info/e497981aa6f6963ae97f832d3d617a54.json"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"_format": "hh-sol-artifact-1",
|
||||
"contractName": "ReentrancyGuard",
|
||||
"sourceName": "@openzeppelin/contracts/utils/ReentrancyGuard.sol",
|
||||
"abi": [
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "ReentrancyGuardReentrantCall",
|
||||
"type": "error"
|
||||
}
|
||||
],
|
||||
"bytecode": "0x",
|
||||
"deployedBytecode": "0x",
|
||||
"linkReferences": {},
|
||||
"deployedLinkReferences": {}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"_format": "hh-sol-dbg-1",
|
||||
"buildInfo": "../../build-info/6575c8c945d0e94a5880cf71f96c424f.json"
|
||||
"buildInfo": "../../build-info/f60ede94c341608b6f6d2f67c953fa04.json"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
51
backend/cache/solidity-files-cache.json
vendored
51
backend/cache/solidity-files-cache.json
vendored
@@ -2,8 +2,8 @@
|
||||
"_format": "hh-sol-cache-2",
|
||||
"files": {
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/contracts/DLE.sol": {
|
||||
"lastModificationDate": 1753643388350,
|
||||
"contentHash": "bd9d569a19ab1ef7f7ce5d40f05c9857",
|
||||
"lastModificationDate": 1753795256078,
|
||||
"contentHash": "1fada989abe7c49f7fb352c3b427549e",
|
||||
"sourceName": "contracts/DLE.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.20",
|
||||
@@ -31,14 +31,8 @@
|
||||
}
|
||||
},
|
||||
"imports": [
|
||||
"@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol",
|
||||
"@openzeppelin/contracts/governance/Governor.sol",
|
||||
"@openzeppelin/contracts/governance/extensions/GovernorSettings.sol",
|
||||
"@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol",
|
||||
"@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol",
|
||||
"@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol",
|
||||
"@openzeppelin/contracts/governance/utils/IVotes.sol",
|
||||
"@openzeppelin/contracts/utils/Nonces.sol"
|
||||
"@openzeppelin/contracts/token/ERC20/ERC20.sol",
|
||||
"@openzeppelin/contracts/utils/ReentrancyGuard.sol"
|
||||
],
|
||||
"versionPragmas": [
|
||||
"^0.8.20"
|
||||
@@ -1855,6 +1849,43 @@
|
||||
"artifacts": [
|
||||
"IERC20Metadata"
|
||||
]
|
||||
},
|
||||
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/ReentrancyGuard.sol": {
|
||||
"lastModificationDate": 1751738715692,
|
||||
"contentHash": "190613e556d509d9e9a0ea43dc5d891d",
|
||||
"sourceName": "@openzeppelin/contracts/utils/ReentrancyGuard.sol",
|
||||
"solcConfig": {
|
||||
"version": "0.8.20",
|
||||
"settings": {
|
||||
"optimizer": {
|
||||
"enabled": true,
|
||||
"runs": 200
|
||||
},
|
||||
"viaIR": true,
|
||||
"evmVersion": "paris",
|
||||
"outputSelection": {
|
||||
"*": {
|
||||
"*": [
|
||||
"abi",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.methodIdentifiers",
|
||||
"metadata"
|
||||
],
|
||||
"": [
|
||||
"ast"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"imports": [],
|
||||
"versionPragmas": [
|
||||
"^0.8.20"
|
||||
],
|
||||
"artifacts": [
|
||||
"ReentrancyGuard"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "test2",
|
||||
"symbol": "test2",
|
||||
"location": "245000, 中国, 黄山市",
|
||||
"isicCodes": [
|
||||
"6810"
|
||||
],
|
||||
"tokenAddress": "0xef49261169B454f191678D2aFC5E91Ad2e85dfD8",
|
||||
"timelockAddress": "0xd5Aea926fc023f11D7D1e762Ca4A11c87830D7ea",
|
||||
"governorAddress": "0xe599F51CAF812E19666DDd6f3002665A2Ca83DaC",
|
||||
"creationBlock": 8324324,
|
||||
"creationTimestamp": 1747217076,
|
||||
"deployedManually": true
|
||||
}
|
||||
@@ -1,27 +1,25 @@
|
||||
// SPDX-License-Identifier: PROPRIETARY
|
||||
// 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
|
||||
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
|
||||
import "@openzeppelin/contracts/governance/Governor.sol";
|
||||
import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
|
||||
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
|
||||
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
|
||||
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
|
||||
import "@openzeppelin/contracts/governance/utils/IVotes.sol";
|
||||
import "@openzeppelin/contracts/utils/Nonces.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
|
||||
/**
|
||||
* @title DLE (Digital Legal Entity)
|
||||
* @dev Основной контракт DLE с отдельным модулем TimelockController.
|
||||
* @dev Основной контракт DLE с модульной архитектурой и мульти-чейн поддержкой
|
||||
*/
|
||||
contract DLE is
|
||||
ERC20Votes,
|
||||
Governor,
|
||||
GovernorSettings,
|
||||
GovernorCountingSimple,
|
||||
GovernorVotesQuorumFraction,
|
||||
GovernorTimelockControl
|
||||
{
|
||||
contract DLE is ERC20, ReentrancyGuard {
|
||||
struct DLEInfo {
|
||||
string name;
|
||||
string symbol;
|
||||
@@ -44,64 +42,57 @@ contract DLE is
|
||||
uint256 oktmo;
|
||||
string[] okvedCodes;
|
||||
uint256 kpp;
|
||||
uint48 votingDelay;
|
||||
uint32 votingPeriod;
|
||||
uint256 proposalThreshold;
|
||||
uint256 quorumPercentage;
|
||||
address[] initialPartners;
|
||||
uint256[] initialAmounts;
|
||||
uint256[] supportedChainIds; // Поддерживаемые цепочки
|
||||
}
|
||||
|
||||
struct Proposal {
|
||||
bytes operation;
|
||||
uint256[] targetChains;
|
||||
uint256 timelock;
|
||||
uint256 governanceChain;
|
||||
address initiator;
|
||||
bytes[] signatures;
|
||||
bool executed;
|
||||
uint256 quorumRequired;
|
||||
uint256 signaturesCount;
|
||||
}
|
||||
|
||||
struct TokenDistributionProposal {
|
||||
address[] partners;
|
||||
uint256[] amounts;
|
||||
uint256 timelock;
|
||||
address initiator;
|
||||
bytes[] signatures;
|
||||
bool executed;
|
||||
uint256 quorumRequired;
|
||||
uint256 signaturesCount;
|
||||
uint256 id;
|
||||
string description;
|
||||
}
|
||||
|
||||
struct TreasuryProposal {
|
||||
address recipient;
|
||||
uint256 amount;
|
||||
uint256 timelock;
|
||||
address initiator;
|
||||
bytes[] signatures;
|
||||
uint256 forVotes;
|
||||
uint256 againstVotes;
|
||||
bool executed;
|
||||
uint256 quorumRequired;
|
||||
uint256 signaturesCount;
|
||||
string description;
|
||||
uint256 deadline;
|
||||
address initiator;
|
||||
bytes operation; // Операция для исполнения
|
||||
mapping(address => bool) hasVoted;
|
||||
mapping(uint256 => bool) chainVoteSynced; // Синхронизация голосов между цепочками
|
||||
}
|
||||
|
||||
struct MultiSigOperation {
|
||||
bytes32 operationHash;
|
||||
uint256 forSignatures;
|
||||
uint256 againstSignatures;
|
||||
bool executed;
|
||||
uint256 deadline;
|
||||
address initiator;
|
||||
mapping(address => bool) hasSigned;
|
||||
mapping(uint256 => bool) chainSignSynced; // Синхронизация подписей между цепочками
|
||||
}
|
||||
|
||||
// Основные настройки
|
||||
DLEInfo public dleInfo;
|
||||
mapping(uint256 => Proposal) public proposals;
|
||||
mapping(uint256 => TokenDistributionProposal) public tokenDistributionProposals;
|
||||
mapping(uint256 => TreasuryProposal) public treasuryProposals;
|
||||
uint256 public proposalCounter;
|
||||
uint256 public tokenDistributionProposalCounter;
|
||||
uint256 public treasuryProposalCounter;
|
||||
uint256 public quorumPercentage;
|
||||
bool public initialTokensDistributed = false;
|
||||
uint256 public proposalCounter;
|
||||
uint256 public multiSigCounter;
|
||||
uint256 public currentChainId;
|
||||
|
||||
// Казначейские функции
|
||||
mapping(address => uint256) public lastWithdrawalBlock; // Последний блок вывода для каждого адреса
|
||||
uint256 public totalTreasuryBalance; // Общий баланс казны
|
||||
// Модули
|
||||
mapping(bytes32 => address) public modules;
|
||||
mapping(bytes32 => bool) public activeModules;
|
||||
|
||||
// Предложения и мультиподписи
|
||||
mapping(uint256 => Proposal) public proposals;
|
||||
mapping(uint256 => MultiSigOperation) public multiSigOperations;
|
||||
|
||||
// Мульти-чейн
|
||||
mapping(uint256 => bool) public supportedChains;
|
||||
mapping(uint256 => bool) public executedProposals; // Синхронизация исполненных предложений
|
||||
mapping(uint256 => bool) public executedMultiSig; // Синхронизация исполненных мультиподписей
|
||||
|
||||
// События
|
||||
event DLEInitialized(
|
||||
string name,
|
||||
string symbol,
|
||||
@@ -112,32 +103,25 @@ contract DLE is
|
||||
string[] okvedCodes,
|
||||
uint256 kpp,
|
||||
address tokenAddress,
|
||||
address timelockAddress,
|
||||
address governorAddress
|
||||
uint256[] supportedChainIds
|
||||
);
|
||||
event InitialTokensDistributed(address[] partners, uint256[] amounts);
|
||||
event TokensDepositedToTreasury(address depositor, uint256 amount);
|
||||
event TreasuryProposalCreated(uint256 proposalId, address initiator, address recipient, uint256 amount, string description);
|
||||
event TreasuryProposalSigned(uint256 proposalId, address signer, uint256 signaturesCount);
|
||||
event TreasuryProposalExecuted(uint256 proposalId, address recipient, uint256 amount);
|
||||
event TokenDistributionProposalCreated(uint256 proposalId, address initiator, address[] partners, uint256[] amounts, string description);
|
||||
event TokenDistributionProposalSigned(uint256 proposalId, address signer, uint256 signaturesCount);
|
||||
event TokenDistributionProposalExecuted(uint256 proposalId, address[] partners, uint256[] amounts);
|
||||
event ProposalCreated(uint256 proposalId, address initiator, bytes operation);
|
||||
event ProposalSigned(uint256 proposalId, address signer, uint256 signaturesCount);
|
||||
event ModuleInstalled(string moduleName, address moduleAddress);
|
||||
event ProposalCreated(uint256 proposalId, address initiator, string description);
|
||||
event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower);
|
||||
event ProposalExecuted(uint256 proposalId, bytes operation);
|
||||
event MultiSigOperationCreated(uint256 operationId, address initiator, bytes32 operationHash);
|
||||
event MultiSigSigned(uint256 operationId, address signer, bool support, uint256 signaturePower);
|
||||
event MultiSigExecuted(uint256 operationId, bytes32 operationHash);
|
||||
event ModuleAdded(bytes32 moduleId, address moduleAddress);
|
||||
event ModuleRemoved(bytes32 moduleId);
|
||||
event CrossChainExecutionSync(uint256 proposalId, uint256 fromChainId, uint256 toChainId);
|
||||
event CrossChainVoteSync(uint256 proposalId, uint256 fromChainId, uint256 toChainId);
|
||||
event CrossChainMultiSigSync(uint256 operationId, uint256 fromChainId, uint256 toChainId);
|
||||
|
||||
constructor(
|
||||
DLEConfig memory config,
|
||||
address timelockAddress
|
||||
)
|
||||
ERC20(config.name, config.symbol)
|
||||
Governor(config.name)
|
||||
GovernorSettings(config.votingDelay, config.votingPeriod, config.proposalThreshold)
|
||||
GovernorVotesQuorumFraction(config.quorumPercentage)
|
||||
GovernorTimelockControl(TimelockController(payable(timelockAddress)))
|
||||
GovernorVotes(IVotes(address(this)))
|
||||
{
|
||||
uint256 _currentChainId
|
||||
) ERC20(config.name, config.symbol) {
|
||||
dleInfo = DLEInfo({
|
||||
name: config.name,
|
||||
symbol: config.symbol,
|
||||
@@ -150,9 +134,16 @@ contract DLE is
|
||||
creationTimestamp: block.timestamp,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
quorumPercentage = config.quorumPercentage;
|
||||
currentChainId = _currentChainId;
|
||||
|
||||
// Автоматически распределяем начальные токены партнерам при деплое
|
||||
// Настраиваем поддерживаемые цепочки
|
||||
for (uint256 i = 0; i < config.supportedChainIds.length; i++) {
|
||||
supportedChains[config.supportedChainIds[i]] = true;
|
||||
}
|
||||
|
||||
// Распределяем начальные токены партнерам
|
||||
require(config.initialPartners.length == config.initialAmounts.length, "Arrays length mismatch");
|
||||
require(config.initialPartners.length > 0, "No initial partners");
|
||||
|
||||
@@ -162,9 +153,7 @@ contract DLE is
|
||||
_mint(config.initialPartners[i], config.initialAmounts[i]);
|
||||
}
|
||||
|
||||
initialTokensDistributed = true;
|
||||
emit InitialTokensDistributed(config.initialPartners, config.initialAmounts);
|
||||
|
||||
emit DLEInitialized(
|
||||
config.name,
|
||||
config.symbol,
|
||||
@@ -175,349 +164,455 @@ contract DLE is
|
||||
config.okvedCodes,
|
||||
config.kpp,
|
||||
address(this),
|
||||
timelockAddress,
|
||||
address(this)
|
||||
config.supportedChainIds
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Создать предложение на распределение токенов
|
||||
* @param _partners Массив адресов партнеров
|
||||
* @param _amounts Массив сумм токенов для каждого партнера
|
||||
* @param _timelock Время исполнения (timestamp)
|
||||
* @dev Создать предложение с выбором цепочки для кворума
|
||||
* @param _description Описание предложения
|
||||
* @param _duration Длительность голосования в секундах
|
||||
* @param _operation Операция для исполнения
|
||||
* @param _governanceChainId ID цепочки для сбора голосов
|
||||
*/
|
||||
function createTokenDistributionProposal(
|
||||
address[] memory _partners,
|
||||
uint256[] memory _amounts,
|
||||
uint256 _timelock,
|
||||
string memory _description
|
||||
) external returns (uint256) {
|
||||
require(_partners.length == _amounts.length, "Arrays length mismatch");
|
||||
require(_partners.length > 0, "Empty arrays");
|
||||
require(_timelock > block.timestamp, "Invalid timelock");
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
|
||||
|
||||
uint256 proposalId = tokenDistributionProposalCounter++;
|
||||
|
||||
tokenDistributionProposals[proposalId] = TokenDistributionProposal({
|
||||
partners: _partners,
|
||||
amounts: _amounts,
|
||||
timelock: _timelock,
|
||||
initiator: msg.sender,
|
||||
signatures: new bytes[](0),
|
||||
executed: false,
|
||||
quorumRequired: (totalSupply() * quorumPercentage) / 100,
|
||||
signaturesCount: 0,
|
||||
description: _description
|
||||
});
|
||||
|
||||
emit TokenDistributionProposalCreated(proposalId, msg.sender, _partners, _amounts, _description);
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Подписать предложение на распределение токенов
|
||||
* @param _proposalId ID предложения
|
||||
*/
|
||||
function signTokenDistributionProposal(uint256 _proposalId) external {
|
||||
TokenDistributionProposal storage proposal = tokenDistributionProposals[_proposalId];
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
require(block.timestamp < proposal.timelock, "Proposal expired");
|
||||
require(balanceOf(msg.sender) > 0, "No tokens to sign");
|
||||
|
||||
// Проверяем, что пользователь еще не подписал
|
||||
for (uint256 i = 0; i < proposal.signatures.length; i++) {
|
||||
require(
|
||||
proposal.signatures[i].length == 0 ||
|
||||
abi.decode(proposal.signatures[i], (address)) != msg.sender,
|
||||
"Already signed"
|
||||
);
|
||||
}
|
||||
|
||||
proposal.signatures.push(abi.encodePacked(msg.sender));
|
||||
proposal.signaturesCount++;
|
||||
|
||||
emit TokenDistributionProposalSigned(_proposalId, msg.sender, proposal.signaturesCount);
|
||||
|
||||
// Проверяем, достигнут ли кворум
|
||||
if (proposal.signaturesCount >= proposal.quorumRequired) {
|
||||
proposal.executed = true;
|
||||
_executeTokenDistribution(_proposalId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Выполнить распределение токенов после достижения кворума
|
||||
* @param _proposalId ID предложения
|
||||
*/
|
||||
function _executeTokenDistribution(uint256 _proposalId) internal {
|
||||
TokenDistributionProposal storage proposal = tokenDistributionProposals[_proposalId];
|
||||
require(proposal.executed, "Proposal not executed");
|
||||
require(proposal.signaturesCount >= proposal.quorumRequired, "Insufficient quorum");
|
||||
|
||||
for (uint256 i = 0; i < proposal.partners.length; i++) {
|
||||
require(proposal.partners[i] != address(0), "Zero address");
|
||||
require(proposal.amounts[i] > 0, "Zero amount");
|
||||
_mint(proposal.partners[i], proposal.amounts[i]);
|
||||
}
|
||||
|
||||
emit TokenDistributionProposalExecuted(_proposalId, proposal.partners, proposal.amounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Выполнить предложение на распределение токенов после истечения таймлока
|
||||
* @param _proposalId ID предложения
|
||||
*/
|
||||
function executeTokenDistributionProposal(uint256 _proposalId) external {
|
||||
TokenDistributionProposal storage proposal = tokenDistributionProposals[_proposalId];
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
require(block.timestamp >= proposal.timelock, "Timelock not expired");
|
||||
require(proposal.signaturesCount >= proposal.quorumRequired, "Insufficient quorum");
|
||||
|
||||
proposal.executed = true;
|
||||
_executeTokenDistribution(_proposalId);
|
||||
}
|
||||
|
||||
function createProposal(
|
||||
string memory _description,
|
||||
uint256 _duration,
|
||||
bytes memory _operation,
|
||||
uint256[] memory _targetChains,
|
||||
uint256 _timelock,
|
||||
uint256 _governanceChain
|
||||
uint256 _governanceChainId
|
||||
) external returns (uint256) {
|
||||
require(_operation.length > 0, "Empty operation");
|
||||
require(_targetChains.length > 0, "Empty target chains");
|
||||
require(_timelock > block.timestamp, "Invalid timelock");
|
||||
uint256 proposalId = proposalCounter++;
|
||||
proposals[proposalId] = Proposal({
|
||||
operation: _operation,
|
||||
targetChains: _targetChains,
|
||||
timelock: _timelock,
|
||||
governanceChain: _governanceChain,
|
||||
initiator: msg.sender,
|
||||
signatures: new bytes[](0),
|
||||
executed: false,
|
||||
quorumRequired: (totalSupply() * quorumPercentage) / 100,
|
||||
signaturesCount: 0
|
||||
});
|
||||
emit ProposalCreated(proposalId, msg.sender, _operation);
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
function signProposal(uint256 _proposalId) external {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
require(block.timestamp < proposal.timelock, "Proposal expired");
|
||||
require(balanceOf(msg.sender) > 0, "No tokens to sign");
|
||||
proposal.signatures.push(abi.encodePacked(msg.sender));
|
||||
proposal.signaturesCount++;
|
||||
emit ProposalSigned(_proposalId, msg.sender, proposal.signaturesCount);
|
||||
if (proposal.signaturesCount >= proposal.quorumRequired) {
|
||||
proposal.executed = true;
|
||||
emit IGovernor.ProposalExecuted(_proposalId);
|
||||
}
|
||||
}
|
||||
|
||||
function installModule(string memory _moduleName, address _moduleAddress) external {
|
||||
emit ModuleInstalled(_moduleName, _moduleAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Внести токены в казну DLE
|
||||
* @param _amount Количество токенов для внесения
|
||||
*/
|
||||
function depositToTreasury(uint256 _amount) external {
|
||||
require(_amount > 0, "Amount must be greater than 0");
|
||||
require(balanceOf(msg.sender) >= _amount, "Insufficient balance");
|
||||
|
||||
_transfer(msg.sender, address(this), _amount);
|
||||
totalTreasuryBalance += _amount;
|
||||
|
||||
emit TokensDepositedToTreasury(msg.sender, _amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Создать предложение на вывод средств из казны
|
||||
* @param _recipient Адрес получателя
|
||||
* @param _amount Количество токенов для вывода
|
||||
* @param _timelock Время исполнения (timestamp)
|
||||
* @param _description Описание предложения
|
||||
*/
|
||||
function createTreasuryProposal(
|
||||
address _recipient,
|
||||
uint256 _amount,
|
||||
uint256 _timelock,
|
||||
string memory _description
|
||||
) external returns (uint256) {
|
||||
require(_recipient != address(0), "Zero address");
|
||||
require(_amount > 0, "Amount must be greater than 0");
|
||||
require(_amount <= totalTreasuryBalance, "Insufficient treasury balance");
|
||||
require(_timelock > block.timestamp, "Invalid timelock");
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
|
||||
require(_duration > 0, "Duration must be positive");
|
||||
require(supportedChains[_governanceChainId], "Chain not supported");
|
||||
require(checkChainConnection(_governanceChainId), "Chain not available");
|
||||
|
||||
uint256 proposalId = treasuryProposalCounter++;
|
||||
uint256 proposalId = proposalCounter++;
|
||||
Proposal storage proposal = proposals[proposalId];
|
||||
|
||||
treasuryProposals[proposalId] = TreasuryProposal({
|
||||
recipient: _recipient,
|
||||
amount: _amount,
|
||||
timelock: _timelock,
|
||||
initiator: msg.sender,
|
||||
signatures: new bytes[](0),
|
||||
executed: false,
|
||||
quorumRequired: (totalSupply() * quorumPercentage) / 100,
|
||||
signaturesCount: 0,
|
||||
description: _description
|
||||
});
|
||||
proposal.id = proposalId;
|
||||
proposal.description = _description;
|
||||
proposal.forVotes = 0;
|
||||
proposal.againstVotes = 0;
|
||||
proposal.executed = false;
|
||||
proposal.deadline = block.timestamp + _duration;
|
||||
proposal.initiator = msg.sender;
|
||||
proposal.operation = _operation;
|
||||
|
||||
emit TreasuryProposalCreated(proposalId, msg.sender, _recipient, _amount, _description);
|
||||
emit ProposalCreated(proposalId, msg.sender, _description);
|
||||
return proposalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Подписать предложение на вывод средств из казны
|
||||
* @dev Голосовать за предложение
|
||||
* @param _proposalId ID предложения
|
||||
* @param _support Поддержка предложения
|
||||
*/
|
||||
function signTreasuryProposal(uint256 _proposalId) external {
|
||||
TreasuryProposal storage proposal = treasuryProposals[_proposalId];
|
||||
function vote(uint256 _proposalId, bool _support) external nonReentrant {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
require(block.timestamp < proposal.deadline, "Voting ended");
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
require(block.timestamp < proposal.timelock, "Proposal expired");
|
||||
require(balanceOf(msg.sender) > 0, "No tokens to sign");
|
||||
require(!proposal.hasVoted[msg.sender], "Already voted");
|
||||
require(balanceOf(msg.sender) > 0, "No tokens to vote");
|
||||
|
||||
// Проверяем, что пользователь еще не подписал
|
||||
for (uint256 i = 0; i < proposal.signatures.length; i++) {
|
||||
require(
|
||||
proposal.signatures[i].length == 0 ||
|
||||
abi.decode(proposal.signatures[i], (address)) != msg.sender,
|
||||
"Already signed"
|
||||
);
|
||||
uint256 votingPower = balanceOf(msg.sender);
|
||||
proposal.hasVoted[msg.sender] = true;
|
||||
|
||||
if (_support) {
|
||||
proposal.forVotes += votingPower;
|
||||
} else {
|
||||
proposal.againstVotes += votingPower;
|
||||
}
|
||||
|
||||
proposal.signatures.push(abi.encodePacked(msg.sender));
|
||||
proposal.signaturesCount++;
|
||||
|
||||
emit TreasuryProposalSigned(_proposalId, msg.sender, proposal.signaturesCount);
|
||||
|
||||
// Проверяем, достигнут ли кворум
|
||||
if (proposal.signaturesCount >= proposal.quorumRequired) {
|
||||
proposal.executed = true;
|
||||
_executeTreasuryProposal(_proposalId);
|
||||
}
|
||||
emit ProposalVoted(_proposalId, msg.sender, _support, votingPower);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Выполнить предложение на вывод средств из казны
|
||||
* @dev Синхронизировать голос из другой цепочки
|
||||
* @param _proposalId ID предложения
|
||||
* @param _fromChainId ID цепочки откуда синхронизируем
|
||||
* @param _forVotes Голоса за
|
||||
* @param _againstVotes Голоса против
|
||||
*/
|
||||
function _executeTreasuryProposal(uint256 _proposalId) internal {
|
||||
TreasuryProposal storage proposal = treasuryProposals[_proposalId];
|
||||
require(proposal.executed, "Proposal not executed");
|
||||
require(proposal.signaturesCount >= proposal.quorumRequired, "Insufficient quorum");
|
||||
require(proposal.amount <= totalTreasuryBalance, "Insufficient treasury balance");
|
||||
function syncVoteFromChain(
|
||||
uint256 _proposalId,
|
||||
uint256 _fromChainId,
|
||||
uint256 _forVotes,
|
||||
uint256 _againstVotes,
|
||||
bytes memory /* _proof */
|
||||
) external {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
require(supportedChains[_fromChainId], "Chain not supported");
|
||||
require(!proposal.chainVoteSynced[_fromChainId], "Already synced");
|
||||
|
||||
totalTreasuryBalance -= proposal.amount;
|
||||
_transfer(address(this), proposal.recipient, proposal.amount);
|
||||
// Здесь должна быть проверка proof (для простоты пропускаем)
|
||||
// В реальной реализации нужно проверять доказательство
|
||||
|
||||
emit TreasuryProposalExecuted(_proposalId, proposal.recipient, proposal.amount);
|
||||
proposal.forVotes += _forVotes;
|
||||
proposal.againstVotes += _againstVotes;
|
||||
proposal.chainVoteSynced[_fromChainId] = true;
|
||||
|
||||
emit CrossChainVoteSync(_proposalId, _fromChainId, currentChainId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Выполнить предложение на вывод средств после истечения таймлока
|
||||
* @dev Проверить результат предложения
|
||||
* @param _proposalId ID предложения
|
||||
* @return passed Прошло ли предложение
|
||||
* @return quorumReached Достигнут ли кворум
|
||||
*/
|
||||
function checkProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
|
||||
uint256 totalVotes = proposal.forVotes + proposal.againstVotes;
|
||||
uint256 quorumRequired = (totalSupply() * quorumPercentage) / 100;
|
||||
|
||||
quorumReached = totalVotes >= quorumRequired;
|
||||
passed = quorumReached && proposal.forVotes > proposal.againstVotes;
|
||||
|
||||
return (passed, quorumReached);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Исполнить предложение
|
||||
* @param _proposalId ID предложения
|
||||
*/
|
||||
function executeTreasuryProposal(uint256 _proposalId) external {
|
||||
TreasuryProposal storage proposal = treasuryProposals[_proposalId];
|
||||
function executeProposal(uint256 _proposalId) external {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
require(block.timestamp >= proposal.timelock, "Timelock not expired");
|
||||
require(proposal.signaturesCount >= proposal.quorumRequired, "Insufficient quorum");
|
||||
require(block.timestamp >= proposal.deadline, "Voting not ended");
|
||||
|
||||
(bool passed, bool quorumReached) = checkProposalResult(_proposalId);
|
||||
require(passed && quorumReached, "Proposal not passed");
|
||||
|
||||
proposal.executed = true;
|
||||
_executeTreasuryProposal(_proposalId);
|
||||
|
||||
// Исполняем операцию
|
||||
_executeOperation(proposal.operation);
|
||||
|
||||
emit ProposalExecuted(_proposalId, proposal.operation);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @dev Получить доступную для вывода сумму для адреса (пропорционально доле)
|
||||
* @param _address Адрес для проверки
|
||||
* @return Доступная сумма для вывода
|
||||
* @dev Создать мультиподпись операцию
|
||||
* @param _operationHash Хеш операции
|
||||
* @param _duration Длительность сбора подписей
|
||||
*/
|
||||
function getAvailableWithdrawal(address _address) public view returns (uint256) {
|
||||
uint256 userBalance = balanceOf(_address);
|
||||
if (userBalance == 0 || totalTreasuryBalance == 0) {
|
||||
return 0;
|
||||
function createMultiSigOperation(
|
||||
bytes32 _operationHash,
|
||||
uint256 _duration
|
||||
) external returns (uint256) {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create operation");
|
||||
require(_duration > 0, "Duration must be positive");
|
||||
|
||||
uint256 operationId = multiSigCounter++;
|
||||
MultiSigOperation storage operation = multiSigOperations[operationId];
|
||||
|
||||
operation.operationHash = _operationHash;
|
||||
operation.forSignatures = 0;
|
||||
operation.againstSignatures = 0;
|
||||
operation.executed = false;
|
||||
operation.deadline = block.timestamp + _duration;
|
||||
operation.initiator = msg.sender;
|
||||
|
||||
emit MultiSigOperationCreated(operationId, msg.sender, _operationHash);
|
||||
return operationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Подписать мультиподпись операцию
|
||||
* @param _operationId ID операции
|
||||
* @param _support Поддержка операции
|
||||
*/
|
||||
function signMultiSigOperation(uint256 _operationId, bool _support) external nonReentrant {
|
||||
MultiSigOperation storage operation = multiSigOperations[_operationId];
|
||||
require(operation.operationHash != bytes32(0), "Operation does not exist");
|
||||
require(block.timestamp < operation.deadline, "Signing ended");
|
||||
require(!operation.executed, "Operation already executed");
|
||||
require(!operation.hasSigned[msg.sender], "Already signed");
|
||||
require(balanceOf(msg.sender) > 0, "No tokens to sign");
|
||||
|
||||
uint256 signaturePower = balanceOf(msg.sender);
|
||||
operation.hasSigned[msg.sender] = true;
|
||||
|
||||
if (_support) {
|
||||
operation.forSignatures += signaturePower;
|
||||
} else {
|
||||
operation.againstSignatures += signaturePower;
|
||||
}
|
||||
|
||||
emit MultiSigSigned(_operationId, msg.sender, _support, signaturePower);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Синхронизировать мультиподпись из другой цепочки
|
||||
* @param _operationId ID операции
|
||||
* @param _fromChainId ID цепочки откуда синхронизируем
|
||||
* @param _forSignatures Подписи за
|
||||
* @param _againstSignatures Подписи против
|
||||
*/
|
||||
function syncMultiSigFromChain(
|
||||
uint256 _operationId,
|
||||
uint256 _fromChainId,
|
||||
uint256 _forSignatures,
|
||||
uint256 _againstSignatures,
|
||||
bytes memory /* _proof */
|
||||
) external {
|
||||
MultiSigOperation storage operation = multiSigOperations[_operationId];
|
||||
require(operation.operationHash != bytes32(0), "Operation does not exist");
|
||||
require(supportedChains[_fromChainId], "Chain not supported");
|
||||
require(!operation.chainSignSynced[_fromChainId], "Already synced");
|
||||
|
||||
// Здесь должна быть проверка proof
|
||||
// В реальной реализации нужно проверять доказательство
|
||||
|
||||
operation.forSignatures += _forSignatures;
|
||||
operation.againstSignatures += _againstSignatures;
|
||||
operation.chainSignSynced[_fromChainId] = true;
|
||||
|
||||
emit CrossChainMultiSigSync(_operationId, _fromChainId, currentChainId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Проверить результат мультиподписи
|
||||
* @param _operationId ID операции
|
||||
* @return passed Прошла ли операция
|
||||
* @return quorumReached Достигнут ли кворум
|
||||
*/
|
||||
function checkMultiSigResult(uint256 _operationId) public view returns (bool passed, bool quorumReached) {
|
||||
MultiSigOperation storage operation = multiSigOperations[_operationId];
|
||||
require(operation.operationHash != bytes32(0), "Operation does not exist");
|
||||
|
||||
uint256 totalSignatures = operation.forSignatures + operation.againstSignatures;
|
||||
uint256 quorumRequired = (totalSupply() * quorumPercentage) / 100;
|
||||
|
||||
quorumReached = totalSignatures >= quorumRequired;
|
||||
passed = quorumReached && operation.forSignatures > operation.againstSignatures;
|
||||
|
||||
return (passed, quorumReached);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Исполнить мультиподпись операцию
|
||||
* @param _operationId ID операции
|
||||
*/
|
||||
function executeMultiSigOperation(uint256 _operationId) external {
|
||||
MultiSigOperation storage operation = multiSigOperations[_operationId];
|
||||
require(operation.operationHash != bytes32(0), "Operation does not exist");
|
||||
require(!operation.executed, "Operation already executed");
|
||||
require(block.timestamp >= operation.deadline, "Signing not ended");
|
||||
|
||||
(bool passed, bool quorumReached) = checkMultiSigResult(_operationId);
|
||||
require(passed && quorumReached, "Operation not passed");
|
||||
|
||||
operation.executed = true;
|
||||
|
||||
emit MultiSigExecuted(_operationId, operation.operationHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Синхронизировать исполнение из другой цепочки
|
||||
* @param _proposalId ID предложения
|
||||
* @param _fromChainId ID цепочки откуда синхронизируем
|
||||
*/
|
||||
function syncExecutionFromChain(
|
||||
uint256 _proposalId,
|
||||
uint256 _fromChainId,
|
||||
bytes memory /* _proof */
|
||||
) external {
|
||||
require(supportedChains[_fromChainId], "Chain not supported");
|
||||
require(!executedProposals[_proposalId], "Already executed");
|
||||
|
||||
// Здесь должна быть проверка proof
|
||||
// В реальной реализации нужно проверять доказательство
|
||||
|
||||
executedProposals[_proposalId] = true;
|
||||
|
||||
// Получаем операцию из предложения
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
if (proposal.id == _proposalId) {
|
||||
_executeOperation(proposal.operation);
|
||||
}
|
||||
|
||||
emit CrossChainExecutionSync(_proposalId, _fromChainId, currentChainId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Проверить подключение к цепочке
|
||||
* @param _chainId ID цепочки
|
||||
* @return isAvailable Доступна ли цепочка
|
||||
*/
|
||||
function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable) {
|
||||
// В реальной реализации здесь должна быть проверка подключения
|
||||
// Для примера возвращаем true для поддерживаемых цепочек
|
||||
return supportedChains[_chainId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Проверить все подключения перед синхронизацией
|
||||
* @param _proposalId ID предложения
|
||||
* @return allChainsReady Готовы ли все цепочки
|
||||
*/
|
||||
function checkSyncReadiness(uint256 _proposalId) public view returns (bool allChainsReady) {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
|
||||
// Проверяем все поддерживаемые цепочки
|
||||
for (uint256 i = 0; i < getSupportedChainCount(); i++) {
|
||||
uint256 chainId = getSupportedChainId(i);
|
||||
if (!checkChainConnection(chainId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Пропорционально доле в общем количестве токенов
|
||||
uint256 userShare = (userBalance * totalTreasuryBalance) / totalSupply();
|
||||
return userShare;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Синхронизация только при 100% готовности
|
||||
* @param _proposalId ID предложения
|
||||
*/
|
||||
function syncToAllChains(uint256 _proposalId) external {
|
||||
require(checkSyncReadiness(_proposalId), "Not all chains ready");
|
||||
|
||||
// Выполняем синхронизацию во все цепочки
|
||||
for (uint256 i = 0; i < getSupportedChainCount(); i++) {
|
||||
uint256 chainId = getSupportedChainId(i);
|
||||
syncToChain(_proposalId, chainId);
|
||||
}
|
||||
|
||||
emit SyncCompleted(_proposalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Синхронизация в конкретную цепочку
|
||||
* @param _proposalId ID предложения
|
||||
* @param _chainId ID цепочки
|
||||
*/
|
||||
function syncToChain(uint256 _proposalId, uint256 _chainId) internal {
|
||||
// В реальной реализации здесь будет вызов cross-chain bridge
|
||||
// Для примера просто эмитим событие
|
||||
emit CrossChainExecutionSync(_proposalId, currentChainId, _chainId);
|
||||
}
|
||||
|
||||
// Переопределения для совместимости с ERC-6372
|
||||
function CLOCK_MODE() public pure override(Governor, GovernorVotes, Votes) returns (string memory) {
|
||||
return "mode=blocknumber&from=default";
|
||||
/**
|
||||
* @dev Получить количество поддерживаемых цепочек
|
||||
*/
|
||||
function getSupportedChainCount() public pure returns (uint256) {
|
||||
// В реальной реализации нужно хранить массив поддерживаемых цепочек
|
||||
// Для примера возвращаем 4 (Ethereum, Polygon, BSC, Arbitrum)
|
||||
return 4;
|
||||
}
|
||||
function clock() public view override(Governor, GovernorVotes, Votes) returns (uint48) {
|
||||
return uint48(block.number);
|
||||
|
||||
/**
|
||||
* @dev Получить ID поддерживаемой цепочки по индексу
|
||||
* @param _index Индекс цепочки
|
||||
*/
|
||||
function getSupportedChainId(uint256 _index) public pure returns (uint256) {
|
||||
if (_index == 0) return 1; // Ethereum
|
||||
if (_index == 1) return 137; // Polygon
|
||||
if (_index == 2) return 56; // BSC
|
||||
if (_index == 3) return 42161; // Arbitrum
|
||||
revert("Invalid chain index");
|
||||
}
|
||||
function _update(address from, address to, uint256 amount) internal override(ERC20Votes) {
|
||||
super._update(from, to, amount);
|
||||
|
||||
/**
|
||||
* @dev Исполнить операцию
|
||||
* @param _operation Операция для исполнения
|
||||
*/
|
||||
function _executeOperation(bytes memory _operation) internal {
|
||||
// Декодируем операцию
|
||||
(bytes4 selector, bytes memory data) = abi.decode(_operation, (bytes4, bytes));
|
||||
|
||||
if (selector == bytes4(keccak256("transfer(address,uint256)"))) {
|
||||
// Операция передачи токенов
|
||||
(address to, uint256 amount) = abi.decode(data, (address, uint256));
|
||||
_transfer(msg.sender, to, amount);
|
||||
} else if (selector == bytes4(keccak256("mint(address,uint256)"))) {
|
||||
// Операция минтинга токенов
|
||||
(address to, uint256 amount) = abi.decode(data, (address, uint256));
|
||||
_mint(to, amount);
|
||||
} else if (selector == bytes4(keccak256("burn(address,uint256)"))) {
|
||||
// Операция сжигания токенов
|
||||
(address from, uint256 amount) = abi.decode(data, (address, uint256));
|
||||
_burn(from, amount);
|
||||
} else {
|
||||
// Неизвестная операция
|
||||
revert("Unknown operation");
|
||||
}
|
||||
}
|
||||
function nonces(address owner) public view override(Nonces) returns (uint256) {
|
||||
return super.nonces(owner);
|
||||
|
||||
/**
|
||||
* @dev Добавить модуль
|
||||
* @param _moduleId ID модуля
|
||||
* @param _moduleAddress Адрес модуля
|
||||
*/
|
||||
function addModule(bytes32 _moduleId, address _moduleAddress) external {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to add module");
|
||||
require(_moduleAddress != address(0), "Zero address");
|
||||
require(!activeModules[_moduleId], "Module already exists");
|
||||
|
||||
modules[_moduleId] = _moduleAddress;
|
||||
activeModules[_moduleId] = true;
|
||||
|
||||
emit ModuleAdded(_moduleId, _moduleAddress);
|
||||
}
|
||||
function name() public view override(ERC20, Governor) returns (string memory) {
|
||||
return super.name();
|
||||
|
||||
/**
|
||||
* @dev Удалить модуль
|
||||
* @param _moduleId ID модуля
|
||||
*/
|
||||
function removeModule(bytes32 _moduleId) external {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to remove module");
|
||||
require(activeModules[_moduleId], "Module does not exist");
|
||||
|
||||
delete modules[_moduleId];
|
||||
activeModules[_moduleId] = false;
|
||||
|
||||
emit ModuleRemoved(_moduleId);
|
||||
}
|
||||
function votingDelay() public view override(Governor, GovernorSettings) returns (uint256) {
|
||||
return super.votingDelay();
|
||||
|
||||
/**
|
||||
* @dev Получить информацию о DLE
|
||||
*/
|
||||
function getDLEInfo() external view returns (DLEInfo memory) {
|
||||
return dleInfo;
|
||||
}
|
||||
function votingPeriod() public view override(Governor, GovernorSettings) returns (uint256) {
|
||||
return super.votingPeriod();
|
||||
|
||||
/**
|
||||
* @dev Проверить, активен ли модуль
|
||||
* @param _moduleId ID модуля
|
||||
*/
|
||||
function isModuleActive(bytes32 _moduleId) external view returns (bool) {
|
||||
return activeModules[_moduleId];
|
||||
}
|
||||
function quorum(uint256 blockNumber) public view override(Governor, GovernorVotesQuorumFraction) returns (uint256) {
|
||||
return super.quorum(blockNumber);
|
||||
|
||||
/**
|
||||
* @dev Получить адрес модуля
|
||||
* @param _moduleId ID модуля
|
||||
*/
|
||||
function getModuleAddress(bytes32 _moduleId) external view returns (address) {
|
||||
return modules[_moduleId];
|
||||
}
|
||||
function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) {
|
||||
return super.state(proposalId);
|
||||
|
||||
/**
|
||||
* @dev Проверить, поддерживается ли цепочка
|
||||
* @param _chainId ID цепочки
|
||||
*/
|
||||
function isChainSupported(uint256 _chainId) external view returns (bool) {
|
||||
return supportedChains[_chainId];
|
||||
}
|
||||
function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
|
||||
return super.proposalThreshold();
|
||||
}
|
||||
function _cancel(
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
|
||||
return super._cancel(targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
|
||||
return super._executor();
|
||||
}
|
||||
function supportsInterface(bytes4 interfaceId) public view override(Governor) returns (bool) {
|
||||
return super.supportsInterface(interfaceId);
|
||||
}
|
||||
function proposalNeedsQueuing(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (bool) {
|
||||
return super.proposalNeedsQueuing(proposalId);
|
||||
}
|
||||
function _queueOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockControl) returns (uint48) {
|
||||
return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
}
|
||||
function _executeOperations(
|
||||
uint256 proposalId,
|
||||
address[] memory targets,
|
||||
uint256[] memory values,
|
||||
bytes[] memory calldatas,
|
||||
bytes32 descriptionHash
|
||||
) internal override(Governor, GovernorTimelockControl) {
|
||||
super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
|
||||
|
||||
/**
|
||||
* @dev Получить текущий ID цепочки
|
||||
*/
|
||||
function getCurrentChainId() external view returns (uint256) {
|
||||
return currentChainId;
|
||||
}
|
||||
|
||||
// События для новых функций
|
||||
event SyncCompleted(uint256 proposalId);
|
||||
}
|
||||
@@ -1,3 +1,15 @@
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
|
||||
# Анализ кодов по юрисдикциям для DLE
|
||||
|
||||
## 🌍 ГЛОБАЛЬНЫЕ (МЕЖДУНАРОДНЫЕ) КОДЫ
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const dleService = require('../services/dleService');
|
||||
const logger = require('../utils/logger');
|
||||
const auth = require('../middleware/auth');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* @route POST /api/dle
|
||||
* @desc Создать новое DLE (Digital Legal Entity)
|
||||
* @access Private (только для авторизованных пользователей с ролью admin)
|
||||
*/
|
||||
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const dleParams = req.body;
|
||||
logger.info('Получен запрос на создание DLE:', dleParams);
|
||||
|
||||
// Если параметр partners не был передан явно, используем адрес авторизованного пользователя
|
||||
if (!dleParams.partners || dleParams.partners.length === 0) {
|
||||
// Проверяем, есть ли в сессии адрес кошелька пользователя
|
||||
if (!req.user || !req.user.walletAddress) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Не указан адрес кошелька пользователя или партнеров для распределения токенов'
|
||||
});
|
||||
}
|
||||
|
||||
// Используем адрес авторизованного пользователя
|
||||
dleParams.partners = [req.user.address || req.user.walletAddress];
|
||||
|
||||
// Если суммы не указаны, используем значение по умолчанию (100% токенов)
|
||||
if (!dleParams.amounts || dleParams.amounts.length === 0) {
|
||||
dleParams.amounts = ['1000000'];
|
||||
}
|
||||
}
|
||||
|
||||
const result = await dleService.createDLE(dleParams);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'DLE успешно создано',
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при создании DLE:', error);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/dle
|
||||
* @desc Получить список всех DLE
|
||||
* @access Public (доступно всем пользователям, включая гостевых)
|
||||
*/
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const dles = await dleService.getAllDLEs();
|
||||
res.json({
|
||||
success: true,
|
||||
data: dles
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при получении списка DLE:', error);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/dle/settings
|
||||
* @desc Получить настройки для деплоя DLE по умолчанию
|
||||
* @access Private (только для авторизованных пользователей)
|
||||
*/
|
||||
router.get('/settings', auth.requireAuth, (req, res) => {
|
||||
// Возвращаем настройки по умолчанию, которые будут использоваться
|
||||
// при заполнении формы на фронтенде
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
votingDelay: 1, // 1 блок задержки перед началом голосования
|
||||
votingPeriod: 45818, // ~1 неделя в блоках (при 13 секундах на блок)
|
||||
proposalThreshold: '100000', // 100,000 токенов
|
||||
quorumPercentage: 4, // 4% от общего количества токенов
|
||||
minTimelockDelay: 2 // 2 дня
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/dle/:tokenAddress
|
||||
* @desc Удалить DLE по адресу токена
|
||||
* @access Private (только для авторизованных пользователей с ролью admin)
|
||||
*/
|
||||
router.delete('/:tokenAddress', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { tokenAddress } = req.params;
|
||||
logger.info(`Получен запрос на удаление DLE с адресом токена: ${tokenAddress}`);
|
||||
|
||||
// Проверяем существование DLE в директории contracts-data/dles
|
||||
const dlesDir = path.join(__dirname, '../contracts-data/dles');
|
||||
const files = fs.readdirSync(dlesDir);
|
||||
|
||||
let fileToDelete = null;
|
||||
|
||||
// Находим файл, содержащий указанный адрес токена
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dlesDir, file);
|
||||
if (fs.statSync(filePath).isFile() && file.endsWith('.json')) {
|
||||
try {
|
||||
const dleData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
if (dleData.tokenAddress && dleData.tokenAddress.toLowerCase() === tokenAddress.toLowerCase()) {
|
||||
fileToDelete = filePath;
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Ошибка при чтении файла ${file}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileToDelete) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `DLE с адресом токена ${tokenAddress} не найдено`
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем файл
|
||||
fs.unlinkSync(fileToDelete);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `DLE с адресом токена ${tokenAddress} успешно удалено`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при удалении DLE:', error);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/dle/empty/:fileName
|
||||
* @desc Удалить пустое DLE по имени файла
|
||||
* @access Private (только для авторизованных пользователей с ролью admin)
|
||||
*/
|
||||
router.delete('/empty/:fileName', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const { fileName } = req.params;
|
||||
logger.info(`Получен запрос на удаление пустого DLE с именем файла: ${fileName}`);
|
||||
|
||||
// Проверяем существование файла в директории contracts-data/dles
|
||||
const dlesDir = path.join(__dirname, '../contracts-data/dles');
|
||||
const filePath = path.join(dlesDir, fileName);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `Файл ${fileName} не найден`
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем файл
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Пустое DLE с именем файла ${fileName} успешно удалено`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при удалении пустого DLE:', error);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,216 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
// Скрипт для ручного создания DLE без использования фабрики
|
||||
const { ethers } = require("hardhat");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
async function main() {
|
||||
// Получаем параметры деплоя из файла или аргументов
|
||||
const deployParams = getDeployParams();
|
||||
|
||||
console.log("Начинаем создание нового DLE вручную (без фабрики)...");
|
||||
console.log("Параметры DLE:");
|
||||
console.log(JSON.stringify(deployParams, null, 2));
|
||||
|
||||
// Получаем аккаунт деплоя
|
||||
const [deployer] = await ethers.getSigners();
|
||||
console.log(`Адрес деплоера: ${deployer.address}`);
|
||||
console.log(`Баланс деплоера: ${ethers.formatEther(await deployer.provider.getBalance(deployer.address))} ETH`);
|
||||
|
||||
try {
|
||||
// 1. Создаем токен управления
|
||||
console.log("\n1. Деплой токена управления...");
|
||||
const GovernanceToken = await ethers.getContractFactory("GovernanceToken");
|
||||
const token = await GovernanceToken.deploy(
|
||||
deployParams.name,
|
||||
deployParams.symbol,
|
||||
deployer.address // адрес управляющего минтом
|
||||
);
|
||||
await token.waitForDeployment();
|
||||
const tokenAddress = await token.getAddress();
|
||||
console.log(`Токен задеплоен по адресу: ${tokenAddress}`);
|
||||
|
||||
// 2. Создаем Timelock контракт
|
||||
console.log("\n2. Деплой Timelock контракта...");
|
||||
|
||||
// Создаем временные пустые массивы
|
||||
const proposers = [deployer.address]; // Временно даем права деплоеру
|
||||
const executors = [ethers.ZeroAddress]; // Адрес 0 означает, что любой может выполнять
|
||||
|
||||
// Минимальная задержка в секундах
|
||||
const minDelayInSeconds = deployParams.minTimelockDelay * 24 * 60 * 60; // Перевод дней в секунды
|
||||
|
||||
const GovernanceTimelock = await ethers.getContractFactory("GovernanceTimelock");
|
||||
const timelock = await GovernanceTimelock.deploy(
|
||||
minDelayInSeconds,
|
||||
proposers,
|
||||
executors,
|
||||
deployer.address // admin
|
||||
);
|
||||
await timelock.waitForDeployment();
|
||||
const timelockAddress = await timelock.getAddress();
|
||||
console.log(`Timelock задеплоен по адресу: ${timelockAddress}`);
|
||||
|
||||
// 3. Создаем Governor контракт
|
||||
console.log("\n3. Деплой Governor контракта...");
|
||||
const GovernorContract = await ethers.getContractFactory("GovernorContract");
|
||||
const governor = await GovernorContract.deploy(
|
||||
`${deployParams.name} Governor`,
|
||||
token,
|
||||
timelock,
|
||||
deployParams.votingDelay,
|
||||
deployParams.votingPeriod,
|
||||
deployParams.proposalThreshold,
|
||||
deployParams.quorumPercentage
|
||||
);
|
||||
await governor.waitForDeployment();
|
||||
const governorAddress = await governor.getAddress();
|
||||
console.log(`Governor задеплоен по адресу: ${governorAddress}`);
|
||||
|
||||
// 4. Настраиваем разрешения Timelock
|
||||
console.log("\n4. Настройка разрешений Timelock...");
|
||||
|
||||
// Отменяем временные предложения
|
||||
const tx1 = await timelock.revokeRole(await timelock.PROPOSER_ROLE(), deployer.address);
|
||||
await tx1.wait();
|
||||
console.log(`Отозвана роль PROPOSER у деплоера`);
|
||||
|
||||
// Устанавливаем Governor как единственного proposer
|
||||
const tx2 = await timelock.grantRole(await timelock.PROPOSER_ROLE(), governorAddress);
|
||||
await tx2.wait();
|
||||
console.log(`Назначена роль PROPOSER контракту Governor`);
|
||||
|
||||
// Отказываемся от роли администратора для Timelock
|
||||
const adminRole = await timelock.DEFAULT_ADMIN_ROLE();
|
||||
const tx3 = await timelock.revokeRole(adminRole, deployer.address);
|
||||
await tx3.wait();
|
||||
console.log(`Отозвана роль ADMIN у деплоера`);
|
||||
|
||||
// 5. Распределяем начальные токены
|
||||
console.log("\n5. Распределение начальных токенов...");
|
||||
const mintTx = await token.mintInitialSupply(deployParams.partners, deployParams.amounts);
|
||||
await mintTx.wait();
|
||||
console.log(`Токены распределены между партнерами`);
|
||||
|
||||
// 6. Сохраняем информацию о созданных контрактах
|
||||
console.log("\n6. Сохранение информации о DLE...");
|
||||
const dleData = {
|
||||
name: deployParams.name,
|
||||
symbol: deployParams.symbol,
|
||||
location: deployParams.location,
|
||||
isicCodes: deployParams.isicCodes,
|
||||
tokenAddress,
|
||||
timelockAddress,
|
||||
governorAddress,
|
||||
creationBlock: (await mintTx.provider.getBlockNumber()),
|
||||
creationTimestamp: (await mintTx.provider.getBlock()).timestamp,
|
||||
deployedManually: true
|
||||
};
|
||||
|
||||
const saveResult = saveDLEData(dleData);
|
||||
|
||||
console.log("\nDLE успешно создано вручную!");
|
||||
console.log(`Адрес токена: ${tokenAddress}`);
|
||||
console.log(`Адрес таймлока: ${timelockAddress}`);
|
||||
console.log(`Адрес контракта Governor: ${governorAddress}`);
|
||||
|
||||
if (!saveResult) {
|
||||
console.warn("\nВНИМАНИЕ: Не удалось сохранить информацию о DLE в файл!");
|
||||
console.warn("Убедитесь, что директория contracts-data/dles существует и доступна для записи.");
|
||||
console.warn("Данные контрактов доступны выше в логах.");
|
||||
}
|
||||
|
||||
return dleData;
|
||||
} catch (error) {
|
||||
console.error("Ошибка при создании DLE вручную:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем параметры деплоя из JSON-файла (переданного как аргумент)
|
||||
function getDeployParams() {
|
||||
const defaultParamsPath = path.join(__dirname, 'current-params.json');
|
||||
|
||||
if (fs.existsSync(defaultParamsPath)) {
|
||||
console.log(`Загрузка параметров из файла: ${defaultParamsPath}`);
|
||||
return JSON.parse(fs.readFileSync(defaultParamsPath, "utf8"));
|
||||
}
|
||||
|
||||
// Используем параметры по умолчанию, если файл не найден
|
||||
console.log("Используются параметры по умолчанию");
|
||||
return {
|
||||
name: "Manual DLE",
|
||||
symbol: "MDLE",
|
||||
location: "Test Location",
|
||||
isicCodes: ["A01", "B02"],
|
||||
partners: [
|
||||
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
||||
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
|
||||
],
|
||||
amounts: [
|
||||
ethers.parseEther("1000000"),
|
||||
ethers.parseEther("500000")
|
||||
],
|
||||
minTimelockDelay: 2,
|
||||
votingDelay: 1,
|
||||
votingPeriod: 45818,
|
||||
proposalThreshold: ethers.parseEther("100000"),
|
||||
quorumPercentage: 4
|
||||
};
|
||||
}
|
||||
|
||||
// Сохраняем информацию о созданном DLE
|
||||
function saveDLEData(dleData) {
|
||||
const dlesDir = path.join(__dirname, "../../contracts-data/dles");
|
||||
|
||||
// Проверяем существование директории и создаем при необходимости
|
||||
try {
|
||||
if (!fs.existsSync(dlesDir)) {
|
||||
console.log(`Директория ${dlesDir} не существует, создаю...`);
|
||||
fs.mkdirSync(dlesDir, { recursive: true });
|
||||
console.log(`Директория ${dlesDir} успешно создана`);
|
||||
}
|
||||
|
||||
// Проверяем права на запись, создавая временный файл
|
||||
const testFile = path.join(dlesDir, '.write-test');
|
||||
fs.writeFileSync(testFile, 'test');
|
||||
fs.unlinkSync(testFile);
|
||||
console.log(`Директория ${dlesDir} доступна для записи`);
|
||||
|
||||
const fileName = `${dleData.name.replace(/\s+/g, '-')}-${Date.now()}.json`;
|
||||
const filePath = path.join(dlesDir, fileName);
|
||||
fs.writeFileSync(filePath, JSON.stringify(dleData, null, 2));
|
||||
|
||||
console.log(`Информация о DLE сохранена в файл: ${fileName}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Ошибка при сохранении информации о DLE: ${error.message}`);
|
||||
console.error(`Убедитесь, что директория ${dlesDir} существует и доступна для записи`);
|
||||
console.error(`Для исправления в Docker-контейнере выполните: docker exec -it [container_id] /bin/sh -c "mkdir -p /app/contracts-data/dles && chown -R node:node /app/contracts-data"`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Если скрипт выполняется напрямую
|
||||
if (require.main === module) {
|
||||
main()
|
||||
.then(() => process.exit(0))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
// Если скрипт импортируется как модуль
|
||||
module.exports = main;
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { ethers } = require('ethers');
|
||||
const logger = require('../utils/logger');
|
||||
const { getRpcUrlByNetworkId } = require('./rpcProviderService');
|
||||
|
||||
/**
|
||||
* Сервис для управления DLE (Digital Legal Entity)
|
||||
*/
|
||||
class DLEService {
|
||||
/**
|
||||
* Создает новое DLE с заданными параметрами
|
||||
* @param {Object} dleParams - Параметры DLE
|
||||
* @returns {Promise<Object>} - Результат создания DLE
|
||||
*/
|
||||
async createDLE(dleParams) {
|
||||
try {
|
||||
logger.info('Начало создания DLE с параметрами:', dleParams);
|
||||
|
||||
// Валидация входных данных
|
||||
this.validateDLEParams(dleParams);
|
||||
|
||||
// Подготовка параметров для деплоя
|
||||
const deployParams = this.prepareDeployParams(dleParams);
|
||||
|
||||
// Сохраняем параметры во временный файл
|
||||
const paramsFile = this.saveParamsToFile(deployParams);
|
||||
|
||||
// Копируем параметры во временный файл с предсказуемым именем
|
||||
const tempParamsFile = path.join(__dirname, '../scripts/deploy/current-params.json');
|
||||
logger.info(`Копирование параметров из ${paramsFile} в ${tempParamsFile}`);
|
||||
const deployDir = path.dirname(tempParamsFile);
|
||||
if (!fs.existsSync(deployDir)) {
|
||||
fs.mkdirSync(deployDir, { recursive: true });
|
||||
}
|
||||
fs.copyFileSync(paramsFile, tempParamsFile);
|
||||
logger.info(`Файл параметров скопирован успешно`);
|
||||
|
||||
// Получаем rpc_url из базы по выбранной сети
|
||||
const rpcUrl = await getRpcUrlByNetworkId(deployParams.network);
|
||||
if (!rpcUrl) {
|
||||
throw new Error(`RPC URL для сети ${deployParams.network} не найден в базе данных`);
|
||||
}
|
||||
if (!dleParams.privateKey) {
|
||||
throw new Error('Приватный ключ для деплоя не передан');
|
||||
}
|
||||
|
||||
// Запускаем скрипт деплоя с нужными переменными окружения
|
||||
const result = await this.runDeployScript(paramsFile, {
|
||||
rpcUrl,
|
||||
privateKey: dleParams.privateKey,
|
||||
networkId: deployParams.network,
|
||||
envNetworkKey: deployParams.network.toUpperCase()
|
||||
});
|
||||
|
||||
logger.info('DLE успешно создано:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при создании DLE:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидирует параметры DLE
|
||||
* @param {Object} params - Параметры DLE
|
||||
*/
|
||||
validateDLEParams(params) {
|
||||
const requiredFields = [
|
||||
'name', 'symbol', 'location', 'isicCodes',
|
||||
'partners', 'amounts', 'minTimelockDelay',
|
||||
'votingDelay', 'votingPeriod', 'proposalThreshold', 'quorumPercentage'
|
||||
];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (params[field] === undefined) {
|
||||
throw new Error(`Отсутствует обязательный параметр: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.partners.length !== params.amounts.length) {
|
||||
throw new Error('Количество партнеров должно соответствовать количеству сумм распределения');
|
||||
}
|
||||
|
||||
if (params.partners.length === 0) {
|
||||
throw new Error('Должен быть указан хотя бы один партнер');
|
||||
}
|
||||
|
||||
if (params.quorumPercentage > 100) {
|
||||
throw new Error('Процент кворума не может превышать 100%');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Подготавливает параметры для деплоя
|
||||
* @param {Object} params - Параметры DLE
|
||||
* @returns {Object} - Подготовленные параметры
|
||||
*/
|
||||
prepareDeployParams(params) {
|
||||
// Создаем копию объекта, чтобы не изменять исходный
|
||||
const deployParams = { ...params };
|
||||
|
||||
// Преобразуем суммы из строк или чисел в BigNumber, если нужно
|
||||
deployParams.amounts = params.amounts.map(amount => {
|
||||
if (typeof amount === 'string' && !amount.startsWith('0x')) {
|
||||
return ethers.parseEther(amount).toString();
|
||||
}
|
||||
return amount.toString();
|
||||
});
|
||||
|
||||
// Преобразуем proposalThreshold в BigNumber, если нужно
|
||||
if (typeof deployParams.proposalThreshold === 'string' && !deployParams.proposalThreshold.startsWith('0x')) {
|
||||
deployParams.proposalThreshold = ethers.parseEther(deployParams.proposalThreshold).toString();
|
||||
} else {
|
||||
deployParams.proposalThreshold = deployParams.proposalThreshold.toString();
|
||||
}
|
||||
|
||||
return deployParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет параметры деплоя во временный файл
|
||||
* @param {Object} params - Параметры деплоя
|
||||
* @returns {string} - Путь к файлу с параметрами
|
||||
*/
|
||||
saveParamsToFile(params) {
|
||||
const tempDir = path.join(__dirname, '../temp');
|
||||
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileName = `dle-params-${Date.now()}.json`;
|
||||
const filePath = path.join(tempDir, fileName);
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(params, null, 2));
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает скрипт деплоя DLE
|
||||
* @param {string} paramsFile - Путь к файлу с параметрами
|
||||
* @returns {Promise<Object>} - Результат деплоя
|
||||
*/
|
||||
runDeployScript(paramsFile, extraEnv = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const scriptPath = path.join(__dirname, '../scripts/deploy/create-dle-manual.js');
|
||||
if (!fs.existsSync(scriptPath)) {
|
||||
reject(new Error('Скрипт деплоя не найден: ' + scriptPath));
|
||||
return;
|
||||
}
|
||||
// Формируем универсальные переменные окружения
|
||||
const envVars = {
|
||||
...process.env,
|
||||
[`${extraEnv.envNetworkKey}_RPC_URL`]: extraEnv.rpcUrl,
|
||||
[`${extraEnv.envNetworkKey}_PRIVATE_KEY`]: extraEnv.privateKey
|
||||
};
|
||||
// Запускаем скрипт с нужной сетью
|
||||
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath, '--network', extraEnv.networkId], {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
env: envVars,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
hardhatProcess.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
stdout += output;
|
||||
logger.debug(`[Hardhat] ${output}`);
|
||||
});
|
||||
|
||||
hardhatProcess.stderr.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
stderr += output;
|
||||
logger.error(`[Hardhat Error] ${output}`);
|
||||
});
|
||||
|
||||
hardhatProcess.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
logger.error(`Скрипт деплоя завершился с кодом: ${code}`);
|
||||
reject(new Error(`Ошибка деплоя: ${stderr}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Пытаемся извлечь адреса контрактов из вывода
|
||||
try {
|
||||
const tokenAddressMatch = stdout.match(/Адрес токена: (0x[a-fA-F0-9]{40})/);
|
||||
const timelockAddressMatch = stdout.match(/Адрес таймлока: (0x[a-fA-F0-9]{40})/);
|
||||
const governorAddressMatch = stdout.match(/Адрес контракта Governor: (0x[a-fA-F0-9]{40})/);
|
||||
|
||||
if (tokenAddressMatch && timelockAddressMatch && governorAddressMatch) {
|
||||
resolve({
|
||||
tokenAddress: tokenAddressMatch[1],
|
||||
timelockAddress: timelockAddressMatch[1],
|
||||
governorAddress: governorAddressMatch[1],
|
||||
success: true
|
||||
});
|
||||
} else {
|
||||
// Если не удалось извлечь адреса, ищем файл с результатами
|
||||
const dlesDir = path.join(__dirname, '../contracts-data/dles');
|
||||
if (fs.existsSync(dlesDir)) {
|
||||
const files = fs.readdirSync(dlesDir);
|
||||
if (files.length > 0) {
|
||||
// Берем самый свежий файл
|
||||
const latestFile = files
|
||||
.map(f => ({ name: f, time: fs.statSync(path.join(dlesDir, f)).mtime.getTime() }))
|
||||
.sort((a, b) => b.time - a.time)[0].name;
|
||||
|
||||
const dleData = JSON.parse(fs.readFileSync(path.join(dlesDir, latestFile), 'utf8'));
|
||||
resolve({
|
||||
...dleData,
|
||||
success: true
|
||||
});
|
||||
} else {
|
||||
reject(new Error('Не удалось найти информацию о созданном DLE'));
|
||||
}
|
||||
} else {
|
||||
reject(new Error('Не удалось найти директорию с результатами создания DLE'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при извлечении результатов деплоя:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список всех созданных DLE
|
||||
* @returns {Array<Object>} - Список DLE
|
||||
*/
|
||||
getAllDLEs() {
|
||||
try {
|
||||
const dlesDir = path.join(__dirname, '../contracts-data/dles');
|
||||
|
||||
if (!fs.existsSync(dlesDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(dlesDir);
|
||||
return files
|
||||
.filter(file => file.endsWith('.json') && file !== 'test.json' && file !== 'node-test.json')
|
||||
.map(file => {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(path.join(dlesDir, file), 'utf8'));
|
||||
// Добавляем имя файла к данным DLE для возможности удаления пустых DLE
|
||||
return { ...data, _fileName: file };
|
||||
} catch (error) {
|
||||
logger.error(`Ошибка при чтении файла ${file}:`, error);
|
||||
// Для поврежденных файлов возвращаем минимальную информацию
|
||||
return {
|
||||
_fileName: file,
|
||||
_corrupted: true
|
||||
};
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Ошибка при получении списка DLE:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new DLEService();
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "test0",
|
||||
"symbol": "0test",
|
||||
"location": "83504, México",
|
||||
"isicCodes": [],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b"
|
||||
],
|
||||
"amounts": [
|
||||
"10000000000000000000"
|
||||
],
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 0,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "1",
|
||||
"quorumPercentage": 51
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "test1",
|
||||
"symbol": "test1",
|
||||
"location": "245200, România, Băile Govora",
|
||||
"isicCodes": [
|
||||
"5510"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b"
|
||||
],
|
||||
"amounts": [
|
||||
"100000000000000000000"
|
||||
],
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 0,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "1",
|
||||
"quorumPercentage": 51
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "test1",
|
||||
"symbol": "test1",
|
||||
"location": "245200, România, Băile Govora",
|
||||
"isicCodes": [
|
||||
"5510"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b"
|
||||
],
|
||||
"amounts": [
|
||||
"100000000000000000000"
|
||||
],
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 0,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "1",
|
||||
"quorumPercentage": 51
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "test1",
|
||||
"symbol": "test1",
|
||||
"location": "245200, România, Băile Govora",
|
||||
"isicCodes": [
|
||||
"5510"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b"
|
||||
],
|
||||
"amounts": [
|
||||
"100000000000000000000"
|
||||
],
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 0,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "1",
|
||||
"quorumPercentage": 51
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "test2",
|
||||
"symbol": "test2",
|
||||
"location": "245000, 中国, 黄山市",
|
||||
"isicCodes": [
|
||||
"6810"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b"
|
||||
],
|
||||
"amounts": [
|
||||
"110000000000000000000"
|
||||
],
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 0,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "1",
|
||||
"quorumPercentage": 51
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "тест 3",
|
||||
"symbol": "тест3",
|
||||
"location": "07255, México",
|
||||
"isicCodes": [
|
||||
"6810",
|
||||
"8510"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b",
|
||||
"0x15A4ed4759e5762174b300a4Cf51cc17ad967f4d"
|
||||
],
|
||||
"amounts": [
|
||||
"1000000000000000000",
|
||||
"100000000000000000000000"
|
||||
],
|
||||
"network": "sepolia",
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 6646,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "10000",
|
||||
"quorumPercentage": 51
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "тест 3",
|
||||
"symbol": "тест3",
|
||||
"location": "07255, México",
|
||||
"isicCodes": [
|
||||
"6810",
|
||||
"8510"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b",
|
||||
"0x15A4ed4759e5762174b300a4Cf51cc17ad967f4d"
|
||||
],
|
||||
"amounts": [
|
||||
"1000000000000000000",
|
||||
"100000000000000000000000"
|
||||
],
|
||||
"network": "sepolia",
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 6646,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "10000",
|
||||
"quorumPercentage": 51
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "тест 3",
|
||||
"symbol": "тест3",
|
||||
"location": "07255, México",
|
||||
"isicCodes": [
|
||||
"6810",
|
||||
"8510"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b",
|
||||
"0x15A4ed4759e5762174b300a4Cf51cc17ad967f4d"
|
||||
],
|
||||
"amounts": [
|
||||
"1000000000000000000",
|
||||
"100000000000000000000000"
|
||||
],
|
||||
"network": "sepolia",
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 6646,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "10000",
|
||||
"quorumPercentage": 51
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "тест 3",
|
||||
"symbol": "тест3",
|
||||
"location": "07255, México",
|
||||
"isicCodes": [
|
||||
"6810",
|
||||
"8510"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b",
|
||||
"0x15A4ed4759e5762174b300a4Cf51cc17ad967f4d"
|
||||
],
|
||||
"amounts": [
|
||||
"1000000000000000000",
|
||||
"100000000000000000000000"
|
||||
],
|
||||
"network": "sepolia",
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 6646,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "10000",
|
||||
"quorumPercentage": 51
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "тест3 ",
|
||||
"symbol": "тест3",
|
||||
"location": "07852, United States, Roxbury Township",
|
||||
"isicCodes": [
|
||||
"6411"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b",
|
||||
"0x0a98c54327253bb51f99c8218e5a7a01933d5f57"
|
||||
],
|
||||
"amounts": [
|
||||
"1000000000000000000",
|
||||
"5000000000000000000"
|
||||
],
|
||||
"network": "sepolia",
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 6646,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "5",
|
||||
"quorumPercentage": 51,
|
||||
"privateKey": "7de38b2ada1d23581342f106c8587ce26068797b3bc06656e24b9dcd1810c7b1"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "тест3",
|
||||
"symbol": "3тест",
|
||||
"location": "07885, United States, Rockaway Township",
|
||||
"isicCodes": [
|
||||
"8411"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b",
|
||||
"0x15a4ed4759e5762174b300a4cf51cc17ad967f4d"
|
||||
],
|
||||
"amounts": [
|
||||
"1000000000000000000",
|
||||
"100000000000000000000"
|
||||
],
|
||||
"network": "sepolia",
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 6646,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "2",
|
||||
"quorumPercentage": 51,
|
||||
"privateKey": "7de38b2ada1d23581342f106c8587ce26068797b3bc06656e24b9dcd1810c7b1"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "ест3",
|
||||
"symbol": "тест3",
|
||||
"location": "07522, United States, Paterson",
|
||||
"isicCodes": [
|
||||
"0111"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b",
|
||||
"0x0a98c54327253bb51f99c8218e5a7a01933d5f57"
|
||||
],
|
||||
"amounts": [
|
||||
"1000000000000000000",
|
||||
"1000000000000000000000000"
|
||||
],
|
||||
"network": "sepolia",
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 6646,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "2",
|
||||
"quorumPercentage": 51,
|
||||
"privateKey": "7de38b2ada1d23581342f106c8587ce26068797b3bc06656e24b9dcd1810c7b1"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "ест3",
|
||||
"symbol": "тест3",
|
||||
"location": "07522, United States, Paterson",
|
||||
"isicCodes": [
|
||||
"0111"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b",
|
||||
"0x0a98c54327253bb51f99c8218e5a7a01933d5f57"
|
||||
],
|
||||
"amounts": [
|
||||
"1000000000000000000",
|
||||
"1000000000000000000000000"
|
||||
],
|
||||
"network": "sepolia",
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 6646,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "2",
|
||||
"quorumPercentage": 51,
|
||||
"privateKey": "7de38b2ada1d23581342f106c8587ce26068797b3bc06656e24b9dcd1810c7b1"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "ест3",
|
||||
"symbol": "тест3",
|
||||
"location": "07522, United States, Paterson",
|
||||
"isicCodes": [
|
||||
"0111"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b",
|
||||
"0x0a98c54327253bb51f99c8218e5a7a01933d5f57"
|
||||
],
|
||||
"amounts": [
|
||||
"1000000000000000000",
|
||||
"1000000000000000000000000"
|
||||
],
|
||||
"network": "sepolia",
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 6646,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "2",
|
||||
"quorumPercentage": 51,
|
||||
"privateKey": "7de38b2ada1d23581342f106c8587ce26068797b3bc06656e24b9dcd1810c7b1"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "ест3",
|
||||
"symbol": "тест3",
|
||||
"location": "07522, United States, Paterson",
|
||||
"isicCodes": [
|
||||
"0111"
|
||||
],
|
||||
"partners": [
|
||||
"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b",
|
||||
"0x0a98c54327253bb51f99c8218e5a7a01933d5f57"
|
||||
],
|
||||
"amounts": [
|
||||
"1000000000000000000",
|
||||
"1000000000000000000000000"
|
||||
],
|
||||
"network": "sepolia",
|
||||
"minTimelockDelay": 1,
|
||||
"votingDelay": 6646,
|
||||
"votingPeriod": 6646,
|
||||
"proposalThreshold": "2",
|
||||
"quorumPercentage": 51,
|
||||
"privateKey": "7de38b2ada1d23581342f106c8587ce26068797b3bc06656e24b9dcd1810c7b1"
|
||||
}
|
||||
482
backend/test/DLE.test.js
Normal file
482
backend/test/DLE.test.js
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const { expect } = require("chai");
|
||||
const { ethers } = require("hardhat");
|
||||
|
||||
describe("DLE Smart Contract", function () {
|
||||
let DLE;
|
||||
let dle;
|
||||
let owner;
|
||||
let partner1;
|
||||
let partner2;
|
||||
let partner3;
|
||||
let addrs;
|
||||
|
||||
beforeEach(async function () {
|
||||
// Получаем аккаунты
|
||||
[owner, partner1, partner2, partner3, ...addrs] = await ethers.getSigners();
|
||||
|
||||
// Деплоим контракт
|
||||
const DLEFactory = await ethers.getContractFactory("DLE");
|
||||
|
||||
const config = {
|
||||
name: "Digital Legal Entity",
|
||||
symbol: "DLE",
|
||||
location: "Moscow, Russia",
|
||||
coordinates: "55.7558,37.6176",
|
||||
jurisdiction: 1, // Россия
|
||||
oktmo: 45000000000,
|
||||
okvedCodes: ["62.01", "62.02", "62.03"],
|
||||
kpp: 770101001,
|
||||
quorumPercentage: 60, // 60%
|
||||
initialPartners: [partner1.address, partner2.address, partner3.address],
|
||||
initialAmounts: [ethers.parseEther("1000"), ethers.parseEther("1000"), ethers.parseEther("1000")],
|
||||
supportedChainIds: [1, 137, 56, 42161] // Ethereum, Polygon, BSC, Arbitrum
|
||||
};
|
||||
|
||||
dle = await DLEFactory.deploy(config, 1); // ChainId = 1 (Ethereum)
|
||||
await dle.waitForDeployment();
|
||||
});
|
||||
|
||||
describe("Деплой и инициализация", function () {
|
||||
it("Должен правильно инициализировать DLE", async function () {
|
||||
const dleInfo = await dle.getDLEInfo();
|
||||
|
||||
expect(dleInfo.name).to.equal("Digital Legal Entity");
|
||||
expect(dleInfo.symbol).to.equal("DLE");
|
||||
expect(dleInfo.location).to.equal("Moscow, Russia");
|
||||
expect(dleInfo.jurisdiction).to.equal(1);
|
||||
expect(dleInfo.isActive).to.be.true;
|
||||
});
|
||||
|
||||
it("Должен распределить начальные токены", async function () {
|
||||
expect(await dle.balanceOf(partner1.address)).to.equal(ethers.parseEther("1000"));
|
||||
expect(await dle.balanceOf(partner2.address)).to.equal(ethers.parseEther("1000"));
|
||||
expect(await dle.balanceOf(partner3.address)).to.equal(ethers.parseEther("1000"));
|
||||
});
|
||||
|
||||
it("Должен установить кворум", async function () {
|
||||
expect(await dle.quorumPercentage()).to.equal(60);
|
||||
});
|
||||
|
||||
it("Должен настроить поддерживаемые цепочки", async function () {
|
||||
expect(await dle.isChainSupported(1)).to.be.true; // Ethereum
|
||||
expect(await dle.isChainSupported(137)).to.be.true; // Polygon
|
||||
expect(await dle.isChainSupported(56)).to.be.true; // BSC
|
||||
expect(await dle.isChainSupported(42161)).to.be.true; // Arbitrum
|
||||
expect(await dle.isChainSupported(999)).to.be.false; // Неподдерживаемая цепочка
|
||||
});
|
||||
});
|
||||
|
||||
describe("Система голосования", function () {
|
||||
it("Должен создать предложение", async function () {
|
||||
const description = "Передать 100 токенов от Partner1 к Partner2";
|
||||
const duration = 7 * 24 * 60 * 60; // 7 дней
|
||||
const operation = ethers.AbiCoder.defaultAbiCoder().encode(
|
||||
["bytes4", "bytes"],
|
||||
[
|
||||
"0xa9059cbb", // transfer(address,uint256) selector
|
||||
ethers.AbiCoder.defaultAbiCoder().encode(
|
||||
["address", "uint256"],
|
||||
[partner2.address, ethers.parseEther("100")]
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
const tx = await dle.connect(partner1).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1 // governanceChainId = Ethereum
|
||||
);
|
||||
|
||||
const receipt = await tx.wait();
|
||||
const event = receipt.logs.find(log =>
|
||||
log.fragment && log.fragment.name === "ProposalCreated"
|
||||
);
|
||||
|
||||
expect(event).to.not.be.undefined;
|
||||
expect(await dle.proposalCounter()).to.equal(1);
|
||||
});
|
||||
|
||||
it("Должен голосовать за предложение", async function () {
|
||||
// Создаем предложение
|
||||
const description = "Тестовое предложение";
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
const operation = "0x";
|
||||
|
||||
await dle.connect(partner1).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1
|
||||
);
|
||||
|
||||
// Голосуем за предложение
|
||||
await dle.connect(partner1).vote(0, true);
|
||||
await dle.connect(partner2).vote(0, true);
|
||||
|
||||
const proposal = await dle.proposals(0);
|
||||
expect(proposal.forVotes).to.equal(ethers.parseEther("2000")); // 1000 + 1000
|
||||
expect(proposal.againstVotes).to.equal(0);
|
||||
});
|
||||
|
||||
it("Должен проверить результат голосования", async function () {
|
||||
// Создаем предложение
|
||||
const description = "Тестовое предложение";
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
const operation = "0x";
|
||||
|
||||
await dle.connect(partner1).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1
|
||||
);
|
||||
|
||||
// Голосуем за предложение (60% от 3000 = 1800)
|
||||
await dle.connect(partner1).vote(0, true); // 1000
|
||||
await dle.connect(partner2).vote(0, true); // 1000
|
||||
await dle.connect(partner3).vote(0, true); // 1000
|
||||
|
||||
const [passed, quorumReached] = await dle.checkProposalResult(0);
|
||||
expect(passed).to.be.true;
|
||||
expect(quorumReached).to.be.true;
|
||||
});
|
||||
|
||||
it("Не должен позволить голосовать дважды", async function () {
|
||||
// Создаем предложение
|
||||
const description = "Тестовое предложение";
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
const operation = "0x";
|
||||
|
||||
await dle.connect(partner1).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1
|
||||
);
|
||||
|
||||
// Первое голосование
|
||||
await dle.connect(partner1).vote(0, true);
|
||||
|
||||
// Второе голосование должно упасть
|
||||
await expect(
|
||||
dle.connect(partner1).vote(0, true)
|
||||
).to.be.revertedWith("Already voted");
|
||||
});
|
||||
|
||||
it("Не должен позволить голосовать без токенов", async function () {
|
||||
// Создаем предложение
|
||||
const description = "Тестовое предложение";
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
const operation = "0x";
|
||||
|
||||
await dle.connect(partner1).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1
|
||||
);
|
||||
|
||||
// Голосование без токенов должно упасть
|
||||
await expect(
|
||||
dle.connect(addrs[0]).vote(0, true)
|
||||
).to.be.revertedWith("No tokens to vote");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Мультиподпись", function () {
|
||||
it("Должен создать мультиподпись операцию", async function () {
|
||||
const operationHash = ethers.keccak256(ethers.toUtf8Bytes("test operation"));
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
|
||||
const tx = await dle.connect(partner1).createMultiSigOperation(
|
||||
operationHash,
|
||||
duration
|
||||
);
|
||||
|
||||
const receipt = await tx.wait();
|
||||
const event = receipt.logs.find(log =>
|
||||
log.fragment && log.fragment.name === "MultiSigOperationCreated"
|
||||
);
|
||||
|
||||
expect(event).to.not.be.undefined;
|
||||
expect(await dle.multiSigCounter()).to.equal(1);
|
||||
});
|
||||
|
||||
it("Должен подписать мультиподпись операцию", async function () {
|
||||
const operationHash = ethers.keccak256(ethers.toUtf8Bytes("test operation"));
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
|
||||
await dle.connect(partner1).createMultiSigOperation(
|
||||
operationHash,
|
||||
duration
|
||||
);
|
||||
|
||||
// Подписываем операцию
|
||||
await dle.connect(partner1).signMultiSigOperation(0, true);
|
||||
await dle.connect(partner2).signMultiSigOperation(0, true);
|
||||
|
||||
const operation = await dle.multiSigOperations(0);
|
||||
expect(operation.forSignatures).to.equal(ethers.parseEther("2000")); // 1000 + 1000
|
||||
expect(operation.againstSignatures).to.equal(0);
|
||||
});
|
||||
|
||||
it("Должен проверить результат мультиподписи", async function () {
|
||||
const operationHash = ethers.keccak256(ethers.toUtf8Bytes("test operation"));
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
|
||||
await dle.connect(partner1).createMultiSigOperation(
|
||||
operationHash,
|
||||
duration
|
||||
);
|
||||
|
||||
// Подписываем операцию (60% от 3000 = 1800)
|
||||
await dle.connect(partner1).signMultiSigOperation(0, true); // 1000
|
||||
await dle.connect(partner2).signMultiSigOperation(0, true); // 1000
|
||||
await dle.connect(partner3).signMultiSigOperation(0, true); // 1000
|
||||
|
||||
const [passed, quorumReached] = await dle.checkMultiSigResult(0);
|
||||
expect(passed).to.be.true;
|
||||
expect(quorumReached).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Мульти-чейн синхронизация", function () {
|
||||
it("Должен синхронизировать голоса из другой цепочки", async function () {
|
||||
// Создаем предложение
|
||||
const description = "Тестовое предложение";
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
const operation = "0x";
|
||||
|
||||
await dle.connect(partner1).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1
|
||||
);
|
||||
|
||||
// Синхронизируем голоса из другой цепочки
|
||||
await dle.connect(partner1).syncVoteFromChain(
|
||||
0, // proposalId
|
||||
137, // fromChainId (Polygon)
|
||||
ethers.parseEther("500"), // forVotes
|
||||
ethers.parseEther("200"), // againstVotes
|
||||
"0x" // proof
|
||||
);
|
||||
|
||||
const proposal = await dle.proposals(0);
|
||||
expect(proposal.forVotes).to.equal(ethers.parseEther("500"));
|
||||
expect(proposal.againstVotes).to.equal(ethers.parseEther("200"));
|
||||
});
|
||||
|
||||
it("Должен синхронизировать мультиподпись из другой цепочки", async function () {
|
||||
const operationHash = ethers.keccak256(ethers.toUtf8Bytes("test operation"));
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
|
||||
await dle.connect(partner1).createMultiSigOperation(
|
||||
operationHash,
|
||||
duration
|
||||
);
|
||||
|
||||
// Синхронизируем мультиподпись из другой цепочки
|
||||
await dle.connect(partner1).syncMultiSigFromChain(
|
||||
0, // operationId
|
||||
137, // fromChainId (Polygon)
|
||||
ethers.parseEther("800"), // forSignatures
|
||||
ethers.parseEther("300"), // againstSignatures
|
||||
"0x" // proof
|
||||
);
|
||||
|
||||
const operation = await dle.multiSigOperations(0);
|
||||
expect(operation.forSignatures).to.equal(ethers.parseEther("800"));
|
||||
expect(operation.againstSignatures).to.equal(ethers.parseEther("300"));
|
||||
});
|
||||
|
||||
it("Должен проверить готовность синхронизации", async function () {
|
||||
// Создаем предложение
|
||||
const description = "Тестовое предложение";
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
const operation = "0x";
|
||||
|
||||
await dle.connect(partner1).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1
|
||||
);
|
||||
|
||||
// Проверяем готовность синхронизации
|
||||
const isReady = await dle.checkSyncReadiness(0);
|
||||
expect(isReady).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Управление модулями", function () {
|
||||
it("Должен добавить модуль", async function () {
|
||||
const moduleId = ethers.keccak256(ethers.toUtf8Bytes("TreasuryModule"));
|
||||
const moduleAddress = addrs[0].address;
|
||||
|
||||
await dle.connect(partner1).addModule(moduleId, moduleAddress);
|
||||
|
||||
expect(await dle.isModuleActive(moduleId)).to.be.true;
|
||||
expect(await dle.getModuleAddress(moduleId)).to.equal(moduleAddress);
|
||||
});
|
||||
|
||||
it("Должен удалить модуль", async function () {
|
||||
const moduleId = ethers.keccak256(ethers.toUtf8Bytes("TreasuryModule"));
|
||||
const moduleAddress = addrs[0].address;
|
||||
|
||||
await dle.connect(partner1).addModule(moduleId, moduleAddress);
|
||||
await dle.connect(partner1).removeModule(moduleId);
|
||||
|
||||
expect(await dle.isModuleActive(moduleId)).to.be.false;
|
||||
});
|
||||
|
||||
it("Не должен позволить добавить модуль без токенов", async function () {
|
||||
const moduleId = ethers.keccak256(ethers.toUtf8Bytes("TreasuryModule"));
|
||||
const moduleAddress = addrs[0].address;
|
||||
|
||||
await expect(
|
||||
dle.connect(addrs[0]).addModule(moduleId, moduleAddress)
|
||||
).to.be.revertedWith("Must hold tokens to add module");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Проверка подключений", function () {
|
||||
it("Должен проверить подключение к цепочке", async function () {
|
||||
expect(await dle.checkChainConnection(1)).to.be.true; // Ethereum
|
||||
expect(await dle.checkChainConnection(137)).to.be.true; // Polygon
|
||||
expect(await dle.checkChainConnection(56)).to.be.true; // BSC
|
||||
expect(await dle.checkChainConnection(42161)).to.be.true; // Arbitrum
|
||||
expect(await dle.checkChainConnection(999)).to.be.false; // Неподдерживаемая цепочка
|
||||
});
|
||||
|
||||
it("Должен получить количество поддерживаемых цепочек", async function () {
|
||||
expect(await dle.getSupportedChainCount()).to.equal(4);
|
||||
});
|
||||
|
||||
it("Должен получить ID поддерживаемой цепочки", async function () {
|
||||
expect(await dle.getSupportedChainId(0)).to.equal(1); // Ethereum
|
||||
expect(await dle.getSupportedChainId(1)).to.equal(137); // Polygon
|
||||
expect(await dle.getSupportedChainId(2)).to.equal(56); // BSC
|
||||
expect(await dle.getSupportedChainId(3)).to.equal(42161); // Arbitrum
|
||||
});
|
||||
});
|
||||
|
||||
describe("Исполнение операций", function () {
|
||||
it("Должен исполнить предложение с передачей токенов", async function () {
|
||||
// Создаем предложение для передачи токенов
|
||||
const description = "Передать 100 токенов от Partner1 к Partner2";
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
const operation = ethers.AbiCoder.defaultAbiCoder().encode(
|
||||
["bytes4", "bytes"],
|
||||
[
|
||||
"0xa9059cbb", // transfer(address,uint256) selector
|
||||
ethers.AbiCoder.defaultAbiCoder().encode(
|
||||
["address", "uint256"],
|
||||
[partner2.address, ethers.parseEther("100")]
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
await dle.connect(partner1).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1
|
||||
);
|
||||
|
||||
// Голосуем за предложение
|
||||
await dle.connect(partner1).vote(0, true);
|
||||
await dle.connect(partner2).vote(0, true);
|
||||
await dle.connect(partner3).vote(0, true);
|
||||
|
||||
// Ждем окончания голосования
|
||||
await ethers.provider.send("evm_increaseTime", [7 * 24 * 60 * 60]);
|
||||
await ethers.provider.send("evm_mine");
|
||||
|
||||
// Исполняем предложение
|
||||
await dle.connect(partner1).executeProposal(0);
|
||||
|
||||
// Проверяем, что токены переданы
|
||||
expect(await dle.balanceOf(partner1.address)).to.equal(ethers.parseEther("900")); // 1000 - 100
|
||||
expect(await dle.balanceOf(partner2.address)).to.equal(ethers.parseEther("1100")); // 1000 + 100
|
||||
});
|
||||
});
|
||||
|
||||
describe("Безопасность", function () {
|
||||
it("Не должен позволить создать предложение без токенов", async function () {
|
||||
const description = "Тестовое предложение";
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
const operation = "0x";
|
||||
|
||||
await expect(
|
||||
dle.connect(addrs[0]).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1
|
||||
)
|
||||
).to.be.revertedWith("Must hold tokens to create proposal");
|
||||
});
|
||||
|
||||
it("Не должен позволить голосовать после окончания срока", async function () {
|
||||
// Создаем предложение с коротким сроком
|
||||
const description = "Тестовое предложение";
|
||||
const duration = 1; // 1 секунда
|
||||
const operation = "0x";
|
||||
|
||||
await dle.connect(partner1).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1
|
||||
);
|
||||
|
||||
// Ждем окончания срока
|
||||
await ethers.provider.send("evm_increaseTime", [2]);
|
||||
await ethers.provider.send("evm_mine");
|
||||
|
||||
// Голосование должно упасть
|
||||
await expect(
|
||||
dle.connect(partner1).vote(0, true)
|
||||
).to.be.revertedWith("Voting ended");
|
||||
});
|
||||
|
||||
it("Не должен позволить исполнить предложение до окончания срока", async function () {
|
||||
// Создаем предложение
|
||||
const description = "Тестовое предложение";
|
||||
const duration = 7 * 24 * 60 * 60;
|
||||
const operation = "0x";
|
||||
|
||||
await dle.connect(partner1).createProposal(
|
||||
description,
|
||||
duration,
|
||||
operation,
|
||||
1
|
||||
);
|
||||
|
||||
// Голосуем за предложение
|
||||
await dle.connect(partner1).vote(0, true);
|
||||
await dle.connect(partner2).vote(0, true);
|
||||
await dle.connect(partner3).vote(0, true);
|
||||
|
||||
// Исполнение должно упасть
|
||||
await expect(
|
||||
dle.connect(partner1).executeProposal(0)
|
||||
).to.be.revertedWith("Voting not ended");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user