1094 lines
46 KiB
Solidity
1094 lines
46 KiB
Solidity
// SPDX-License-Identifier: PROPRIETARY AND MIT
|
||
// Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||
// All rights reserved.
|
||
//
|
||
// This software is proprietary and confidential.
|
||
// Unauthorized copying, modification, or distribution is prohibited.
|
||
//
|
||
// For licensing inquiries: info@hb3-accelerator.com
|
||
// Website: https://hb3-accelerator.com
|
||
// GitHub: https://github.com/HB3-ACCELERATOR
|
||
pragma solidity ^0.8.20;
|
||
|
||
import "@openzeppelin/contracts/token/ERC20/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);
|
||
}
|
||
|
||
/**
|
||
* @dev Интерфейс для мультичейн метаданных (EIP-3668 inspired)
|
||
*/
|
||
interface IMultichainMetadata {
|
||
/**
|
||
* @dev Возвращает информацию о мультичейн развертывании
|
||
* @return supportedChainIds Массив всех поддерживаемых chain ID
|
||
* @return defaultVotingChain ID сети по умолчанию для голосования (может быть любая из поддерживаемых)
|
||
*/
|
||
function getMultichainInfo() external view returns (uint256[] memory supportedChainIds, uint256 defaultVotingChain);
|
||
|
||
/**
|
||
* @dev Возвращает адреса контракта в других сетях
|
||
* @return chainIds Массив chain ID где развернут контракт
|
||
* @return addresses Массив адресов контракта в соответствующих сетях
|
||
*/
|
||
function getMultichainAddresses() external view returns (uint256[] memory chainIds, address[] memory addresses);
|
||
}
|
||
|
||
/**
|
||
* @title DLE (Digital Legal Entity)
|
||
* @dev Основной контракт DLE с модульной архитектурой, Single-Chain Governance
|
||
* и безопасной мульти-чейн синхронизацией без сторонних мостов (через подписи холдеров).
|
||
*
|
||
* КЛЮЧЕВЫЕ ОСОБЕННОСТИ:
|
||
* - Прямые переводы токенов ЗАБЛОКИРОВАНЫ (transfer, transferFrom, approve)
|
||
* - Перевод токенов возможен ТОЛЬКО через governance предложения
|
||
* - Токены служат только для голосования и управления DLE
|
||
* - Все операции с токенами требуют коллективного решения
|
||
*/
|
||
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 governanceChainId; // сеть голосования (Single-Chain Governance)
|
||
uint256[] targetChains; // целевые сети для исполнения
|
||
uint256 snapshotTimepoint; // блок/временная точка для getPastVotes
|
||
mapping(address => bool) hasVoted;
|
||
}
|
||
|
||
|
||
|
||
// Основные настройки
|
||
DLEInfo public dleInfo;
|
||
uint256 public quorumPercentage;
|
||
uint256 public proposalCounter;
|
||
uint256 public currentChainId;
|
||
// Публичный 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 ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId);
|
||
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 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 ErrNoPower();
|
||
error ErrNotReady();
|
||
error ErrNotInitiator();
|
||
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,
|
||
uint256 _currentChainId,
|
||
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;
|
||
currentChainId = _currentChainId;
|
||
|
||
// Настраиваем поддерживаемые цепочки
|
||
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
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @dev Одноразовая инициализация 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);
|
||
}
|
||
|
||
/**
|
||
* @dev Создать предложение с выбором цепочки для кворума
|
||
* @param _description Описание предложения
|
||
* @param _duration Длительность голосования в секундах
|
||
* @param _operation Операция для исполнения
|
||
* @param _governanceChainId ID цепочки для сбора голосов
|
||
*/
|
||
function createProposal(
|
||
string memory _description,
|
||
uint256 _duration,
|
||
bytes memory _operation,
|
||
uint256 _governanceChainId,
|
||
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();
|
||
if (!supportedChains[_governanceChainId]) revert ErrBadChain();
|
||
// _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль
|
||
return _createProposalInternal(
|
||
_description,
|
||
_duration,
|
||
_operation,
|
||
_governanceChainId,
|
||
_targetChains,
|
||
msg.sender
|
||
);
|
||
}
|
||
|
||
function _createProposalInternal(
|
||
string memory _description,
|
||
uint256 _duration,
|
||
bytes memory _operation,
|
||
uint256 _governanceChainId,
|
||
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;
|
||
proposal.governanceChainId = _governanceChainId;
|
||
|
||
// Снимок голосов: используем прошлую точку времени, чтобы 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 ProposalGovernanceChainSet(proposalId, _governanceChainId);
|
||
emit ProposalTargetsSet(proposalId, _targetChains);
|
||
return proposalId;
|
||
}
|
||
|
||
/**
|
||
* @dev Голосовать за предложение
|
||
* @param _proposalId ID предложения
|
||
* @param _support Поддержка предложения
|
||
*/
|
||
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 (currentChainId != proposal.governanceChainId) revert ErrWrongChain();
|
||
|
||
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);
|
||
}
|
||
|
||
// УДАЛЕНО: syncVoteFromChain с MerkleProof — небезопасно без доверенного моста
|
||
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 (currentChainId != proposal.governanceChainId) revert ErrWrongChain();
|
||
|
||
(bool passed, bool quorumReached) = checkProposalResult(_proposalId);
|
||
|
||
// Предложение можно выполнить если:
|
||
// 1. Дедлайн истек ИЛИ кворум достигнут
|
||
if (!(block.timestamp >= proposal.deadline || quorumReached)) revert ErrNotReady();
|
||
if (!(passed && quorumReached)) revert ErrNotReady();
|
||
|
||
proposal.executed = true;
|
||
|
||
// Исполняем операцию
|
||
_executeOperation(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 (currentChainId == proposal.governanceChainId) revert ErrWrongChain();
|
||
if (!_isTargetChain(proposal, currentChainId)) 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,
|
||
currentChainId,
|
||
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(proposal.operation);
|
||
emit ProposalExecuted(_proposalId, proposal.operation);
|
||
emit ProposalExecutionApprovedInChain(_proposalId, currentChainId);
|
||
|
||
}
|
||
|
||
// Sync функции удалены для экономии байт-кода
|
||
|
||
// УДАЛЕНО: syncToChain — не используется в подпись‑ориентированной схеме
|
||
|
||
/**
|
||
* @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 != currentChainId, "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 != currentChainId, "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 Установить Merkle root для цепочки (только для владельцев токенов)
|
||
* @param _chainId ID цепочки
|
||
* @param _merkleRoot Merkle root для цепочки
|
||
*/
|
||
// УДАЛЕНО: setChainMerkleRoot — небезопасно отдавать любому холдеру
|
||
|
||
/**
|
||
* @dev Получить Merkle root для цепочки
|
||
* @param _chainId ID цепочки
|
||
*/
|
||
// УДАЛЕНО: getChainMerkleRoot — устарело
|
||
|
||
/**
|
||
* @dev Исполнить операцию
|
||
* @param _operation Операция для исполнения
|
||
*/
|
||
function _executeOperation(bytes memory _operation) internal {
|
||
if (_operation.length < 4) revert ErrInvalidOperation();
|
||
|
||
// Декодируем операцию из 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,uint256)"))) {
|
||
// Операция перевода токенов через governance
|
||
(address recipient, uint256 amount) = abi.decode(data, (address, uint256));
|
||
_transferTokens(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 _recipient, uint256 _amount) internal {
|
||
if (_recipient == address(0)) revert ErrZeroAddress();
|
||
if (_amount == 0) revert ErrZeroAmount();
|
||
require(balanceOf(address(this)) >= _amount, "Insufficient DLE balance");
|
||
|
||
// Переводим токены от имени DLE (address(this))
|
||
_transfer(address(this), _recipient, _amount);
|
||
|
||
emit TokensTransferredByGovernance(_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,
|
||
_chainId,
|
||
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,
|
||
_chainId,
|
||
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 цепочки
|
||
*/
|
||
function getCurrentChainId() external view returns (uint256) {
|
||
return currentChainId;
|
||
}
|
||
|
||
/**
|
||
* @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, currentChainId);
|
||
}
|
||
|
||
/**
|
||
* @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); // CREATE2 обеспечивает одинаковые адреса
|
||
}
|
||
|
||
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(currentChainId),
|
||
',',
|
||
'"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);
|
||
}
|
||
|
||
/**
|
||
* @dev Получить информацию об архитектуре мультичейн governance
|
||
* @return architecture Описание архитектуры в JSON формате
|
||
*/
|
||
function getGovernanceArchitecture() external pure returns (string memory architecture) {
|
||
return string(abi.encodePacked(
|
||
'{"architecture": {',
|
||
'"type": "Single-Chain Governance",',
|
||
'"description": "Voting happens on one chain per proposal, execution on any supported chain",',
|
||
'"features": [',
|
||
'"Equal chain support - no primary chain",',
|
||
'"Cross-chain execution via signatures",',
|
||
'"Deterministic addresses via CREATE2",',
|
||
'"No bridge dependencies"',
|
||
'],',
|
||
'"voting": "One chain per proposal (chosen by proposer)",',
|
||
'"execution": "Any supported chain via signature verification"',
|
||
'}}'
|
||
));
|
||
}
|
||
|
||
// 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 контракт
|
||
|
||
// Деактивация вынесена в отдельный модуль. См. 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();
|
||
}
|
||
}
|