Files
DLE/backend/contracts/DLE.sol

523 lines
21 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);
}
}