523 lines
21 KiB
Solidity
523 lines
21 KiB
Solidity
// 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";
|
||
|
||
/**
|
||
* @title DLE (Digital Legal Entity)
|
||
* @dev Основной контракт DLE с отдельным модулем TimelockController.
|
||
*/
|
||
contract DLE is
|
||
ERC20Votes,
|
||
Governor,
|
||
GovernorSettings,
|
||
GovernorCountingSimple,
|
||
GovernorVotesQuorumFraction,
|
||
GovernorTimelockControl
|
||
{
|
||
struct DLEInfo {
|
||
string name;
|
||
string symbol;
|
||
string location;
|
||
string coordinates;
|
||
uint256 jurisdiction;
|
||
uint256 oktmo;
|
||
string[] okvedCodes;
|
||
uint256 kpp;
|
||
uint256 creationTimestamp;
|
||
bool isActive;
|
||
}
|
||
|
||
struct DLEConfig {
|
||
string name;
|
||
string symbol;
|
||
string location;
|
||
string coordinates;
|
||
uint256 jurisdiction;
|
||
uint256 oktmo;
|
||
string[] okvedCodes;
|
||
uint256 kpp;
|
||
uint48 votingDelay;
|
||
uint32 votingPeriod;
|
||
uint256 proposalThreshold;
|
||
uint256 quorumPercentage;
|
||
address[] initialPartners;
|
||
uint256[] initialAmounts;
|
||
}
|
||
|
||
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;
|
||
string description;
|
||
}
|
||
|
||
struct TreasuryProposal {
|
||
address recipient;
|
||
uint256 amount;
|
||
uint256 timelock;
|
||
address initiator;
|
||
bytes[] signatures;
|
||
bool executed;
|
||
uint256 quorumRequired;
|
||
uint256 signaturesCount;
|
||
string description;
|
||
}
|
||
|
||
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;
|
||
|
||
// Казначейские функции
|
||
mapping(address => uint256) public lastWithdrawalBlock; // Последний блок вывода для каждого адреса
|
||
uint256 public totalTreasuryBalance; // Общий баланс казны
|
||
|
||
event DLEInitialized(
|
||
string name,
|
||
string symbol,
|
||
string location,
|
||
string coordinates,
|
||
uint256 jurisdiction,
|
||
uint256 oktmo,
|
||
string[] okvedCodes,
|
||
uint256 kpp,
|
||
address tokenAddress,
|
||
address timelockAddress,
|
||
address governorAddress
|
||
);
|
||
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);
|
||
|
||
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)))
|
||
{
|
||
dleInfo = DLEInfo({
|
||
name: config.name,
|
||
symbol: config.symbol,
|
||
location: config.location,
|
||
coordinates: config.coordinates,
|
||
jurisdiction: config.jurisdiction,
|
||
oktmo: config.oktmo,
|
||
okvedCodes: config.okvedCodes,
|
||
kpp: config.kpp,
|
||
creationTimestamp: block.timestamp,
|
||
isActive: true
|
||
});
|
||
quorumPercentage = config.quorumPercentage;
|
||
|
||
// Автоматически распределяем начальные токены партнерам при деплое
|
||
require(config.initialPartners.length == config.initialAmounts.length, "Arrays length mismatch");
|
||
require(config.initialPartners.length > 0, "No initial partners");
|
||
|
||
for (uint256 i = 0; i < config.initialPartners.length; i++) {
|
||
require(config.initialPartners[i] != address(0), "Zero address");
|
||
require(config.initialAmounts[i] > 0, "Zero amount");
|
||
_mint(config.initialPartners[i], config.initialAmounts[i]);
|
||
}
|
||
|
||
initialTokensDistributed = true;
|
||
emit InitialTokensDistributed(config.initialPartners, config.initialAmounts);
|
||
|
||
emit DLEInitialized(
|
||
config.name,
|
||
config.symbol,
|
||
config.location,
|
||
config.coordinates,
|
||
config.jurisdiction,
|
||
config.oktmo,
|
||
config.okvedCodes,
|
||
config.kpp,
|
||
address(this),
|
||
timelockAddress,
|
||
address(this)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @dev Создать предложение на распределение токенов
|
||
* @param _partners Массив адресов партнеров
|
||
* @param _amounts Массив сумм токенов для каждого партнера
|
||
* @param _timelock Время исполнения (timestamp)
|
||
* @param _description Описание предложения
|
||
*/
|
||
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(
|
||
bytes memory _operation,
|
||
uint256[] memory _targetChains,
|
||
uint256 _timelock,
|
||
uint256 _governanceChain
|
||
) 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");
|
||
|
||
uint256 proposalId = treasuryProposalCounter++;
|
||
|
||
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
|
||
});
|
||
|
||
emit TreasuryProposalCreated(proposalId, msg.sender, _recipient, _amount, _description);
|
||
return proposalId;
|
||
}
|
||
|
||
/**
|
||
* @dev Подписать предложение на вывод средств из казны
|
||
* @param _proposalId ID предложения
|
||
*/
|
||
function signTreasuryProposal(uint256 _proposalId) external {
|
||
TreasuryProposal storage proposal = treasuryProposals[_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 TreasuryProposalSigned(_proposalId, msg.sender, proposal.signaturesCount);
|
||
|
||
// Проверяем, достигнут ли кворум
|
||
if (proposal.signaturesCount >= proposal.quorumRequired) {
|
||
proposal.executed = true;
|
||
_executeTreasuryProposal(_proposalId);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @dev Выполнить предложение на вывод средств из казны
|
||
* @param _proposalId ID предложения
|
||
*/
|
||
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");
|
||
|
||
totalTreasuryBalance -= proposal.amount;
|
||
_transfer(address(this), proposal.recipient, proposal.amount);
|
||
|
||
emit TreasuryProposalExecuted(_proposalId, proposal.recipient, proposal.amount);
|
||
}
|
||
|
||
/**
|
||
* @dev Выполнить предложение на вывод средств после истечения таймлока
|
||
* @param _proposalId ID предложения
|
||
*/
|
||
function executeTreasuryProposal(uint256 _proposalId) external {
|
||
TreasuryProposal storage proposal = treasuryProposals[_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;
|
||
_executeTreasuryProposal(_proposalId);
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* @dev Получить доступную для вывода сумму для адреса (пропорционально доле)
|
||
* @param _address Адрес для проверки
|
||
* @return Доступная сумма для вывода
|
||
*/
|
||
function getAvailableWithdrawal(address _address) public view returns (uint256) {
|
||
uint256 userBalance = balanceOf(_address);
|
||
if (userBalance == 0 || totalTreasuryBalance == 0) {
|
||
return 0;
|
||
}
|
||
|
||
// Пропорционально доле в общем количестве токенов
|
||
uint256 userShare = (userBalance * totalTreasuryBalance) / totalSupply();
|
||
return userShare;
|
||
}
|
||
|
||
|
||
|
||
// Переопределения для совместимости с ERC-6372
|
||
function CLOCK_MODE() public pure override(Governor, GovernorVotes, Votes) returns (string memory) {
|
||
return "mode=blocknumber&from=default";
|
||
}
|
||
function clock() public view override(Governor, GovernorVotes, Votes) returns (uint48) {
|
||
return uint48(block.number);
|
||
}
|
||
function _update(address from, address to, uint256 amount) internal override(ERC20Votes) {
|
||
super._update(from, to, amount);
|
||
}
|
||
function nonces(address owner) public view override(Nonces) returns (uint256) {
|
||
return super.nonces(owner);
|
||
}
|
||
function name() public view override(ERC20, Governor) returns (string memory) {
|
||
return super.name();
|
||
}
|
||
function votingDelay() public view override(Governor, GovernorSettings) returns (uint256) {
|
||
return super.votingDelay();
|
||
}
|
||
function votingPeriod() public view override(Governor, GovernorSettings) returns (uint256) {
|
||
return super.votingPeriod();
|
||
}
|
||
function quorum(uint256 blockNumber) public view override(Governor, GovernorVotesQuorumFraction) returns (uint256) {
|
||
return super.quorum(blockNumber);
|
||
}
|
||
function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) {
|
||
return super.state(proposalId);
|
||
}
|
||
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);
|
||
}
|
||
} |