// SPDX-License-Identifier: PROPRIETARY AND MIT // Copyright (c) 2024-2026 Тарабанов Александр Викторович // All rights reserved. // For licensing inquiries: info@hb3-accelerator.com pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; interface IERC1271 { function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue); } interface IMultichainMetadata { function getMultichainInfo() external view returns (uint256[] memory supportedChainIds, uint256 defaultVotingChain); function getMultichainAddresses() external view returns (uint256[] memory chainIds, address[] memory addresses); } // DLE (Digital Legal Entity) - основной контракт с модульной архитектурой contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMetadata { using ECDSA for bytes32; struct DLEInfo { string name; string symbol; string location; string coordinates; uint256 jurisdiction; string[] okvedCodes; uint256 kpp; uint256 creationTimestamp; bool isActive; } struct DLEConfig { string name; string symbol; string location; string coordinates; uint256 jurisdiction; string[] okvedCodes; uint256 kpp; uint256 quorumPercentage; address[] initialPartners; uint256[] initialAmounts; uint256[] supportedChainIds; // Поддерживаемые цепочки } struct Proposal { uint256 id; string description; uint256 forVotes; uint256 againstVotes; bool executed; bool canceled; uint256 deadline; // конец периода голосования (sec) address initiator; bytes operation; // операция для исполнения uint256[] targetChains; // целевые сети для исполнения uint256 snapshotTimepoint; // блок/временная точка для getPastVotes mapping(address => bool) hasVoted; } // Основные настройки DLEInfo public dleInfo; uint256 public quorumPercentage; uint256 public proposalCounter; // Удален currentChainId - теперь используется block.chainid для проверок // Публичный URI логотипа токена/организации (можно установить при деплое через инициализатор) string public logoURI; // Модули mapping(bytes32 => address) public modules; mapping(bytes32 => bool) public activeModules; address public immutable initializer; // Адрес, имеющий право на однократную инициализацию логотипа // Предложения mapping(uint256 => Proposal) public proposals; uint256[] public allProposalIds; // Мульти-чейн mapping(uint256 => bool) public supportedChains; uint256[] public supportedChainIds; // События event DLEInitialized( string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp, address tokenAddress, uint256[] supportedChainIds ); event InitialTokensDistributed(address[] partners, uint256[] amounts); 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 ProposalCancelled(uint256 proposalId, string reason); event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains); event ModuleAdded(bytes32 moduleId, address moduleAddress); event ModuleRemoved(bytes32 moduleId); event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId); event ChainAdded(uint256 chainId); event ChainRemoved(uint256 chainId); event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp); event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage); event TokensTransferredByGovernance(address indexed sender, address indexed recipient, uint256 amount); event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration); event LogoURIUpdated(string oldURI, string newURI); // EIP712 typehash для подписи одобрения исполнения предложения bytes32 private constant EXECUTION_APPROVAL_TYPEHASH = keccak256( "ExecutionApproval(uint256 proposalId,bytes32 operationHash,uint256 chainId,uint256 snapshotTimepoint)" ); // Custom errors (reduce bytecode size) error ErrZeroAddress(); error ErrArrayMismatch(); error ErrNoPartners(); error ErrZeroAmount(); error ErrOnlyInitializer(); error ErrLogoAlreadySet(); error ErrNotHolder(); error ErrTooShort(); error ErrTooLong(); error ErrBadChain(); error ErrProposalMissing(); error ErrProposalEnded(); error ErrProposalExecuted(); error ErrAlreadyVoted(); error ErrWrongChain(); error ErrUnsupportedChain(); error ErrNoPower(); error ErrNotReady(); error ErrNotInitiator(); error ErrUnauthorized(); error ErrLowPower(); error ErrBadTarget(); error ErrBadSig1271(); error ErrBadSig(); error ErrDuplicateSigner(); error ErrNoSigners(); error ErrSigLengthMismatch(); error ErrInvalidOperation(); error ErrNameEmpty(); error ErrSymbolEmpty(); error ErrLocationEmpty(); error ErrBadJurisdiction(); error ErrBadKPP(); error ErrBadQuorum(); error ErrChainAlreadySupported(); error ErrChainNotSupported(); error ErrCannotRemoveCurrentChain(); error ErrTransfersDisabled(); error ErrApprovalsDisabled(); error ErrProposalCanceled(); // Константы безопасности (можно изменять через governance) uint256 public maxVotingDuration = 30 days; // Максимальное время голосования uint256 public minVotingDuration = 1 hours; // Минимальное время голосования // Удалён буфер ограничения голосования в последние минуты перед дедлайном constructor( DLEConfig memory config, address _initializer ) ERC20(config.name, config.symbol) ERC20Permit(config.name) { if (_initializer == address(0)) revert ErrZeroAddress(); initializer = _initializer; dleInfo = DLEInfo({ name: config.name, symbol: config.symbol, location: config.location, coordinates: config.coordinates, jurisdiction: config.jurisdiction, okvedCodes: config.okvedCodes, kpp: config.kpp, creationTimestamp: block.timestamp, isActive: true }); quorumPercentage = config.quorumPercentage; // Настраиваем поддерживаемые цепочки for (uint256 i = 0; i < config.supportedChainIds.length; i++) { supportedChains[config.supportedChainIds[i]] = true; supportedChainIds.push(config.supportedChainIds[i]); } // Распределяем начальные токены партнерам if (config.initialPartners.length != config.initialAmounts.length) revert ErrArrayMismatch(); if (config.initialPartners.length == 0) revert ErrNoPartners(); for (uint256 i = 0; i < config.initialPartners.length; i++) { address partner = config.initialPartners[i]; uint256 amount = config.initialAmounts[i]; if (partner == address(0)) revert ErrZeroAddress(); if (amount == 0) revert ErrZeroAmount(); _mint(partner, amount); // Авто-делегирование голосов себе, чтобы getPastVotes работал без действия пользователя _delegate(partner, partner); } emit InitialTokensDistributed(config.initialPartners, config.initialAmounts); emit DLEInitialized( config.name, config.symbol, config.location, config.coordinates, config.jurisdiction, config.okvedCodes, config.kpp, address(this), config.supportedChainIds ); } // Одноразовая инициализация URI логотипа function initializeLogoURI(string calldata _logoURI) external { if (msg.sender != initializer) revert ErrOnlyInitializer(); if (bytes(logoURI).length != 0) revert ErrLogoAlreadySet(); string memory old = logoURI; logoURI = _logoURI; emit LogoURIUpdated(old, _logoURI); } // Создать предложение для multi-chain голосования function createProposal( string memory _description, uint256 _duration, bytes memory _operation, uint256[] memory _targetChains, uint256 /* _timelockDelay */ ) external returns (uint256) { if (balanceOf(msg.sender) == 0) revert ErrNotHolder(); if (_duration < minVotingDuration) revert ErrTooShort(); if (_duration > maxVotingDuration) revert ErrTooLong(); // _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль return _createProposalInternal( _description, _duration, _operation, _targetChains, msg.sender ); } function _createProposalInternal( string memory _description, uint256 _duration, bytes memory _operation, uint256[] memory _targetChains, address _initiator ) internal returns (uint256) { uint256 proposalId = proposalCounter++; Proposal storage proposal = proposals[proposalId]; proposal.id = proposalId; proposal.description = _description; proposal.forVotes = 0; proposal.againstVotes = 0; proposal.executed = false; proposal.deadline = block.timestamp + _duration; proposal.initiator = _initiator; proposal.operation = _operation; // Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке uint256 nowClock = clock(); proposal.snapshotTimepoint = nowClock == 0 ? 0 : nowClock - 1; // запись целевых сетей for (uint256 i = 0; i < _targetChains.length; i++) { if (!supportedChains[_targetChains[i]]) revert ErrBadTarget(); proposal.targetChains.push(_targetChains[i]); } allProposalIds.push(proposalId); emit ProposalCreated(proposalId, _initiator, _description); emit ProposalTargetsSet(proposalId, _targetChains); return proposalId; } // Голосовать за предложение function vote(uint256 _proposalId, bool _support) external nonReentrant { Proposal storage proposal = proposals[_proposalId]; if (proposal.id != _proposalId) revert ErrProposalMissing(); if (block.timestamp >= proposal.deadline) revert ErrProposalEnded(); if (proposal.executed) revert ErrProposalExecuted(); if (proposal.canceled) revert ErrProposalCanceled(); if (proposal.hasVoted[msg.sender]) revert ErrAlreadyVoted(); // Проверяем, что текущая сеть поддерживается if (!supportedChains[block.chainid]) revert ErrUnsupportedChain(); uint256 votingPower = getPastVotes(msg.sender, proposal.snapshotTimepoint); if (votingPower == 0) revert ErrNoPower(); proposal.hasVoted[msg.sender] = true; if (_support) { proposal.forVotes += votingPower; } else { proposal.againstVotes += votingPower; } emit ProposalVoted(_proposalId, msg.sender, _support, votingPower); } function checkProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) { Proposal storage proposal = proposals[_proposalId]; if (proposal.id != _proposalId) revert ErrProposalMissing(); uint256 totalVotes = proposal.forVotes + proposal.againstVotes; // Используем снапшот totalSupply на момент начала голосования uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint); uint256 quorumRequired = (pastSupply * quorumPercentage) / 100; quorumReached = totalVotes >= quorumRequired; passed = quorumReached && proposal.forVotes > proposal.againstVotes; return (passed, quorumReached); } function executeProposal(uint256 _proposalId) external { Proposal storage proposal = proposals[_proposalId]; if (proposal.id != _proposalId) revert ErrProposalMissing(); if (proposal.executed) revert ErrProposalExecuted(); if (proposal.canceled) revert ErrProposalCanceled(); // Проверяем, что текущая сеть поддерживается if (!supportedChains[block.chainid]) revert ErrUnsupportedChain(); (bool passed, bool quorumReached) = checkProposalResult(_proposalId); // Предложение можно выполнить если: // 1. Дедлайн истек ИЛИ кворум достигнут if (!(block.timestamp >= proposal.deadline || quorumReached)) revert ErrNotReady(); if (!(passed && quorumReached)) revert ErrNotReady(); proposal.executed = true; // Исполняем операцию _executeOperation(_proposalId, proposal.operation); emit ProposalExecuted(_proposalId, proposal.operation); } function cancelProposal(uint256 _proposalId, string calldata reason) external { Proposal storage proposal = proposals[_proposalId]; if (proposal.id != _proposalId) revert ErrProposalMissing(); if (proposal.executed) revert ErrProposalExecuted(); if (block.timestamp + 900 >= proposal.deadline) revert ErrProposalEnded(); if (msg.sender != proposal.initiator) revert ErrNotInitiator(); uint256 vp = getPastVotes(msg.sender, proposal.snapshotTimepoint); uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint); if (vp * 10 < pastSupply) revert ErrLowPower(); proposal.canceled = true; emit ProposalCancelled(_proposalId, reason); } // УДАЛЕНО: syncExecutionFromChain с MerkleProof — небезопасно без доверенного моста function executeProposalBySignatures( uint256 _proposalId, address[] calldata signers, bytes[] calldata signatures ) external nonReentrant { Proposal storage proposal = proposals[_proposalId]; if (proposal.id != _proposalId) revert ErrProposalMissing(); if (proposal.executed) revert ErrProposalExecuted(); if (proposal.canceled) revert ErrProposalCanceled(); // Проверяем, что текущая сеть поддерживается if (!supportedChains[block.chainid]) revert ErrUnsupportedChain(); // Проверяем, что текущая сеть является целевой для предложения if (!_isTargetChain(proposal, block.chainid)) revert ErrBadTarget(); if (signers.length != signatures.length) revert ErrSigLengthMismatch(); if (signers.length == 0) revert ErrNoSigners(); // Все держатели токенов имеют право голосовать bytes32 opHash = keccak256(proposal.operation); bytes32 structHash = keccak256(abi.encode( EXECUTION_APPROVAL_TYPEHASH, _proposalId, opHash, block.chainid, proposal.snapshotTimepoint )); bytes32 digest = _hashTypedDataV4(structHash); uint256 votesFor = 0; for (uint256 i = 0; i < signers.length; i++) { address signer = signers[i]; if (signer.code.length > 0) { // Контрактный кошелёк: проверяем подпись по EIP-1271 try IERC1271(signer).isValidSignature(digest, signatures[i]) returns (bytes4 magic) { if (magic != 0x1626ba7e) revert ErrBadSig1271(); } catch { revert ErrBadSig1271(); } } else { // EOA подпись через ECDSA address recovered = ECDSA.recover(digest, signatures[i]); if (recovered != signer) revert ErrBadSig(); } for (uint256 j = 0; j < i; j++) { if (signers[j] == signer) revert ErrDuplicateSigner(); } uint256 vp = getPastVotes(signer, proposal.snapshotTimepoint); if (vp == 0) revert ErrNoPower(); votesFor += vp; } uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint); uint256 quorumRequired = (pastSupply * quorumPercentage) / 100; if (votesFor < quorumRequired) revert ErrNoPower(); proposal.executed = true; _executeOperation(_proposalId, proposal.operation); emit ProposalExecuted(_proposalId, proposal.operation); emit ProposalExecutionApprovedInChain(_proposalId, block.chainid); } /** * @dev Получить количество поддерживаемых цепочек */ function getSupportedChainCount() public view returns (uint256) { return supportedChainIds.length; } /** * @dev Получить ID поддерживаемой цепочки по индексу * @param _index Индекс цепочки */ function getSupportedChainId(uint256 _index) public view returns (uint256) { require(_index < supportedChainIds.length, "Invalid chain index"); return supportedChainIds[_index]; } /** * @dev Добавить поддерживаемую цепочку (только для владельцев токенов) * @param _chainId ID цепочки */ // Управление списком сетей теперь выполняется только через предложения function _addSupportedChain(uint256 _chainId) internal { require(!supportedChains[_chainId], "Chain already supported"); require(_chainId != block.chainid, "Cannot add current chain"); supportedChains[_chainId] = true; supportedChainIds.push(_chainId); emit ChainAdded(_chainId); } /** * @dev Удалить поддерживаемую цепочку (только для владельцев токенов) * @param _chainId ID цепочки */ function _removeSupportedChain(uint256 _chainId) internal { require(supportedChains[_chainId], "Chain not supported"); require(_chainId != block.chainid, "Cannot remove current chain"); supportedChains[_chainId] = false; // Удаляем из массива for (uint256 i = 0; i < supportedChainIds.length; i++) { if (supportedChainIds[i] == _chainId) { supportedChainIds[i] = supportedChainIds[supportedChainIds.length - 1]; supportedChainIds.pop(); break; } } emit ChainRemoved(_chainId); } /** * @dev Исполнить операцию * @param _proposalId ID предложения * @param _operation Операция для исполнения */ function _executeOperation(uint256 _proposalId, bytes memory _operation) internal { if (_operation.length < 4) revert ErrInvalidOperation(); // Получаем информацию о предложении для доступа к initiator Proposal storage proposal = proposals[_proposalId]; // Декодируем операцию из formата abi.encodeWithSelector bytes4 selector; bytes memory data; // Извлекаем селектор (первые 4 байта) assembly { selector := mload(add(_operation, 0x20)) } // Извлекаем данные (все после первых 4 байтов) if (_operation.length > 4) { data = new bytes(_operation.length - 4); for (uint256 i = 0; i < data.length; i++) { data[i] = _operation[i + 4]; } } else { data = new bytes(0); } if (selector == bytes4(keccak256("_addModule(bytes32,address)"))) { // Операция добавления модуля (bytes32 moduleId, address moduleAddress) = abi.decode(data, (bytes32, address)); _addModule(moduleId, moduleAddress); } else if (selector == bytes4(keccak256("_removeModule(bytes32)"))) { // Операция удаления модуля (bytes32 moduleId) = abi.decode(data, (bytes32)); _removeModule(moduleId); } else if (selector == bytes4(keccak256("_addSupportedChain(uint256)"))) { (uint256 chainIdToAdd) = abi.decode(data, (uint256)); _addSupportedChain(chainIdToAdd); } else if (selector == bytes4(keccak256("_removeSupportedChain(uint256)"))) { (uint256 chainIdToRemove) = abi.decode(data, (uint256)); _removeSupportedChain(chainIdToRemove); } else if (selector == bytes4(keccak256("_transferTokens(address,address,uint256)"))) { // Операция перевода токенов через governance от инициатора (address sender, address recipient, uint256 amount) = abi.decode(data, (address, address, uint256)); // Проверяем, что sender совпадает с инициатором предложения if (sender != proposal.initiator) revert ErrUnauthorized(); _transferTokens(sender, recipient, amount); } else if (selector == bytes4(keccak256("_updateVotingDurations(uint256,uint256)"))) { // Операция обновления времени голосования (uint256 newMinDuration, uint256 newMaxDuration) = abi.decode(data, (uint256, uint256)); _updateVotingDurations(newMinDuration, newMaxDuration); } else if (selector == bytes4(keccak256("_setLogoURI(string)"))) { // Обновление логотипа через governance (string memory newLogo) = abi.decode(data, (string)); _setLogoURI(newLogo); } else if (selector == bytes4(keccak256("_updateQuorumPercentage(uint256)"))) { // Операция обновления процента кворума (uint256 newQuorumPercentage) = abi.decode(data, (uint256)); _updateQuorumPercentage(newQuorumPercentage); } else if (selector == bytes4(keccak256("_updateDLEInfo(string,string,string,string,uint256,string[],uint256)"))) { // Операция обновления информации DLE (string memory name, string memory symbol, string memory location, string memory coordinates, uint256 jurisdiction, string[] memory okvedCodes, uint256 kpp) = abi.decode(data, (string, string, string, string, uint256, string[], uint256)); _updateDLEInfo(name, symbol, location, coordinates, jurisdiction, okvedCodes, kpp); } else if (selector == bytes4(keccak256("offchainAction(bytes32,string,bytes32)"))) { // Оффчейн операция для приложения: идентификатор, тип, хеш полезной нагрузки // (bytes32 actionId, string memory kind, bytes32 payloadHash) = abi.decode(data, (bytes32, string, bytes32)); // Ончейн-побочных эффектов нет. Факт решения фиксируется событием ProposalExecuted. } else { revert ErrInvalidOperation(); } } /** * @dev Обновить информацию DLE * @param _name Новое название * @param _symbol Новый символ * @param _location Новое местонахождение * @param _coordinates Новые координаты * @param _jurisdiction Новая юрисдикция * @param _okvedCodes Новые коды ОКВЭД * @param _kpp Новый КПП */ function _updateDLEInfo( string memory _name, string memory _symbol, string memory _location, string memory _coordinates, uint256 _jurisdiction, string[] memory _okvedCodes, uint256 _kpp ) internal { if (bytes(_name).length == 0) revert ErrNameEmpty(); if (bytes(_symbol).length == 0) revert ErrSymbolEmpty(); if (bytes(_location).length == 0) revert ErrLocationEmpty(); if (_jurisdiction == 0) revert ErrBadJurisdiction(); if (_kpp == 0) revert ErrBadKPP(); dleInfo.name = _name; dleInfo.symbol = _symbol; dleInfo.location = _location; dleInfo.coordinates = _coordinates; dleInfo.jurisdiction = _jurisdiction; dleInfo.okvedCodes = _okvedCodes; dleInfo.kpp = _kpp; emit DLEInfoUpdated(_name, _symbol, _location, _coordinates, _jurisdiction, _okvedCodes, _kpp); } /** * @dev Обновить процент кворума * @param _newQuorumPercentage Новый процент кворума */ function _updateQuorumPercentage(uint256 _newQuorumPercentage) internal { if (!(_newQuorumPercentage > 0 && _newQuorumPercentage <= 100)) revert ErrBadQuorum(); uint256 oldQuorumPercentage = quorumPercentage; quorumPercentage = _newQuorumPercentage; emit QuorumPercentageUpdated(oldQuorumPercentage, _newQuorumPercentage); } /** * @dev Перевести токены через governance (от имени DLE) * @param _recipient Адрес получателя * @param _amount Количество токенов для перевода */ function _transferTokens(address _sender, address _recipient, uint256 _amount) internal { if (_recipient == address(0)) revert ErrZeroAddress(); if (_amount == 0) revert ErrZeroAmount(); require(balanceOf(_sender) >= _amount, "Insufficient token balance"); // Переводим токены от отправителя к получателю _transfer(_sender, _recipient, _amount); emit TokensTransferredByGovernance(_sender, _recipient, _amount); } /** * @dev Обновить время голосования (только через governance) * @param _newMinDuration Новое минимальное время голосования * @param _newMaxDuration Новое максимальное время голосования */ function _updateVotingDurations(uint256 _newMinDuration, uint256 _newMaxDuration) internal { if (_newMinDuration == 0) revert ErrTooShort(); if (!(_newMaxDuration > _newMinDuration)) revert ErrTooLong(); if (_newMinDuration < 10 minutes) revert ErrTooShort(); if (_newMaxDuration > 365 days) revert ErrTooLong(); uint256 oldMinDuration = minVotingDuration; uint256 oldMaxDuration = maxVotingDuration; minVotingDuration = _newMinDuration; maxVotingDuration = _newMaxDuration; emit VotingDurationsUpdated(oldMinDuration, _newMinDuration, oldMaxDuration, _newMaxDuration); } /** * @dev Внутреннее обновление URI логотипа (только через governance). */ function _setLogoURI(string memory _logoURI) internal { string memory old = logoURI; logoURI = _logoURI; emit LogoURIUpdated(old, _logoURI); } /** * @dev Создать предложение о добавлении модуля * @param _description Описание предложения * @param _duration Длительность голосования в секундах * @param _moduleId ID модуля * @param _moduleAddress Адрес модуля * @param _chainId ID цепочки для голосования */ function createAddModuleProposal( string memory _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId ) external returns (uint256) { if (!supportedChains[_chainId]) revert ErrChainNotSupported(); if (_moduleAddress == address(0)) revert ErrZeroAddress(); if (activeModules[_moduleId]) revert ErrProposalExecuted(); if (balanceOf(msg.sender) == 0) revert ErrNotHolder(); // Операция добавления модуля bytes memory operation = abi.encodeWithSelector( bytes4(keccak256("_addModule(bytes32,address)")), _moduleId, _moduleAddress ); // Целевые сети: по умолчанию все поддерживаемые сети uint256[] memory targets = new uint256[](supportedChainIds.length); for (uint256 i = 0; i < supportedChainIds.length; i++) { targets[i] = supportedChainIds[i]; } // Таймлок больше не используется в ядре; модуль Timelock будет добавлен отдельно return _createProposalInternal( _description, _duration, operation, targets, msg.sender ); } /** * @dev Создать предложение об удалении модуля * @param _description Описание предложения * @param _duration Длительность голосования в секундах * @param _moduleId ID модуля * @param _chainId ID цепочки для голосования */ function createRemoveModuleProposal( string memory _description, uint256 _duration, bytes32 _moduleId, uint256 _chainId ) external returns (uint256) { if (!supportedChains[_chainId]) revert ErrChainNotSupported(); if (!activeModules[_moduleId]) revert ErrProposalMissing(); if (balanceOf(msg.sender) == 0) revert ErrNotHolder(); // Операция удаления модуля bytes memory operation = abi.encodeWithSelector( bytes4(keccak256("_removeModule(bytes32)")), _moduleId ); // Целевые сети: по умолчанию все поддерживаемые сети uint256[] memory targets = new uint256[](supportedChainIds.length); for (uint256 i = 0; i < supportedChainIds.length; i++) { targets[i] = supportedChainIds[i]; } // Таймлок больше не используется в ядре; модуль Timelock будет добавлен отдельно return _createProposalInternal( _description, _duration, operation, targets, msg.sender ); } // Treasury операции перенесены в TreasuryModule для экономии байт-кода /** * @dev Добавить модуль (внутренняя функция, вызывается через кворум) * @param _moduleId ID модуля * @param _moduleAddress Адрес модуля */ function _addModule(bytes32 _moduleId, address _moduleAddress) internal { if (_moduleAddress == address(0)) revert ErrZeroAddress(); if (activeModules[_moduleId]) revert ErrProposalExecuted(); modules[_moduleId] = _moduleAddress; activeModules[_moduleId] = true; emit ModuleAdded(_moduleId, _moduleAddress); } /** * @dev Удалить модуль (внутренняя функция, вызывается через кворум) * @param _moduleId ID модуля */ function _removeModule(bytes32 _moduleId) internal { if (!activeModules[_moduleId]) revert ErrProposalMissing(); delete modules[_moduleId]; activeModules[_moduleId] = false; emit ModuleRemoved(_moduleId); } /** * @dev Получить информацию о DLE */ function getDLEInfo() external view returns (DLEInfo memory) { return dleInfo; } /** * @dev Проверить, активен ли модуль * @param _moduleId ID модуля */ function isModuleActive(bytes32 _moduleId) external view returns (bool) { return activeModules[_moduleId]; } /** * @dev Получить адрес модуля * @param _moduleId ID модуля */ function getModuleAddress(bytes32 _moduleId) external view returns (address) { return modules[_moduleId]; } /** * @dev Проверить, поддерживается ли цепочка * @param _chainId ID цепочки */ function isChainSupported(uint256 _chainId) external view returns (bool) { return supportedChains[_chainId]; } /** * @dev Получить текущий ID цепочки (теперь используется block.chainid) */ function getCurrentChainId() external view returns (uint256) { return block.chainid; } /** * @dev Получить URI логотипа токена (стандартная функция для блокчейн-сканеров) * @return URI логотипа или пустую строку если не установлен */ function tokenURI() external view returns (string memory) { return logoURI; } /** * @dev Получить URI логотипа токена (альтернативная функция для блокчейн-сканеров) * @return URI логотипа или пустую строку если не установлен */ function logo() external view returns (string memory) { return logoURI; } /** * @dev Получить информацию о мультичейн развертывании для блокчейн-сканеров * @return chains Массив всех поддерживаемых chain ID (все сети равноправны) * @return defaultVotingChain ID сети по умолчанию для голосования (может быть любая из поддерживаемых) */ function getMultichainInfo() external view returns (uint256[] memory chains, uint256 defaultVotingChain) { return (supportedChainIds, block.chainid); } /** * @dev Получить адреса контракта в других сетях (для мультичейн сканеров) * @return chainIds Массив chain ID где развернут контракт * @return addresses Массив адресов контракта в соответствующих сетях */ function getMultichainAddresses() external view returns (uint256[] memory chainIds, address[] memory addresses) { uint256[] memory chains = new uint256[](supportedChainIds.length); address[] memory addrs = new address[](supportedChainIds.length); for (uint256 i = 0; i < supportedChainIds.length; i++) { chains[i] = supportedChainIds[i]; addrs[i] = address(this); // Детерминированный деплой обеспечивает одинаковые адреса } return (chains, addrs); } /** * @dev Получить мультичейн метаданные в JSON формате для блокчейн-сканеров * @return metadata JSON строка с информацией о мультичейн развертывании * * Архитектура: Single-Chain Governance - голосование происходит в одной сети, * но исполнение может быть в любой из поддерживаемых сетей через подписи. */ function getMultichainMetadata() external view returns (string memory metadata) { // Формируем JSON с информацией о мультичейн развертывании string memory json = string(abi.encodePacked( '{"multichain": {', '"supportedChains": [' )); for (uint256 i = 0; i < supportedChainIds.length; i++) { if (i > 0) { json = string(abi.encodePacked(json, ',')); } json = string(abi.encodePacked(json, _toString(supportedChainIds[i]))); } json = string(abi.encodePacked( json, '],', '"defaultVotingChain": ', _toString(block.chainid), ',', '"note": "All chains are equal, voting can happen on any supported chain",', '"contractAddress": "', _toHexString(address(this)), '"', '}}' )); return json; } /** * @dev Вспомогательная функция для конвертации uint256 в string */ function _toString(uint256 value) internal pure returns (string memory) { if (value == 0) { return "0"; } uint256 temp = value; uint256 digits; while (temp != 0) { digits++; temp /= 10; } bytes memory buffer = new bytes(digits); while (value != 0) { digits -= 1; buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); value /= 10; } return string(buffer); } /** * @dev Вспомогательная функция для конвертации address в hex string */ function _toHexString(address addr) internal pure returns (string memory) { return _toHexString(abi.encodePacked(addr)); } /** * @dev Вспомогательная функция для конвертации bytes в hex string */ function _toHexString(bytes memory data) internal pure returns (string memory) { bytes memory alphabet = "0123456789abcdef"; bytes memory str = new bytes(2 + data.length * 2); str[0] = "0"; str[1] = "x"; for (uint256 i = 0; i < data.length; i++) { str[2 + i * 2] = alphabet[uint256(uint8(data[i] >> 4))]; str[3 + i * 2] = alphabet[uint256(uint8(data[i] & 0x0f))]; } return string(str); } // API функции вынесены в отдельный reader контракт для экономии байт-кода // 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution function getProposalState(uint256 _proposalId) public view returns (uint8 state) { Proposal storage p = proposals[_proposalId]; require(p.id == _proposalId, "Proposal does not exist"); if (p.canceled) return 4; if (p.executed) return 3; (bool passed, bool quorumReached) = checkProposalResult(_proposalId); bool votingOver = block.timestamp >= p.deadline; bool ready = passed && quorumReached; if (ready) return 5; // ReadyForExecution if (passed && (votingOver || quorumReached)) return 1; // Succeeded if (votingOver && !passed) return 2; // Defeated return 0; // Pending } // Функции для подсчёта голосов вынесены в reader контракт // Получить полную сводку по предложению function getProposalSummary(uint256 _proposalId) external view returns ( uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, bool canceled, uint256 deadline, address initiator, uint256 snapshotTimepoint, uint256[] memory targetChains ) { Proposal storage p = proposals[_proposalId]; require(p.id == _proposalId, "Proposal does not exist"); return ( p.id, p.description, p.forVotes, p.againstVotes, p.executed, p.canceled, p.deadline, p.initiator, p.snapshotTimepoint, p.targetChains ); } // Деактивация вынесена в отдельный модуль. См. DeactivationModule. function isActive() external view returns (bool) { return dleInfo.isActive; } // ===== Вспомогательные функции ===== function _isTargetChain(Proposal storage p, uint256 chainId) internal view returns (bool) { for (uint256 i = 0; i < p.targetChains.length; i++) { if (p.targetChains[i] == chainId) return true; } return false; } // ===== Overrides для ERC20Votes ===== function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) { super._update(from, to, value); } // Разрешение неоднозначности nonces между ERC20Permit и Nonces function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) { return super.nonces(owner); } // Запрет делегирования на третьих лиц: разрешено только делегировать самому себе function _delegate(address delegator, address delegatee) internal override { require(delegator == delegatee, "Delegation disabled"); super._delegate(delegator, delegatee); } // ===== Блокировка прямых переводов токенов ===== // Токены DLE могут быть переведены только через governance /** * @dev Блокирует прямые переводы токенов * @return Всегда ревертится */ function transfer(address /*to*/, uint256 /*amount*/) public pure override returns (bool) { // coverage:ignore-line revert ErrTransfersDisabled(); } /** * @dev Блокирует прямые переводы токенов через approve/transferFrom * @return Всегда ревертится */ function transferFrom(address /*from*/, address /*to*/, uint256 /*amount*/) public pure override returns (bool) { // coverage:ignore-line revert ErrTransfersDisabled(); } /** * @dev Блокирует прямые разрешения на перевод токенов * @return Всегда ревертится */ function approve(address /*spender*/, uint256 /*amount*/) public pure override returns (bool) { // coverage:ignore-line revert ErrApprovalsDisabled(); } }