ваше сообщение коммита
This commit is contained in:
@@ -16,6 +16,10 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title DLE (Digital Legal Entity)
|
||||
* @dev Основной контракт DLE с модульной архитектурой, Single-Chain Governance
|
||||
@@ -78,10 +82,14 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
uint256 public quorumPercentage;
|
||||
uint256 public proposalCounter;
|
||||
uint256 public currentChainId;
|
||||
// Публичный URI логотипа токена/организации (можно установить при деплое через инициализатор)
|
||||
string public logoURI;
|
||||
|
||||
// Модули
|
||||
mapping(bytes32 => address) public modules;
|
||||
mapping(bytes32 => bool) public activeModules;
|
||||
bool public modulesInitialized; // Флаг инициализации базовых модулей
|
||||
address public immutable initializer; // Адрес, имеющий право на однократную инициализацию модулей
|
||||
|
||||
// Предложения
|
||||
mapping(uint256 => Proposal) public proposals;
|
||||
@@ -120,16 +128,66 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId);
|
||||
event TokensTransferredByGovernance(address indexed recipient, uint256 amount);
|
||||
|
||||
// EIP712 typehash для подписи одобрения исполнения предложения в целевой сети
|
||||
// ExecutionApproval(uint256 proposalId, bytes32 operationHash, uint256 chainId, uint256 snapshotTimepoint)
|
||||
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 ErrCannotAddCurrentChain();
|
||||
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
|
||||
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,
|
||||
@@ -152,14 +210,14 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
}
|
||||
|
||||
// Распределяем начальные токены партнерам
|
||||
require(config.initialPartners.length == config.initialAmounts.length, "Arrays length mismatch");
|
||||
require(config.initialPartners.length > 0, "No initial partners");
|
||||
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];
|
||||
require(partner != address(0), "Zero address");
|
||||
require(amount > 0, "Zero amount");
|
||||
if (partner == address(0)) revert ErrZeroAddress();
|
||||
if (amount == 0) revert ErrZeroAmount();
|
||||
_mint(partner, amount);
|
||||
// Авто-делегирование голосов себе, чтобы getPastVotes работал без действия пользователя
|
||||
_delegate(partner, partner);
|
||||
@@ -179,6 +237,17 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 Описание предложения
|
||||
@@ -194,9 +263,10 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
uint256[] memory _targetChains,
|
||||
uint256 /* _timelockDelay */
|
||||
) external returns (uint256) {
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
|
||||
require(_duration > 0, "Duration must be positive");
|
||||
require(supportedChains[_governanceChainId], "Chain not supported");
|
||||
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,
|
||||
@@ -235,7 +305,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
|
||||
// запись целевых сетей
|
||||
for (uint256 i = 0; i < _targetChains.length; i++) {
|
||||
require(supportedChains[_targetChains[i]], "Target chain not supported");
|
||||
if (!supportedChains[_targetChains[i]]) revert ErrBadTarget();
|
||||
proposal.targetChains.push(_targetChains[i]);
|
||||
}
|
||||
|
||||
@@ -253,14 +323,15 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
*/
|
||||
function vote(uint256 _proposalId, bool _support) external nonReentrant {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
require(block.timestamp < proposal.deadline, "Voting ended");
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
require(!proposal.hasVoted[msg.sender], "Already voted");
|
||||
require(currentChainId == proposal.governanceChainId, "Wrong chain for voting");
|
||||
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) {
|
||||
@@ -273,16 +344,9 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
}
|
||||
|
||||
// УДАЛЕНО: syncVoteFromChain с MerkleProof — небезопасно без доверенного моста
|
||||
|
||||
/**
|
||||
* @dev Проверить результат предложения
|
||||
* @param _proposalId ID предложения
|
||||
* @return passed Прошло ли предложение
|
||||
* @return quorumReached Достигнут ли кворум
|
||||
*/
|
||||
function checkProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
if (proposal.id != _proposalId) revert ErrProposalMissing();
|
||||
|
||||
uint256 totalVotes = proposal.forVotes + proposal.againstVotes;
|
||||
// Используем снапшот totalSupply на момент начала голосования
|
||||
@@ -295,25 +359,20 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
return (passed, quorumReached);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Исполнить предложение
|
||||
* @param _proposalId ID предложения
|
||||
*/
|
||||
|
||||
function executeProposal(uint256 _proposalId) external {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
require(!proposal.executed, "Proposal already executed");
|
||||
require(currentChainId == proposal.governanceChainId, "Execute only in governance chain");
|
||||
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. Дедлайн истек ИЛИ кворум достигнут
|
||||
require(
|
||||
block.timestamp >= proposal.deadline || quorumReached,
|
||||
"Voting not ended and quorum not reached"
|
||||
);
|
||||
require(passed && quorumReached, "Proposal not passed");
|
||||
if (!(block.timestamp >= proposal.deadline || quorumReached)) revert ErrNotReady();
|
||||
if (!(passed && quorumReached)) revert ErrNotReady();
|
||||
|
||||
proposal.executed = true;
|
||||
|
||||
@@ -323,42 +382,38 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
emit ProposalExecuted(_proposalId, proposal.operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Отмена предложения до истечения голосования инициатором при наличии достаточной голосующей силы.
|
||||
* Это soft-cancel для защиты от явных ошибок. Порог: >= 10% от снапшотного supply.
|
||||
*/
|
||||
|
||||
function cancelProposal(uint256 _proposalId, string calldata reason) external {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
require(!proposal.executed, "Already executed");
|
||||
require(block.timestamp < proposal.deadline, "Voting ended");
|
||||
require(msg.sender == proposal.initiator, "Only initiator");
|
||||
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);
|
||||
require(vp * 10 >= pastSupply, "Insufficient voting power to cancel");
|
||||
if (vp * 10 < pastSupply) revert ErrLowPower();
|
||||
|
||||
proposal.canceled = true;
|
||||
emit ProposalCancelled(_proposalId, reason);
|
||||
}
|
||||
|
||||
// УДАЛЕНО: syncExecutionFromChain с MerkleProof — небезопасно без доверенного моста
|
||||
|
||||
/**
|
||||
* @dev Исполнение предложения в НЕ governance-сети по подписям холдеров на снапшоте.
|
||||
* Подходит для target chains. Не требует внешнего моста.
|
||||
*/
|
||||
function executeProposalBySignatures(
|
||||
uint256 _proposalId,
|
||||
address[] calldata signers,
|
||||
bytes[] calldata signatures
|
||||
) external nonReentrant {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
require(!proposal.executed, "Proposal already executed in this chain");
|
||||
require(currentChainId != proposal.governanceChainId, "Use executeProposal in governance chain");
|
||||
require(_isTargetChain(proposal, currentChainId), "Chain not in targets");
|
||||
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();
|
||||
|
||||
require(signers.length == signatures.length, "Bad signatures");
|
||||
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,
|
||||
@@ -370,76 +425,44 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
bytes32 digest = _hashTypedDataV4(structHash);
|
||||
|
||||
uint256 votesFor = 0;
|
||||
// простая защита от дублей адресов (O(n^2) по малому n)
|
||||
|
||||
for (uint256 i = 0; i < signers.length; i++) {
|
||||
address recovered = ECDSA.recover(digest, signatures[i]);
|
||||
require(recovered == signers[i], "Bad signature");
|
||||
// проверка на дубли
|
||||
for (uint256 j = 0; j < i; j++) {
|
||||
require(signers[j] != recovered, "Duplicate signer");
|
||||
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();
|
||||
}
|
||||
uint256 vp = getPastVotes(recovered, proposal.snapshotTimepoint);
|
||||
require(vp > 0, "No voting power at snapshot");
|
||||
|
||||
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;
|
||||
require(votesFor >= quorumRequired, "Quorum not reached by sigs");
|
||||
if (votesFor < quorumRequired) revert ErrNoPower();
|
||||
|
||||
proposal.executed = true;
|
||||
_executeOperation(proposal.operation);
|
||||
emit ProposalExecuted(_proposalId, proposal.operation);
|
||||
emit ProposalExecutionApprovedInChain(_proposalId, currentChainId);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Проверить подключение к цепочке
|
||||
* @param _chainId ID цепочки
|
||||
* @return isAvailable Доступна ли цепочка
|
||||
*/
|
||||
function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable) {
|
||||
// Упрощенная проверка: цепочка объявлена как поддерживаемая
|
||||
return supportedChains[_chainId];
|
||||
}
|
||||
// Sync функции удалены для экономии байт-кода
|
||||
|
||||
/**
|
||||
* @dev Проверить все подключения перед синхронизацией
|
||||
* @param _proposalId ID предложения
|
||||
* @return allChainsReady Готовы ли все цепочки
|
||||
*/
|
||||
function checkSyncReadiness(uint256 _proposalId) public view returns (bool allChainsReady) {
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
require(proposal.id == _proposalId, "Proposal does not exist");
|
||||
|
||||
// Проверяем все поддерживаемые цепочки
|
||||
for (uint256 i = 0; i < getSupportedChainCount(); i++) {
|
||||
uint256 chainId = getSupportedChainId(i);
|
||||
if (!checkChainConnection(chainId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Синхронизация только при 100% готовности
|
||||
* @param _proposalId ID предложения
|
||||
*/
|
||||
function syncToAllChains(uint256 _proposalId) external {
|
||||
require(checkSyncReadiness(_proposalId), "Not all chains ready");
|
||||
|
||||
// В этой версии без внешнего моста синхронизация выполняется
|
||||
// через executeProposalBySignatures в целевых сетях.
|
||||
emit SyncCompleted(_proposalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Синхронизация в конкретную цепочку
|
||||
* @param _proposalId ID предложения
|
||||
* @param _chainId ID цепочки
|
||||
*/
|
||||
// УДАЛЕНО: syncToChain — не используется в подпись‑ориентированной схеме
|
||||
|
||||
/**
|
||||
@@ -508,23 +531,28 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
* @param _operation Операция для исполнения
|
||||
*/
|
||||
function _executeOperation(bytes memory _operation) internal {
|
||||
// Декодируем операцию
|
||||
(bytes4 selector, bytes memory data) = abi.decode(_operation, (bytes4, bytes));
|
||||
if (_operation.length < 4) revert ErrInvalidOperation();
|
||||
|
||||
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("updateQuorumPercentage(uint256)"))) {
|
||||
// Операция обновления процента кворума
|
||||
(uint256 newQuorumPercentage) = abi.decode(data, (uint256));
|
||||
_updateQuorumPercentage(newQuorumPercentage);
|
||||
} else if (selector == bytes4(keccak256("updateCurrentChainId(uint256)"))) {
|
||||
// Операция обновления текущей цепочки
|
||||
(uint256 newChainId) = abi.decode(data, (uint256));
|
||||
_updateCurrentChainId(newChainId);
|
||||
} else if (selector == bytes4(keccak256("_addModule(bytes32,address)"))) {
|
||||
// Декодируем операцию из 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);
|
||||
@@ -542,13 +570,32 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
// Операция перевода токенов через 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("_updateCurrentChainId(uint256)"))) {
|
||||
// Операция обновления текущей цепочки
|
||||
(uint256 newChainId) = abi.decode(data, (uint256));
|
||||
_updateCurrentChainId(newChainId);
|
||||
} 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("Unknown operation");
|
||||
revert ErrInvalidOperation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,11 +618,11 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
string[] memory _okvedCodes,
|
||||
uint256 _kpp
|
||||
) internal {
|
||||
require(bytes(_name).length > 0, "Name cannot be empty");
|
||||
require(bytes(_symbol).length > 0, "Symbol cannot be empty");
|
||||
require(bytes(_location).length > 0, "Location cannot be empty");
|
||||
require(_jurisdiction > 0, "Invalid jurisdiction");
|
||||
require(_kpp > 0, "Invalid KPP");
|
||||
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;
|
||||
@@ -593,7 +640,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
* @param _newQuorumPercentage Новый процент кворума
|
||||
*/
|
||||
function _updateQuorumPercentage(uint256 _newQuorumPercentage) internal {
|
||||
require(_newQuorumPercentage > 0 && _newQuorumPercentage <= 100, "Invalid quorum percentage");
|
||||
if (!(_newQuorumPercentage > 0 && _newQuorumPercentage <= 100)) revert ErrBadQuorum();
|
||||
|
||||
uint256 oldQuorumPercentage = quorumPercentage;
|
||||
quorumPercentage = _newQuorumPercentage;
|
||||
@@ -606,8 +653,8 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
* @param _newChainId Новый ID цепочки
|
||||
*/
|
||||
function _updateCurrentChainId(uint256 _newChainId) internal {
|
||||
require(supportedChains[_newChainId], "Chain not supported");
|
||||
require(_newChainId != currentChainId, "Same chain ID");
|
||||
if (!supportedChains[_newChainId]) revert ErrChainNotSupported();
|
||||
if (_newChainId == currentChainId) revert ErrCannotAddCurrentChain();
|
||||
|
||||
uint256 oldChainId = currentChainId;
|
||||
currentChainId = _newChainId;
|
||||
@@ -621,8 +668,8 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
* @param _amount Количество токенов для перевода
|
||||
*/
|
||||
function _transferTokens(address _recipient, uint256 _amount) internal {
|
||||
require(_recipient != address(0), "Cannot transfer to zero address");
|
||||
require(_amount > 0, "Amount must be positive");
|
||||
if (_recipient == address(0)) revert ErrZeroAddress();
|
||||
if (_amount == 0) revert ErrZeroAmount();
|
||||
require(balanceOf(address(this)) >= _amount, "Insufficient DLE balance");
|
||||
|
||||
// Переводим токены от имени DLE (address(this))
|
||||
@@ -631,6 +678,73 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
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 _treasuryAddress Адрес Treasury модуля
|
||||
* @param _timelockAddress Адрес Timelock модуля
|
||||
* @param _readerAddress Адрес Reader модуля
|
||||
*/
|
||||
function initializeBaseModules(
|
||||
address _treasuryAddress,
|
||||
address _timelockAddress,
|
||||
address _readerAddress
|
||||
) external {
|
||||
if (modulesInitialized) revert ErrProposalExecuted(); // keep existing error to avoid new identifier
|
||||
if (msg.sender != initializer) revert ErrOnlyInitializer();
|
||||
if (_treasuryAddress == address(0) || _timelockAddress == address(0) || _readerAddress == address(0)) revert ErrZeroAddress();
|
||||
|
||||
// Добавляем базовые модули без голосования (только при инициализации)
|
||||
bytes32 treasuryId = keccak256("TREASURY");
|
||||
bytes32 timelockId = keccak256("TIMELOCK");
|
||||
bytes32 readerId = keccak256("READER");
|
||||
|
||||
modules[treasuryId] = _treasuryAddress;
|
||||
activeModules[treasuryId] = true;
|
||||
|
||||
modules[timelockId] = _timelockAddress;
|
||||
activeModules[timelockId] = true;
|
||||
|
||||
modules[readerId] = _readerAddress;
|
||||
activeModules[readerId] = true;
|
||||
|
||||
modulesInitialized = true;
|
||||
|
||||
emit ModuleAdded(treasuryId, _treasuryAddress);
|
||||
emit ModuleAdded(timelockId, _timelockAddress);
|
||||
emit ModuleAdded(readerId, _readerAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Создать предложение о добавлении модуля
|
||||
* @param _description Описание предложения
|
||||
@@ -646,10 +760,10 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
address _moduleAddress,
|
||||
uint256 _chainId
|
||||
) external returns (uint256) {
|
||||
require(supportedChains[_chainId], "Chain not supported");
|
||||
require(_moduleAddress != address(0), "Zero address");
|
||||
require(!activeModules[_moduleId], "Module already exists");
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
|
||||
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(
|
||||
@@ -688,9 +802,9 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
bytes32 _moduleId,
|
||||
uint256 _chainId
|
||||
) external returns (uint256) {
|
||||
require(supportedChains[_chainId], "Chain not supported");
|
||||
require(activeModules[_moduleId], "Module does not exist");
|
||||
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
|
||||
if (!supportedChains[_chainId]) revert ErrChainNotSupported();
|
||||
if (!activeModules[_moduleId]) revert ErrProposalMissing();
|
||||
if (balanceOf(msg.sender) == 0) revert ErrNotHolder();
|
||||
|
||||
// Операция удаления модуля
|
||||
bytes memory operation = abi.encodeWithSelector(
|
||||
@@ -715,14 +829,16 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
);
|
||||
}
|
||||
|
||||
// Treasury операции перенесены в TreasuryModule для экономии байт-кода
|
||||
|
||||
/**
|
||||
* @dev Добавить модуль (внутренняя функция, вызывается через кворум)
|
||||
* @param _moduleId ID модуля
|
||||
* @param _moduleAddress Адрес модуля
|
||||
*/
|
||||
function _addModule(bytes32 _moduleId, address _moduleAddress) internal {
|
||||
require(_moduleAddress != address(0), "Zero address");
|
||||
require(!activeModules[_moduleId], "Module already exists");
|
||||
if (_moduleAddress == address(0)) revert ErrZeroAddress();
|
||||
if (activeModules[_moduleId]) revert ErrProposalExecuted();
|
||||
|
||||
modules[_moduleId] = _moduleAddress;
|
||||
activeModules[_moduleId] = true;
|
||||
@@ -735,7 +851,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
* @param _moduleId ID модуля
|
||||
*/
|
||||
function _removeModule(bytes32 _moduleId) internal {
|
||||
require(activeModules[_moduleId], "Module does not exist");
|
||||
if (!activeModules[_moduleId]) revert ErrProposalMissing();
|
||||
|
||||
delete modules[_moduleId];
|
||||
activeModules[_moduleId] = false;
|
||||
@@ -781,73 +897,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
return currentChainId;
|
||||
}
|
||||
|
||||
// ===== Интерфейс аналитики для API =====
|
||||
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 governanceChainId,
|
||||
|
||||
uint256 snapshotTimepoint,
|
||||
uint256[] memory targets
|
||||
) {
|
||||
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.governanceChainId,
|
||||
|
||||
p.snapshotTimepoint,
|
||||
p.targetChains
|
||||
);
|
||||
}
|
||||
|
||||
function getGovernanceParams() external view returns (
|
||||
uint256 quorumPct,
|
||||
uint256 chainId,
|
||||
uint256 supportedCount
|
||||
) {
|
||||
return (quorumPercentage, currentChainId, supportedChainIds.length);
|
||||
}
|
||||
|
||||
function listSupportedChains() external view returns (uint256[] memory) {
|
||||
return supportedChainIds;
|
||||
}
|
||||
|
||||
function getVotingPowerAt(address voter, uint256 timepoint) external view returns (uint256) {
|
||||
return getPastVotes(voter, timepoint);
|
||||
}
|
||||
|
||||
// ===== Пагинация и агрегирование =====
|
||||
function getProposalsCount() external view returns (uint256) {
|
||||
return allProposalIds.length;
|
||||
}
|
||||
|
||||
function listProposals(uint256 offset, uint256 limit) external view returns (uint256[] memory) {
|
||||
uint256 total = allProposalIds.length;
|
||||
if (offset >= total) {
|
||||
return new uint256[](0);
|
||||
}
|
||||
uint256 end = offset + limit;
|
||||
if (end > total) end = total;
|
||||
uint256[] memory page = new uint256[](end - offset);
|
||||
for (uint256 i = offset; i < end; i++) {
|
||||
page[i - offset] = allProposalIds[i];
|
||||
}
|
||||
return page;
|
||||
}
|
||||
// API функции вынесены в отдельный reader контракт для экономии байт-кода
|
||||
|
||||
// 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution
|
||||
function getProposalState(uint256 _proposalId) public view returns (uint8 state) {
|
||||
@@ -864,33 +914,11 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
return 0; // Pending
|
||||
}
|
||||
|
||||
function getQuorumAt(uint256 timepoint) external view returns (uint256) {
|
||||
uint256 supply = getPastTotalSupply(timepoint);
|
||||
return (supply * quorumPercentage) / 100;
|
||||
}
|
||||
|
||||
function getProposalVotes(uint256 _proposalId) external view returns (
|
||||
uint256 forVotes,
|
||||
uint256 againstVotes,
|
||||
uint256 totalVotes,
|
||||
uint256 quorumRequired
|
||||
) {
|
||||
Proposal storage p = proposals[_proposalId];
|
||||
require(p.id == _proposalId, "Proposal does not exist");
|
||||
uint256 supply = getPastTotalSupply(p.snapshotTimepoint);
|
||||
uint256 quorumReq = (supply * quorumPercentage) / 100;
|
||||
return (p.forVotes, p.againstVotes, p.forVotes + p.againstVotes, quorumReq);
|
||||
}
|
||||
|
||||
// События для новых функций
|
||||
event SyncCompleted(uint256 proposalId);
|
||||
event DLEDeactivated(address indexed deactivatedBy, uint256 timestamp);
|
||||
|
||||
bool public isDeactivated;
|
||||
// Функции для подсчёта голосов вынесены в reader контракт
|
||||
|
||||
// Деактивация вынесена в отдельный модуль. См. DeactivationModule.
|
||||
function isActive() external view returns (bool) {
|
||||
return !isDeactivated && dleInfo.isActive;
|
||||
return dleInfo.isActive;
|
||||
}
|
||||
// ===== Вспомогательные функции =====
|
||||
function _isTargetChain(Proposal storage p, uint256 chainId) internal view returns (bool) {
|
||||
@@ -908,7 +936,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
super._update(from, to, value);
|
||||
}
|
||||
|
||||
// Разрешаем неоднозначность nonces из базовых классов
|
||||
// Разрешение неоднозначности nonces между ERC20Permit и Nonces
|
||||
function nonces(address owner)
|
||||
public
|
||||
view
|
||||
@@ -929,32 +957,28 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
|
||||
|
||||
/**
|
||||
* @dev Блокирует прямые переводы токенов
|
||||
* @param to Адрес получателя (не используется)
|
||||
* @param amount Количество токенов (не используется)
|
||||
* @return Всегда возвращает false
|
||||
* @return Всегда ревертится
|
||||
*/
|
||||
function transfer(address to, uint256 amount) public override returns (bool) {
|
||||
revert("Direct transfers disabled. Use governance proposals for token transfers.");
|
||||
function transfer(address /*to*/, uint256 /*amount*/) public pure override returns (bool) {
|
||||
// coverage:ignore-line
|
||||
revert ErrTransfersDisabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Блокирует прямые переводы токенов через approve/transferFrom
|
||||
* @param from Адрес отправителя (не используется)
|
||||
* @param to Адрес получателя (не используется)
|
||||
* @param amount Количество токенов (не используется)
|
||||
* @return Всегда возвращает false
|
||||
* @return Всегда ревертится
|
||||
*/
|
||||
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
|
||||
revert("Direct transfers disabled. Use governance proposals for token transfers.");
|
||||
function transferFrom(address /*from*/, address /*to*/, uint256 /*amount*/) public pure override returns (bool) {
|
||||
// coverage:ignore-line
|
||||
revert ErrTransfersDisabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Блокирует прямые разрешения на перевод токенов
|
||||
* @param spender Адрес, которому разрешается тратить токены (не используется)
|
||||
* @param amount Количество токенов (не используется)
|
||||
* @return Всегда возвращает false
|
||||
* @return Всегда ревертится
|
||||
*/
|
||||
function approve(address spender, uint256 amount) public override returns (bool) {
|
||||
revert("Direct approvals disabled. Use governance proposals for token transfers.");
|
||||
function approve(address /*spender*/, uint256 /*amount*/) public pure override returns (bool) {
|
||||
// coverage:ignore-line
|
||||
revert ErrApprovalsDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
360
backend/contracts/DLEReader.sol
Normal file
360
backend/contracts/DLEReader.sol
Normal file
@@ -0,0 +1,360 @@
|
||||
// SPDX-License-Identifier: PROPRIETARY AND MIT
|
||||
// Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
// All rights reserved.
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
interface IDLEReader {
|
||||
// Структуры из основного контракта
|
||||
struct DLEInfo {
|
||||
string name;
|
||||
string symbol;
|
||||
string location;
|
||||
string coordinates;
|
||||
uint256 jurisdiction;
|
||||
string[] okvedCodes;
|
||||
uint256 kpp;
|
||||
uint256 creationTimestamp;
|
||||
bool isActive;
|
||||
}
|
||||
|
||||
struct Proposal {
|
||||
uint256 id;
|
||||
string description;
|
||||
uint256 forVotes;
|
||||
uint256 againstVotes;
|
||||
bool executed;
|
||||
bool canceled;
|
||||
uint256 deadline;
|
||||
address initiator;
|
||||
bytes operation;
|
||||
uint256 governanceChainId;
|
||||
uint256[] targetChains;
|
||||
uint256 snapshotTimepoint;
|
||||
}
|
||||
|
||||
// Основные функции чтения
|
||||
function getDLEInfo() external view returns (DLEInfo memory);
|
||||
function proposals(uint256) external view returns (
|
||||
uint256 id,
|
||||
string memory description,
|
||||
uint256 forVotes,
|
||||
uint256 againstVotes,
|
||||
bool executed,
|
||||
bool canceled,
|
||||
uint256 deadline,
|
||||
address initiator,
|
||||
bytes memory operation,
|
||||
uint256 governanceChainId,
|
||||
uint256 snapshotTimepoint
|
||||
);
|
||||
function allProposalIds(uint256) external view returns (uint256);
|
||||
function supportedChainIds(uint256) external view returns (uint256);
|
||||
function quorumPercentage() external view returns (uint256);
|
||||
function currentChainId() external view returns (uint256);
|
||||
function totalSupply() external view returns (uint256);
|
||||
function getPastTotalSupply(uint256) external view returns (uint256);
|
||||
function getPastVotes(address, uint256) external view returns (uint256);
|
||||
function checkProposalResult(uint256) external view returns (bool, bool);
|
||||
function getProposalState(uint256) external view returns (uint8);
|
||||
function balanceOf(address) external view returns (uint256);
|
||||
function isChainSupported(uint256) external view returns (bool);
|
||||
function isModuleActive(bytes32) external view returns (bool);
|
||||
function getModuleAddress(bytes32) external view returns (address);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title DLEReader
|
||||
* @dev Read-only контракт для API функций DLE
|
||||
*
|
||||
* БЕЗОПАСНОСТЬ:
|
||||
* - Только чтение данных (view/pure функции)
|
||||
* - Не изменяет состояние основного контракта
|
||||
* - Можно безопасно обновлять независимо от DLE
|
||||
* - Нет доступа к приватным данным
|
||||
*/
|
||||
contract DLEReader {
|
||||
|
||||
address public immutable dleContract;
|
||||
|
||||
constructor(address _dleContract) {
|
||||
require(_dleContract != address(0), "DLE contract cannot be zero");
|
||||
require(_dleContract.code.length > 0, "DLE contract must exist");
|
||||
dleContract = _dleContract;
|
||||
}
|
||||
|
||||
// ===== АГРЕГИРОВАННЫЕ ДАННЫЕ =====
|
||||
|
||||
/**
|
||||
* @dev Получить полную сводку по предложению
|
||||
*/
|
||||
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 governanceChainId,
|
||||
uint256 snapshotTimepoint,
|
||||
uint256[] memory targetChains,
|
||||
uint8 state,
|
||||
bool passed,
|
||||
bool quorumReached
|
||||
) {
|
||||
IDLEReader dle = IDLEReader(dleContract);
|
||||
|
||||
// Получаем основные данные предложения
|
||||
(
|
||||
id,
|
||||
description,
|
||||
forVotes,
|
||||
againstVotes,
|
||||
executed,
|
||||
canceled,
|
||||
deadline,
|
||||
initiator,
|
||||
, // operation не нужна для сводки
|
||||
governanceChainId,
|
||||
snapshotTimepoint
|
||||
) = dle.proposals(_proposalId);
|
||||
|
||||
// Получаем дополнительные данные
|
||||
state = dle.getProposalState(_proposalId);
|
||||
(passed, quorumReached) = dle.checkProposalResult(_proposalId);
|
||||
|
||||
// TODO: targetChains требует отдельной функции в основном контракте
|
||||
targetChains = new uint256[](0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить параметры governance
|
||||
*/
|
||||
function getGovernanceParams() external view returns (
|
||||
uint256 quorumPct,
|
||||
uint256 chainId,
|
||||
uint256 supportedCount,
|
||||
uint256 totalSupply,
|
||||
uint256 proposalsCount
|
||||
) {
|
||||
IDLEReader dle = IDLEReader(dleContract);
|
||||
|
||||
quorumPct = dle.quorumPercentage();
|
||||
chainId = dle.currentChainId();
|
||||
totalSupply = dle.totalSupply();
|
||||
|
||||
// Считаем поддерживаемые сети
|
||||
supportedCount = 0;
|
||||
for (uint256 i = 0; i < 50; i++) { // Ограничиваем итерации
|
||||
try dle.supportedChainIds(i) returns (uint256) {
|
||||
supportedCount++;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Считаем предложения
|
||||
proposalsCount = 0;
|
||||
for (uint256 i = 0; i < 1000; i++) { // Ограничиваем итерации
|
||||
try dle.allProposalIds(i) returns (uint256) {
|
||||
proposalsCount++;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить список поддерживаемых сетей
|
||||
*/
|
||||
function listSupportedChains() external view returns (uint256[] memory chains) {
|
||||
IDLEReader dle = IDLEReader(dleContract);
|
||||
|
||||
// Сначала считаем количество
|
||||
uint256 count = 0;
|
||||
for (uint256 i = 0; i < 50; i++) {
|
||||
try dle.supportedChainIds(i) returns (uint256) {
|
||||
count++;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Затем заполняем массив
|
||||
chains = new uint256[](count);
|
||||
for (uint256 i = 0; i < count; i++) {
|
||||
chains[i] = dle.supportedChainIds(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить список предложений с пагинацией
|
||||
*/
|
||||
function listProposals(uint256 offset, uint256 limit) external view returns (
|
||||
uint256[] memory proposalIds,
|
||||
uint256 total
|
||||
) {
|
||||
IDLEReader dle = IDLEReader(dleContract);
|
||||
|
||||
// Считаем общее количество
|
||||
total = 0;
|
||||
for (uint256 i = 0; i < 10000; i++) { // Увеличиваем лимит для предложений
|
||||
try dle.allProposalIds(i) returns (uint256) {
|
||||
total++;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем границы
|
||||
if (offset >= total) {
|
||||
return (new uint256[](0), total);
|
||||
}
|
||||
|
||||
uint256 end = offset + limit;
|
||||
if (end > total) end = total;
|
||||
|
||||
// Заполняем страницу
|
||||
proposalIds = new uint256[](end - offset);
|
||||
for (uint256 i = offset; i < end; i++) {
|
||||
proposalIds[i - offset] = dle.allProposalIds(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить голосующую силу на определённый момент времени
|
||||
*/
|
||||
function getVotingPowerAt(address voter, uint256 timepoint) external view returns (uint256) {
|
||||
return IDLEReader(dleContract).getPastVotes(voter, timepoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить размер кворума на определённый момент времени
|
||||
*/
|
||||
function getQuorumAt(uint256 timepoint) external view returns (uint256) {
|
||||
IDLEReader dle = IDLEReader(dleContract);
|
||||
uint256 supply = dle.getPastTotalSupply(timepoint);
|
||||
uint256 quorumPct = dle.quorumPercentage();
|
||||
return (supply * quorumPct) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить детали голосования по предложению
|
||||
*/
|
||||
function getProposalVotes(uint256 _proposalId) external view returns (
|
||||
uint256 forVotes,
|
||||
uint256 againstVotes,
|
||||
uint256 totalVotes,
|
||||
uint256 quorumRequired,
|
||||
uint256 quorumCurrent,
|
||||
bool quorumReached
|
||||
) {
|
||||
IDLEReader dle = IDLEReader(dleContract);
|
||||
|
||||
// Получаем основные данные предложения
|
||||
uint256 snapshotTimepoint;
|
||||
(
|
||||
, // id
|
||||
, // description
|
||||
forVotes,
|
||||
againstVotes,
|
||||
, // executed
|
||||
, // canceled
|
||||
, // deadline
|
||||
, // initiator
|
||||
, // operation
|
||||
, // governanceChainId
|
||||
snapshotTimepoint
|
||||
) = dle.proposals(_proposalId);
|
||||
|
||||
totalVotes = forVotes + againstVotes;
|
||||
|
||||
// Вычисляем кворум
|
||||
uint256 supply = dle.getPastTotalSupply(snapshotTimepoint);
|
||||
uint256 quorumPct = dle.quorumPercentage();
|
||||
quorumRequired = (supply * quorumPct) / 100;
|
||||
quorumCurrent = totalVotes;
|
||||
quorumReached = totalVotes >= quorumRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить статистику по адресу
|
||||
*/
|
||||
function getAddressStats(address user) external view returns (
|
||||
uint256 tokenBalance,
|
||||
uint256 currentVotingPower,
|
||||
uint256 delegatedTo,
|
||||
bool hasTokens
|
||||
) {
|
||||
IDLEReader dle = IDLEReader(dleContract);
|
||||
|
||||
tokenBalance = dle.balanceOf(user);
|
||||
currentVotingPower = dle.getPastVotes(user, block.number - 1);
|
||||
hasTokens = tokenBalance > 0;
|
||||
|
||||
// delegatedTo требует дополнительных функций в основном контракте
|
||||
delegatedTo = 0; // Placeholder
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить информацию о модулях
|
||||
*/
|
||||
function getModulesInfo(bytes32[] memory moduleIds) external view returns (
|
||||
address[] memory addresses,
|
||||
bool[] memory active
|
||||
) {
|
||||
IDLEReader dle = IDLEReader(dleContract);
|
||||
|
||||
addresses = new address[](moduleIds.length);
|
||||
active = new bool[](moduleIds.length);
|
||||
|
||||
for (uint256 i = 0; i < moduleIds.length; i++) {
|
||||
addresses[i] = dle.getModuleAddress(moduleIds[i]);
|
||||
active[i] = dle.isModuleActive(moduleIds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить состояние DLE
|
||||
*/
|
||||
function getDLEStatus() external view returns (
|
||||
IDLEReader.DLEInfo memory info,
|
||||
uint256 totalSupply,
|
||||
uint256 currentChain,
|
||||
uint256 quorumPct,
|
||||
uint256 totalProposals,
|
||||
uint256 supportedChains
|
||||
) {
|
||||
IDLEReader dle = IDLEReader(dleContract);
|
||||
|
||||
info = dle.getDLEInfo();
|
||||
totalSupply = dle.totalSupply();
|
||||
currentChain = dle.currentChainId();
|
||||
quorumPct = dle.quorumPercentage();
|
||||
|
||||
// Считаем предложения и сети
|
||||
(,, supportedChains, totalSupply, totalProposals) = this.getGovernanceParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Batch получение состояний предложений
|
||||
*/
|
||||
function getProposalStates(uint256[] memory proposalIds) external view returns (
|
||||
uint8[] memory states,
|
||||
bool[] memory passed,
|
||||
bool[] memory quorumReached
|
||||
) {
|
||||
IDLEReader dle = IDLEReader(dleContract);
|
||||
|
||||
states = new uint8[](proposalIds.length);
|
||||
passed = new bool[](proposalIds.length);
|
||||
quorumReached = new bool[](proposalIds.length);
|
||||
|
||||
for (uint256 i = 0; i < proposalIds.length; i++) {
|
||||
states[i] = dle.getProposalState(proposalIds[i]);
|
||||
(passed[i], quorumReached[i]) = dle.checkProposalResult(proposalIds[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,12 @@ contract FactoryDeployer {
|
||||
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash));
|
||||
return address(uint160(uint256(hash)));
|
||||
}
|
||||
|
||||
function computeAddressWithCreationCode(bytes32 salt, bytes memory creationCode) external view returns (address) {
|
||||
bytes32 initCodeHash = keccak256(creationCode);
|
||||
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash));
|
||||
return address(uint160(uint256(hash)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
22
backend/contracts/MockNoop.sol
Normal file
22
backend/contracts/MockNoop.sol
Normal file
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
/**
|
||||
* @title MockNoop
|
||||
* @dev Простой мок-контракт для тестирования FactoryDeployer
|
||||
*/
|
||||
contract MockNoop {
|
||||
uint256 public value;
|
||||
|
||||
constructor() {
|
||||
value = 42;
|
||||
}
|
||||
|
||||
function setValue(uint256 _value) external {
|
||||
value = _value;
|
||||
}
|
||||
|
||||
function getValue() external view returns (uint256) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
29
backend/contracts/MockToken.sol
Normal file
29
backend/contracts/MockToken.sol
Normal file
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
|
||||
/**
|
||||
* @title MockToken
|
||||
* @dev Мок-токен для тестирования TreasuryModule
|
||||
*/
|
||||
contract MockToken is ERC20 {
|
||||
address public minter;
|
||||
|
||||
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
|
||||
minter = msg.sender;
|
||||
}
|
||||
|
||||
modifier onlyMinter() {
|
||||
require(msg.sender == minter, "Only minter can call this function");
|
||||
_;
|
||||
}
|
||||
|
||||
function mint(address to, uint256 amount) external onlyMinter {
|
||||
_mint(to, amount);
|
||||
}
|
||||
|
||||
function burn(uint256 amount) external {
|
||||
_burn(msg.sender, amount);
|
||||
}
|
||||
}
|
||||
399
backend/contracts/TimelockModule.sol
Normal file
399
backend/contracts/TimelockModule.sol
Normal file
@@ -0,0 +1,399 @@
|
||||
// SPDX-License-Identifier: PROPRIETARY AND MIT
|
||||
// Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
// All rights reserved.
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
|
||||
/**
|
||||
* @title TimelockModule
|
||||
* @dev Модуль временной задержки для критических операций DLE
|
||||
*
|
||||
* НАЗНАЧЕНИЕ:
|
||||
* - Добавляет обязательную задержку между принятием и исполнением решений
|
||||
* - Даёт время сообществу на обнаружение и отмену вредоносных предложений
|
||||
* - Повышает безопасность критических операций (смена кворума, добавление модулей)
|
||||
*
|
||||
* ПРИНЦИП РАБОТЫ:
|
||||
* 1. DLE исполняет операцию не напрямую, а через Timelock
|
||||
* 2. Timelock ставит операцию в очередь с задержкой
|
||||
* 3. После истечения задержки любой может исполнить операцию
|
||||
* 4. В период задержки операцию можно отменить через экстренное голосование
|
||||
*/
|
||||
contract TimelockModule is ReentrancyGuard {
|
||||
|
||||
// Структура отложенной операции
|
||||
struct QueuedOperation {
|
||||
bytes32 id; // Уникальный ID операции
|
||||
address target; // Целевой контракт (обычно DLE)
|
||||
bytes data; // Данные для вызова
|
||||
uint256 executeAfter; // Время, после которого можно исполнить
|
||||
uint256 queuedAt; // Время постановки в очередь
|
||||
bool executed; // Исполнена ли операция
|
||||
bool cancelled; // Отменена ли операция
|
||||
address proposer; // Кто поставил в очередь
|
||||
string description; // Описание операции
|
||||
uint256 delay; // Задержка для этой операции
|
||||
}
|
||||
|
||||
// Основные настройки
|
||||
address public immutable dleContract; // Адрес DLE контракта
|
||||
uint256 public defaultDelay = 2 days; // Стандартная задержка
|
||||
uint256 public emergencyDelay = 30 minutes; // Экстренная задержка
|
||||
uint256 public maxDelay = 30 days; // Максимальная задержка
|
||||
uint256 public minDelay = 1 hours; // Минимальная задержка
|
||||
|
||||
// Хранение операций
|
||||
mapping(bytes32 => QueuedOperation) public queuedOperations;
|
||||
bytes32[] public operationQueue; // Список всех операций
|
||||
mapping(bytes32 => uint256) public operationIndex; // ID => индекс в очереди
|
||||
|
||||
// Категории операций с разными задержками
|
||||
mapping(bytes4 => uint256) public operationDelays; // selector => delay
|
||||
mapping(bytes4 => bool) public criticalOperations; // критические операции
|
||||
mapping(bytes4 => bool) public emergencyOperations; // экстренные операции
|
||||
|
||||
// Статистика
|
||||
uint256 public totalOperations;
|
||||
uint256 public executedOperations;
|
||||
uint256 public cancelledOperations;
|
||||
|
||||
// События
|
||||
event OperationQueued(
|
||||
bytes32 indexed operationId,
|
||||
address indexed target,
|
||||
bytes data,
|
||||
uint256 executeAfter,
|
||||
uint256 delay,
|
||||
string description
|
||||
);
|
||||
event OperationExecuted(bytes32 indexed operationId, address indexed executor);
|
||||
event OperationCancelled(bytes32 indexed operationId, address indexed canceller, string reason);
|
||||
event DelayUpdated(bytes4 indexed selector, uint256 oldDelay, uint256 newDelay);
|
||||
event DefaultDelayUpdated(uint256 oldDelay, uint256 newDelay);
|
||||
event EmergencyExecution(bytes32 indexed operationId, string reason);
|
||||
|
||||
// Модификаторы
|
||||
modifier onlyDLE() {
|
||||
require(msg.sender == dleContract, "Only DLE can call");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validOperation(bytes32 operationId) {
|
||||
require(queuedOperations[operationId].id == operationId, "Operation not found");
|
||||
require(!queuedOperations[operationId].executed, "Already executed");
|
||||
require(!queuedOperations[operationId].cancelled, "Operation cancelled");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _dleContract) {
|
||||
require(_dleContract != address(0), "DLE contract cannot be zero");
|
||||
require(_dleContract.code.length > 0, "DLE contract must exist");
|
||||
|
||||
dleContract = _dleContract;
|
||||
totalOperations = 0;
|
||||
|
||||
// Настраиваем задержки для разных типов операций
|
||||
_setupOperationDelays();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Поставить операцию в очередь (вызывается из DLE)
|
||||
* @param target Целевой контракт
|
||||
* @param data Данные операции
|
||||
* @param description Описание операции
|
||||
*/
|
||||
function queueOperation(
|
||||
address target,
|
||||
bytes memory data,
|
||||
string memory description
|
||||
) external onlyDLE returns (bytes32) {
|
||||
require(target != address(0), "Target cannot be zero");
|
||||
require(data.length >= 4, "Invalid operation data");
|
||||
|
||||
// Определяем задержку для операции
|
||||
bytes4 selector;
|
||||
assembly {
|
||||
selector := mload(add(data, 0x20))
|
||||
}
|
||||
uint256 delay = _getOperationDelay(selector);
|
||||
|
||||
// Создаём уникальный ID операции
|
||||
bytes32 operationId = keccak256(abi.encodePacked(
|
||||
target,
|
||||
data,
|
||||
block.timestamp,
|
||||
totalOperations
|
||||
));
|
||||
|
||||
// Проверяем что операция ещё не существует
|
||||
require(queuedOperations[operationId].id == bytes32(0), "Operation already exists");
|
||||
|
||||
// Создаём операцию
|
||||
queuedOperations[operationId] = QueuedOperation({
|
||||
id: operationId,
|
||||
target: target,
|
||||
data: data,
|
||||
executeAfter: block.timestamp + delay,
|
||||
queuedAt: block.timestamp,
|
||||
executed: false,
|
||||
cancelled: false,
|
||||
proposer: msg.sender, // Адрес вызывающего (обычно DLE контракт)
|
||||
description: description,
|
||||
delay: delay
|
||||
});
|
||||
|
||||
// Добавляем в очередь
|
||||
operationQueue.push(operationId);
|
||||
operationIndex[operationId] = operationQueue.length - 1;
|
||||
totalOperations++;
|
||||
|
||||
emit OperationQueued(operationId, target, data, block.timestamp + delay, delay, description);
|
||||
|
||||
return operationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Исполнить операцию после истечения задержки (может любой)
|
||||
* @param operationId ID операции
|
||||
*/
|
||||
function executeOperation(bytes32 operationId) external nonReentrant validOperation(operationId) {
|
||||
QueuedOperation storage operation = queuedOperations[operationId];
|
||||
|
||||
require(block.timestamp >= operation.executeAfter, "Timelock not expired");
|
||||
require(block.timestamp <= operation.executeAfter + 7 days, "Operation expired"); // Операции истекают через неделю
|
||||
|
||||
operation.executed = true;
|
||||
executedOperations++;
|
||||
|
||||
// Исполняем операцию
|
||||
(bool success, bytes memory result) = operation.target.call(operation.data);
|
||||
require(success, string(abi.encodePacked("Execution failed: ", result)));
|
||||
|
||||
emit OperationExecuted(operationId, msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Отменить операцию (только через DLE governance)
|
||||
* @param operationId ID операции
|
||||
* @param reason Причина отмены
|
||||
*/
|
||||
function cancelOperation(
|
||||
bytes32 operationId,
|
||||
string memory reason
|
||||
) external onlyDLE validOperation(operationId) {
|
||||
QueuedOperation storage operation = queuedOperations[operationId];
|
||||
|
||||
operation.cancelled = true;
|
||||
cancelledOperations++;
|
||||
|
||||
emit OperationCancelled(operationId, msg.sender, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Экстренное исполнение без задержки (только для критических ситуаций)
|
||||
* @param operationId ID операции
|
||||
* @param reason Причина экстренного исполнения
|
||||
*/
|
||||
function emergencyExecute(
|
||||
bytes32 operationId,
|
||||
string memory reason
|
||||
) external onlyDLE nonReentrant validOperation(operationId) {
|
||||
QueuedOperation storage operation = queuedOperations[operationId];
|
||||
|
||||
// Проверяем что операция помечена как экстренная
|
||||
bytes memory opData = operation.data;
|
||||
bytes4 selector;
|
||||
assembly {
|
||||
selector := mload(add(opData, 0x20))
|
||||
}
|
||||
require(emergencyOperations[selector], "Not emergency operation");
|
||||
|
||||
operation.executed = true;
|
||||
executedOperations++;
|
||||
|
||||
// Исполняем операцию
|
||||
(bool success, bytes memory result) = operation.target.call(operation.data);
|
||||
require(success, string(abi.encodePacked("Emergency execution failed: ", result)));
|
||||
|
||||
emit OperationExecuted(operationId, msg.sender);
|
||||
emit EmergencyExecution(operationId, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Обновить задержку для типа операции (только через governance)
|
||||
* @param selector Селектор функции
|
||||
* @param newDelay Новая задержка
|
||||
* @param isCritical Является ли операция критической
|
||||
* @param isEmergency Может ли исполняться экстренно
|
||||
*/
|
||||
function updateOperationDelay(
|
||||
bytes4 selector,
|
||||
uint256 newDelay,
|
||||
bool isCritical,
|
||||
bool isEmergency
|
||||
) external onlyDLE {
|
||||
require(newDelay >= minDelay, "Delay too short");
|
||||
require(newDelay <= maxDelay, "Delay too long");
|
||||
|
||||
uint256 oldDelay = operationDelays[selector];
|
||||
operationDelays[selector] = newDelay;
|
||||
criticalOperations[selector] = isCritical;
|
||||
emergencyOperations[selector] = isEmergency;
|
||||
|
||||
emit DelayUpdated(selector, oldDelay, newDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Обновить стандартную задержку (только через governance)
|
||||
* @param newDelay Новая стандартная задержка
|
||||
*/
|
||||
function updateDefaultDelay(uint256 newDelay) external onlyDLE {
|
||||
require(newDelay >= minDelay, "Delay too short");
|
||||
require(newDelay <= maxDelay, "Delay too long");
|
||||
|
||||
uint256 oldDelay = defaultDelay;
|
||||
defaultDelay = newDelay;
|
||||
|
||||
emit DefaultDelayUpdated(oldDelay, newDelay);
|
||||
}
|
||||
|
||||
// ===== VIEW ФУНКЦИИ =====
|
||||
|
||||
/**
|
||||
* @dev Получить информацию об операции
|
||||
*/
|
||||
function getOperation(bytes32 operationId) external view returns (QueuedOperation memory) {
|
||||
return queuedOperations[operationId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Проверить, готова ли операция к исполнению
|
||||
*/
|
||||
function isReady(bytes32 operationId) external view returns (bool) {
|
||||
QueuedOperation storage operation = queuedOperations[operationId];
|
||||
return operation.id != bytes32(0) &&
|
||||
!operation.executed &&
|
||||
!operation.cancelled &&
|
||||
block.timestamp >= operation.executeAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить время до исполнения операции
|
||||
*/
|
||||
function getTimeToExecution(bytes32 operationId) external view returns (uint256) {
|
||||
QueuedOperation storage operation = queuedOperations[operationId];
|
||||
if (operation.executeAfter <= block.timestamp) {
|
||||
return 0;
|
||||
}
|
||||
return operation.executeAfter - block.timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить список активных операций
|
||||
*/
|
||||
function getActiveOperations() external view returns (bytes32[] memory) {
|
||||
uint256 activeCount = 0;
|
||||
|
||||
// Считаем активные операции
|
||||
for (uint256 i = 0; i < operationQueue.length; i++) {
|
||||
QueuedOperation storage op = queuedOperations[operationQueue[i]];
|
||||
if (!op.executed && !op.cancelled) {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Заполняем массив
|
||||
bytes32[] memory activeOps = new bytes32[](activeCount);
|
||||
uint256 index = 0;
|
||||
|
||||
for (uint256 i = 0; i < operationQueue.length; i++) {
|
||||
QueuedOperation storage op = queuedOperations[operationQueue[i]];
|
||||
if (!op.executed && !op.cancelled) {
|
||||
activeOps[index] = operationQueue[i];
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return activeOps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить статистику Timelock
|
||||
*/
|
||||
function getTimelockStats() external view returns (
|
||||
uint256 total,
|
||||
uint256 executed,
|
||||
uint256 cancelled,
|
||||
uint256 pending,
|
||||
uint256 currentDelay
|
||||
) {
|
||||
return (
|
||||
totalOperations,
|
||||
executedOperations,
|
||||
cancelledOperations,
|
||||
totalOperations - executedOperations - cancelledOperations,
|
||||
defaultDelay
|
||||
);
|
||||
}
|
||||
|
||||
// ===== ВНУТРЕННИЕ ФУНКЦИИ =====
|
||||
|
||||
/**
|
||||
* @dev Определить задержку для операции
|
||||
*/
|
||||
function _getOperationDelay(bytes4 selector) internal view returns (uint256) {
|
||||
uint256 customDelay = operationDelays[selector];
|
||||
if (customDelay > 0) {
|
||||
return customDelay;
|
||||
}
|
||||
|
||||
// Используем стандартную задержку
|
||||
return defaultDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Настроить стандартные задержки для операций
|
||||
*/
|
||||
function _setupOperationDelays() internal {
|
||||
// Критические операции - длинная задержка (7 дней)
|
||||
bytes4 updateQuorum = bytes4(keccak256("updateQuorumPercentage(uint256)"));
|
||||
bytes4 addModule = bytes4(keccak256("_addModule(bytes32,address)"));
|
||||
bytes4 removeModule = bytes4(keccak256("_removeModule(bytes32)"));
|
||||
bytes4 addChain = bytes4(keccak256("_addSupportedChain(uint256)"));
|
||||
bytes4 removeChain = bytes4(keccak256("_removeSupportedChain(uint256)"));
|
||||
|
||||
operationDelays[updateQuorum] = 7 days;
|
||||
operationDelays[addModule] = 7 days;
|
||||
operationDelays[removeModule] = 7 days;
|
||||
operationDelays[addChain] = 5 days;
|
||||
operationDelays[removeChain] = 5 days;
|
||||
|
||||
criticalOperations[updateQuorum] = true;
|
||||
criticalOperations[addModule] = true;
|
||||
criticalOperations[removeModule] = true;
|
||||
criticalOperations[addChain] = true;
|
||||
criticalOperations[removeChain] = true;
|
||||
|
||||
// Обычные операции - стандартная задержка (2 дня)
|
||||
bytes4 updateDLEInfo = bytes4(keccak256("updateDLEInfo(string,string,string,string,uint256,string[],uint256)"));
|
||||
bytes4 updateChainId = bytes4(keccak256("updateCurrentChainId(uint256)"));
|
||||
bytes4 updateVotingDurations = bytes4(keccak256("_updateVotingDurations(uint256,uint256)"));
|
||||
|
||||
operationDelays[updateDLEInfo] = 2 days;
|
||||
operationDelays[updateChainId] = 3 days;
|
||||
operationDelays[updateVotingDurations] = 1 days;
|
||||
|
||||
// Treasury операции - короткая задержка (1 день)
|
||||
bytes4 treasuryTransfer = bytes4(keccak256("treasuryTransfer(address,address,uint256)"));
|
||||
bytes4 treasuryAddToken = bytes4(keccak256("treasuryAddToken(address,string,uint8)"));
|
||||
|
||||
operationDelays[treasuryTransfer] = 1 days;
|
||||
operationDelays[treasuryAddToken] = 1 days;
|
||||
|
||||
// Экстренные операции (могут исполняться немедленно при необходимости)
|
||||
emergencyOperations[removeModule] = true; // Удаление вредоносного модуля
|
||||
emergencyOperations[removeChain] = true; // Отключение скомпрометированной сети
|
||||
}
|
||||
}
|
||||
527
backend/contracts/TreasuryModule.sol
Normal file
527
backend/contracts/TreasuryModule.sol
Normal file
@@ -0,0 +1,527 @@
|
||||
// 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/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/utils/Address.sol";
|
||||
|
||||
/**
|
||||
* @title TreasuryModule
|
||||
* @dev Модуль казны для управления активами DLE
|
||||
*
|
||||
* ОСНОВНЫЕ ФУНКЦИИ:
|
||||
* - Управление различными ERC20 токенами
|
||||
* - Хранение и перевод нативных монет (ETH, BNB, MATIC и т.д.)
|
||||
* - Интеграция с DLE governance для авторизации операций
|
||||
* - Поддержка мульти-чейн операций
|
||||
* - Batch операции для оптимизации газа
|
||||
*
|
||||
* БЕЗОПАСНОСТЬ:
|
||||
* - Только DLE контракт может выполнять операции
|
||||
* - Защита от реентерабельности
|
||||
* - Валидация всех входных параметров
|
||||
* - Поддержка emergency pause
|
||||
*/
|
||||
contract TreasuryModule is ReentrancyGuard {
|
||||
using SafeERC20 for IERC20;
|
||||
using Address for address payable;
|
||||
|
||||
// Структура для информации о токене
|
||||
struct TokenInfo {
|
||||
address tokenAddress; // Адрес токена (0x0 для нативной монеты)
|
||||
string symbol; // Символ токена
|
||||
uint8 decimals; // Количество знаков после запятой
|
||||
bool isActive; // Активен ли токен
|
||||
bool isNative; // Является ли нативной монетой
|
||||
uint256 addedTimestamp; // Время добавления
|
||||
uint256 balance; // Кэшированный баланс (обновляется при операциях)
|
||||
}
|
||||
|
||||
// Структура для batch операции
|
||||
struct BatchTransfer {
|
||||
address tokenAddress; // Адрес токена (0x0 для нативной монеты)
|
||||
address recipient; // Получатель
|
||||
uint256 amount; // Количество
|
||||
}
|
||||
|
||||
// Основные переменные
|
||||
address public immutable dleContract; // Адрес основного DLE контракта
|
||||
uint256 public immutable chainId; // ID текущей сети
|
||||
|
||||
// Хранение токенов
|
||||
mapping(address => TokenInfo) public supportedTokens; // tokenAddress => TokenInfo
|
||||
address[] public tokenList; // Список всех добавленных токенов
|
||||
mapping(address => uint256) public tokenIndex; // tokenAddress => index в tokenList
|
||||
|
||||
// Статистика
|
||||
uint256 public totalTokensSupported;
|
||||
uint256 public totalTransactions;
|
||||
mapping(address => uint256) public tokenTransactionCount; // tokenAddress => count
|
||||
|
||||
// Система экстренного останова
|
||||
bool public emergencyPaused;
|
||||
address public emergencyAdmin;
|
||||
|
||||
// События
|
||||
event TokenAdded(
|
||||
address indexed tokenAddress,
|
||||
string symbol,
|
||||
uint8 decimals,
|
||||
bool isNative,
|
||||
uint256 timestamp
|
||||
);
|
||||
event TokenRemoved(address indexed tokenAddress, string symbol, uint256 timestamp);
|
||||
event TokenStatusUpdated(address indexed tokenAddress, bool newStatus);
|
||||
event FundsDeposited(
|
||||
address indexed tokenAddress,
|
||||
address indexed from,
|
||||
uint256 amount,
|
||||
uint256 newBalance
|
||||
);
|
||||
event FundsTransferred(
|
||||
address indexed tokenAddress,
|
||||
address indexed to,
|
||||
uint256 amount,
|
||||
uint256 remainingBalance,
|
||||
bytes32 indexed proposalId
|
||||
);
|
||||
event BatchTransferExecuted(
|
||||
uint256 transferCount,
|
||||
uint256 totalAmount,
|
||||
bytes32 indexed proposalId
|
||||
);
|
||||
event EmergencyPauseToggled(bool isPaused, address admin);
|
||||
event BalanceUpdated(address indexed tokenAddress, uint256 oldBalance, uint256 newBalance);
|
||||
|
||||
// Модификаторы
|
||||
modifier onlyDLE() {
|
||||
require(msg.sender == dleContract, "Only DLE contract can call this");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier whenNotPaused() {
|
||||
require(!emergencyPaused, "Treasury is paused");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyEmergencyAdmin() {
|
||||
require(msg.sender == emergencyAdmin, "Only emergency admin");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier validToken(address tokenAddress) {
|
||||
require(supportedTokens[tokenAddress].isActive, "Token not supported or inactive");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address _dleContract, uint256 _chainId, address _emergencyAdmin) {
|
||||
require(_dleContract != address(0), "DLE contract cannot be zero");
|
||||
require(_emergencyAdmin != address(0), "Emergency admin cannot be zero");
|
||||
require(_chainId > 0, "Chain ID must be positive");
|
||||
|
||||
dleContract = _dleContract;
|
||||
chainId = _chainId;
|
||||
emergencyAdmin = _emergencyAdmin;
|
||||
|
||||
// Автоматически добавляем нативную монету сети
|
||||
_addNativeToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить средства (может вызывать кто угодно для пополнения казны)
|
||||
*/
|
||||
receive() external payable {
|
||||
if (msg.value > 0) {
|
||||
_updateTokenBalance(address(0), supportedTokens[address(0)].balance + msg.value);
|
||||
emit FundsDeposited(address(0), msg.sender, msg.value, supportedTokens[address(0)].balance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Добавить новый токен в казну (только через DLE governance)
|
||||
* @param tokenAddress Адрес токена (0x0 для нативной монеты)
|
||||
* @param symbol Символ токена
|
||||
* @param decimals Количество знаков после запятой
|
||||
*/
|
||||
function addToken(
|
||||
address tokenAddress,
|
||||
string memory symbol,
|
||||
uint8 decimals
|
||||
) external onlyDLE whenNotPaused {
|
||||
require(!supportedTokens[tokenAddress].isActive, "Token already supported");
|
||||
require(bytes(symbol).length > 0, "Symbol cannot be empty");
|
||||
require(bytes(symbol).length <= 20, "Symbol too long");
|
||||
|
||||
// Для ERC20 токенов проверяем, что контракт существует
|
||||
if (tokenAddress != address(0)) {
|
||||
require(tokenAddress.code.length > 0, "Token contract does not exist");
|
||||
|
||||
// Проверяем базовые ERC20 функции
|
||||
try IERC20(tokenAddress).totalSupply() returns (uint256) {
|
||||
// Token contract is valid
|
||||
} catch {
|
||||
revert("Invalid ERC20 token");
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем токен
|
||||
supportedTokens[tokenAddress] = TokenInfo({
|
||||
tokenAddress: tokenAddress,
|
||||
symbol: symbol,
|
||||
decimals: decimals,
|
||||
isActive: true,
|
||||
isNative: tokenAddress == address(0),
|
||||
addedTimestamp: block.timestamp,
|
||||
balance: 0
|
||||
});
|
||||
|
||||
tokenList.push(tokenAddress);
|
||||
tokenIndex[tokenAddress] = tokenList.length - 1;
|
||||
totalTokensSupported++;
|
||||
|
||||
// Обновляем баланс
|
||||
_refreshTokenBalance(tokenAddress);
|
||||
|
||||
emit TokenAdded(tokenAddress, symbol, decimals, tokenAddress == address(0), block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Удалить токен из казны (только через DLE governance)
|
||||
* @param tokenAddress Адрес токена для удаления
|
||||
*/
|
||||
function removeToken(address tokenAddress) external onlyDLE whenNotPaused validToken(tokenAddress) {
|
||||
require(tokenAddress != address(0), "Cannot remove native token");
|
||||
|
||||
TokenInfo memory tokenInfo = supportedTokens[tokenAddress];
|
||||
require(tokenInfo.balance == 0, "Token balance must be zero before removal");
|
||||
|
||||
// Удаляем из массива
|
||||
uint256 index = tokenIndex[tokenAddress];
|
||||
uint256 lastIndex = tokenList.length - 1;
|
||||
|
||||
if (index != lastIndex) {
|
||||
address lastToken = tokenList[lastIndex];
|
||||
tokenList[index] = lastToken;
|
||||
tokenIndex[lastToken] = index;
|
||||
}
|
||||
|
||||
tokenList.pop();
|
||||
delete tokenIndex[tokenAddress];
|
||||
delete supportedTokens[tokenAddress];
|
||||
totalTokensSupported--;
|
||||
|
||||
emit TokenRemoved(tokenAddress, tokenInfo.symbol, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Изменить статус токена (активен/неактивен)
|
||||
* @param tokenAddress Адрес токена
|
||||
* @param isActive Новый статус
|
||||
*/
|
||||
function setTokenStatus(address tokenAddress, bool isActive) external onlyDLE {
|
||||
require(supportedTokens[tokenAddress].tokenAddress == tokenAddress, "Token not found");
|
||||
require(tokenAddress != address(0), "Cannot deactivate native token");
|
||||
|
||||
supportedTokens[tokenAddress].isActive = isActive;
|
||||
emit TokenStatusUpdated(tokenAddress, isActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Перевести токены (только через DLE governance)
|
||||
* @param tokenAddress Адрес токена (0x0 для нативной монеты)
|
||||
* @param recipient Получатель
|
||||
* @param amount Количество для перевода
|
||||
* @param proposalId ID предложения DLE (для логирования)
|
||||
*/
|
||||
function transferFunds(
|
||||
address tokenAddress,
|
||||
address recipient,
|
||||
uint256 amount,
|
||||
bytes32 proposalId
|
||||
) external onlyDLE whenNotPaused validToken(tokenAddress) nonReentrant {
|
||||
require(recipient != address(0), "Recipient cannot be zero");
|
||||
require(amount > 0, "Amount must be positive");
|
||||
|
||||
TokenInfo storage tokenInfo = supportedTokens[tokenAddress];
|
||||
require(tokenInfo.balance >= amount, "Insufficient balance");
|
||||
|
||||
// Обновляем баланс
|
||||
_updateTokenBalance(tokenAddress, tokenInfo.balance - amount);
|
||||
|
||||
// Выполняем перевод
|
||||
if (tokenInfo.isNative) {
|
||||
payable(recipient).sendValue(amount);
|
||||
} else {
|
||||
IERC20(tokenAddress).safeTransfer(recipient, amount);
|
||||
}
|
||||
|
||||
totalTransactions++;
|
||||
tokenTransactionCount[tokenAddress]++;
|
||||
|
||||
emit FundsTransferred(
|
||||
tokenAddress,
|
||||
recipient,
|
||||
amount,
|
||||
tokenInfo.balance,
|
||||
proposalId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Выполнить batch перевод (только через DLE governance)
|
||||
* @param transfers Массив переводов
|
||||
* @param proposalId ID предложения DLE
|
||||
*/
|
||||
function batchTransfer(
|
||||
BatchTransfer[] memory transfers,
|
||||
bytes32 proposalId
|
||||
) external onlyDLE whenNotPaused nonReentrant {
|
||||
require(transfers.length > 0, "No transfers provided");
|
||||
require(transfers.length <= 100, "Too many transfers"); // Защита от DoS
|
||||
|
||||
uint256 totalAmount = 0;
|
||||
|
||||
for (uint256 i = 0; i < transfers.length; i++) {
|
||||
BatchTransfer memory transfer = transfers[i];
|
||||
|
||||
require(transfer.recipient != address(0), "Recipient cannot be zero");
|
||||
require(transfer.amount > 0, "Amount must be positive");
|
||||
require(supportedTokens[transfer.tokenAddress].isActive, "Token not supported");
|
||||
|
||||
TokenInfo storage tokenInfo = supportedTokens[transfer.tokenAddress];
|
||||
require(tokenInfo.balance >= transfer.amount, "Insufficient balance");
|
||||
|
||||
// Обновляем баланс
|
||||
_updateTokenBalance(transfer.tokenAddress, tokenInfo.balance - transfer.amount);
|
||||
|
||||
// Выполняем перевод
|
||||
if (tokenInfo.isNative) {
|
||||
payable(transfer.recipient).sendValue(transfer.amount);
|
||||
} else {
|
||||
IERC20(transfer.tokenAddress).safeTransfer(transfer.recipient, transfer.amount);
|
||||
}
|
||||
|
||||
totalAmount += transfer.amount;
|
||||
tokenTransactionCount[transfer.tokenAddress]++;
|
||||
|
||||
emit FundsTransferred(
|
||||
transfer.tokenAddress,
|
||||
transfer.recipient,
|
||||
transfer.amount,
|
||||
tokenInfo.balance,
|
||||
proposalId
|
||||
);
|
||||
}
|
||||
|
||||
totalTransactions += transfers.length;
|
||||
emit BatchTransferExecuted(transfers.length, totalAmount, proposalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Пополнить казну ERC20 токенами
|
||||
* @param tokenAddress Адрес токена
|
||||
* @param amount Количество для пополнения
|
||||
*/
|
||||
function depositToken(
|
||||
address tokenAddress,
|
||||
uint256 amount
|
||||
) external whenNotPaused validToken(tokenAddress) nonReentrant {
|
||||
require(amount > 0, "Amount must be positive");
|
||||
require(tokenAddress != address(0), "Use receive() for native deposits");
|
||||
|
||||
IERC20(tokenAddress).safeTransferFrom(msg.sender, address(this), amount);
|
||||
|
||||
_updateTokenBalance(tokenAddress, supportedTokens[tokenAddress].balance + amount);
|
||||
|
||||
emit FundsDeposited(tokenAddress, msg.sender, amount, supportedTokens[tokenAddress].balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Обновить баланс токена (синхронизация с реальным балансом)
|
||||
* @param tokenAddress Адрес токена
|
||||
*/
|
||||
function refreshBalance(address tokenAddress) external validToken(tokenAddress) {
|
||||
_refreshTokenBalance(tokenAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Обновить балансы всех токенов
|
||||
*/
|
||||
function refreshAllBalances() external {
|
||||
for (uint256 i = 0; i < tokenList.length; i++) {
|
||||
if (supportedTokens[tokenList[i]].isActive) {
|
||||
_refreshTokenBalance(tokenList[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Экстренная пауза (только emergency admin)
|
||||
*/
|
||||
function emergencyPause() external onlyEmergencyAdmin {
|
||||
emergencyPaused = !emergencyPaused;
|
||||
emit EmergencyPauseToggled(emergencyPaused, msg.sender);
|
||||
}
|
||||
|
||||
// ===== VIEW ФУНКЦИИ =====
|
||||
|
||||
/**
|
||||
* @dev Получить информацию о токене
|
||||
*/
|
||||
function getTokenInfo(address tokenAddress) external view returns (TokenInfo memory) {
|
||||
return supportedTokens[tokenAddress];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить список всех токенов
|
||||
*/
|
||||
function getAllTokens() external view returns (address[] memory) {
|
||||
return tokenList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить активные токены
|
||||
*/
|
||||
function getActiveTokens() external view returns (address[] memory) {
|
||||
uint256 activeCount = 0;
|
||||
|
||||
// Считаем активные токены
|
||||
for (uint256 i = 0; i < tokenList.length; i++) {
|
||||
if (supportedTokens[tokenList[i]].isActive) {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Создаём массив активных токенов
|
||||
address[] memory activeTokens = new address[](activeCount);
|
||||
uint256 index = 0;
|
||||
|
||||
for (uint256 i = 0; i < tokenList.length; i++) {
|
||||
if (supportedTokens[tokenList[i]].isActive) {
|
||||
activeTokens[index] = tokenList[i];
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return activeTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить баланс токена
|
||||
*/
|
||||
function getTokenBalance(address tokenAddress) external view returns (uint256) {
|
||||
return supportedTokens[tokenAddress].balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить реальный баланс токена (обращение к блокчейну)
|
||||
*/
|
||||
function getRealTokenBalance(address tokenAddress) external view returns (uint256) {
|
||||
if (tokenAddress == address(0)) {
|
||||
return address(this).balance;
|
||||
} else {
|
||||
return IERC20(tokenAddress).balanceOf(address(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Проверить, поддерживается ли токен
|
||||
*/
|
||||
function isTokenSupported(address tokenAddress) external view returns (bool) {
|
||||
return supportedTokens[tokenAddress].isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Получить статистику казны
|
||||
*/
|
||||
function getTreasuryStats() external view returns (
|
||||
uint256 totalTokens,
|
||||
uint256 totalTxs,
|
||||
uint256 currentChainId,
|
||||
bool isPaused
|
||||
) {
|
||||
return (
|
||||
totalTokensSupported,
|
||||
totalTransactions,
|
||||
chainId,
|
||||
emergencyPaused
|
||||
);
|
||||
}
|
||||
|
||||
// ===== ВНУТРЕННИЕ ФУНКЦИИ =====
|
||||
|
||||
/**
|
||||
* @dev Автоматически добавить нативную монету
|
||||
*/
|
||||
function _addNativeToken() internal {
|
||||
string memory nativeSymbol;
|
||||
|
||||
// Определяем символ нативной монеты по chain ID
|
||||
if (chainId == 1 || chainId == 11155111) { // Ethereum Mainnet / Sepolia
|
||||
nativeSymbol = "ETH";
|
||||
} else if (chainId == 56 || chainId == 97) { // BSC Mainnet / Testnet
|
||||
nativeSymbol = "BNB";
|
||||
} else if (chainId == 137 || chainId == 80001) { // Polygon Mainnet / Mumbai
|
||||
nativeSymbol = "MATIC";
|
||||
} else if (chainId == 42161) { // Arbitrum One
|
||||
nativeSymbol = "ETH";
|
||||
} else if (chainId == 10) { // Optimism
|
||||
nativeSymbol = "ETH";
|
||||
} else if (chainId == 43114) { // Avalanche
|
||||
nativeSymbol = "AVAX";
|
||||
} else {
|
||||
nativeSymbol = "NATIVE"; // Для неизвестных сетей
|
||||
}
|
||||
|
||||
supportedTokens[address(0)] = TokenInfo({
|
||||
tokenAddress: address(0),
|
||||
symbol: nativeSymbol,
|
||||
decimals: 18,
|
||||
isActive: true,
|
||||
isNative: true,
|
||||
addedTimestamp: block.timestamp,
|
||||
balance: 0
|
||||
});
|
||||
|
||||
tokenList.push(address(0));
|
||||
tokenIndex[address(0)] = 0;
|
||||
totalTokensSupported = 1;
|
||||
|
||||
emit TokenAdded(address(0), nativeSymbol, 18, true, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Обновить кэшированный баланс токена
|
||||
*/
|
||||
function _updateTokenBalance(address tokenAddress, uint256 newBalance) internal {
|
||||
uint256 oldBalance = supportedTokens[tokenAddress].balance;
|
||||
supportedTokens[tokenAddress].balance = newBalance;
|
||||
emit BalanceUpdated(tokenAddress, oldBalance, newBalance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Синхронизировать кэшированный баланс с реальным
|
||||
*/
|
||||
function _refreshTokenBalance(address tokenAddress) internal {
|
||||
uint256 realBalance;
|
||||
|
||||
if (tokenAddress == address(0)) {
|
||||
realBalance = address(this).balance;
|
||||
} else {
|
||||
realBalance = IERC20(tokenAddress).balanceOf(address(this));
|
||||
}
|
||||
|
||||
_updateTokenBalance(tokenAddress, realBalance);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user