Files
DLE/backend/contracts/TreasuryModule.sol
Alex de0f8aecf2 feat: Добавлены формы деплоя модулей DLE с полными настройками
- Создана форма деплоя TreasuryModule с детальными настройками казны
- Создана форма деплоя TimelockModule с настройками временных задержек
- Создана форма деплоя DLEReader с простой конфигурацией
- Добавлены маршруты и индексы для всех модулей
- Исправлены пути импорта BaseLayout
- Добавлены авторские права во все файлы
- Улучшена архитектура деплоя модулей отдельно от основного DLE
2025-09-23 02:57:59 +03:00

771 lines
29 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: 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";
// ERC-4337 интерфейсы для оплаты газа любым токеном
interface IPaymaster {
function validatePaymasterUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 maxCost
) external returns (bytes memory context, uint256 validationData);
function postOp(
PostOpMode mode,
bytes calldata context,
uint256 actualGasCost
) external;
}
struct UserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}
enum PostOpMode {
opSucceeded,
opReverted,
postOpReverted
}
/**
* @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;
// ERC-4337 Paymaster для оплаты газа любым токеном
address public paymaster;
mapping(address => bool) public gasPaymentTokens; // Токены, которыми можно платить за газ
mapping(address => uint256) public gasTokenRates; // Курсы обмена токенов на нативную монету
// События
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);
event PaymasterUpdated(address indexed oldPaymaster, address indexed newPaymaster);
event GasPaymentTokenAdded(address indexed tokenAddress, uint256 rate);
event GasPaymentTokenRemoved(address indexed tokenAddress);
event GasPaidWithToken(address indexed tokenAddress, uint256 tokenAmount, uint256 nativeAmount);
// Модификаторы
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) {
// Автоматически добавляем нативную монету, если её нет
if (!supportedTokens[address(0)].isActive) {
_addNativeToken();
}
_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);
}
// ===== ФУНКЦИИ ДЛЯ ОПЛАТЫ ГАЗА ЛЮБЫМ ТОКЕНОМ =====
/**
* @dev Установить Paymaster для ERC-4337 (только через DLE governance)
* @param _paymaster Адрес Paymaster контракта
*/
function setPaymaster(address _paymaster) external onlyDLE {
require(_paymaster != address(0), "Paymaster cannot be zero");
address oldPaymaster = paymaster;
paymaster = _paymaster;
emit PaymasterUpdated(oldPaymaster, _paymaster);
}
/**
* @dev Добавить токен для оплаты газа (только через DLE governance)
* @param tokenAddress Адрес токена
* @param rate Курс обмена (сколько токенов за 1 нативную монету)
*/
function addGasPaymentToken(address tokenAddress, uint256 rate) external onlyDLE {
require(rate > 0, "Rate must be positive");
// Для нативной монеты проверяем, что она активна
if (tokenAddress == address(0)) {
require(supportedTokens[tokenAddress].isActive, "Native token must be supported");
} else {
require(supportedTokens[tokenAddress].isActive, "Token must be supported");
}
gasPaymentTokens[tokenAddress] = true;
gasTokenRates[tokenAddress] = rate;
emit GasPaymentTokenAdded(tokenAddress, rate);
}
/**
* @dev Удалить токен для оплаты газа (только через DLE governance)
* @param tokenAddress Адрес токена
*/
function removeGasPaymentToken(address tokenAddress) external onlyDLE {
require(gasPaymentTokens[tokenAddress], "Token not set for gas payment");
gasPaymentTokens[tokenAddress] = false;
gasTokenRates[tokenAddress] = 0;
emit GasPaymentTokenRemoved(tokenAddress);
}
/**
* @dev Обновить курс обмена токена (только через DLE governance)
* @param tokenAddress Адрес токена
* @param newRate Новый курс обмена
*/
function updateGasTokenRate(address tokenAddress, uint256 newRate) external onlyDLE {
require(gasPaymentTokens[tokenAddress], "Token not set for gas payment");
require(newRate > 0, "Rate must be positive");
gasTokenRates[tokenAddress] = newRate;
emit GasPaymentTokenAdded(tokenAddress, newRate); // Переиспользуем событие
}
/**
* @dev Оплатить газ токенами (через ERC-4337 Paymaster)
* @param tokenAddress Адрес токена для оплаты (0x0 для нативной монеты)
* @param gasAmount Количество газа для оплаты
* @param userOp UserOperation для ERC-4337
*/
function payGasWithToken(
address tokenAddress,
uint256 gasAmount,
UserOperation calldata userOp
) external onlyDLE whenNotPaused nonReentrant {
_payGasWithToken(tokenAddress, gasAmount, userOp);
}
/**
* @dev Проверить, можно ли оплатить газ токеном
* @param tokenAddress Адрес токена (0x0 для нативной монеты)
* @param gasAmount Количество газа
* @return canPay Можно ли оплатить
* @return tokenAmount Количество токенов для оплаты
*/
function canPayGasWithToken(
address tokenAddress,
uint256 gasAmount
) external view returns (bool canPay, uint256 tokenAmount) {
if (!gasPaymentTokens[tokenAddress] || !supportedTokens[tokenAddress].isActive) {
return (false, 0);
}
tokenAmount = (gasAmount * gasTokenRates[tokenAddress]) / 1e18;
canPay = supportedTokens[tokenAddress].balance >= tokenAmount;
return (canPay, tokenAmount);
}
/**
* @dev Проверить, можно ли оплатить газ нативной монетой
* @param gasAmount Количество газа
* @return canPay Можно ли оплатить
* @return nativeAmount Количество нативной монеты для оплаты
*/
function canPayGasWithNative(
uint256 gasAmount
) external view returns (bool canPay, uint256 nativeAmount) {
return this.canPayGasWithToken(address(0), gasAmount);
}
/**
* @dev Оплатить газ нативной монетой (упрощенная версия)
* @param gasAmount Количество газа для оплаты
* @param userOp UserOperation для ERC-4337
*/
function payGasWithNative(
uint256 gasAmount,
UserOperation calldata userOp
) external onlyDLE whenNotPaused nonReentrant {
// Используем нативную монету (address(0))
_payGasWithToken(address(0), gasAmount, userOp);
}
/**
* @dev Внутренняя функция для оплаты газа токенами
* @param tokenAddress Адрес токена для оплаты (0x0 для нативной монеты)
* @param gasAmount Количество газа для оплаты
* @param userOp UserOperation для ERC-4337
*/
function _payGasWithToken(
address tokenAddress,
uint256 gasAmount,
UserOperation calldata userOp
) internal {
require(gasPaymentTokens[tokenAddress], "Token not supported for gas payment");
require(paymaster != address(0), "Paymaster not set");
TokenInfo storage tokenInfo = supportedTokens[tokenAddress];
require(tokenInfo.isActive, "Token not active");
// Вычисляем количество токенов для оплаты газа
uint256 tokenAmount = (gasAmount * gasTokenRates[tokenAddress]) / 1e18;
require(tokenInfo.balance >= tokenAmount, "Insufficient token balance");
// Обновляем баланс токена
_updateTokenBalance(tokenAddress, tokenInfo.balance - tokenAmount);
// Переводим токены на Paymaster (поддержка нативных и ERC20 токенов)
if (tokenInfo.isNative) {
// Для нативных токенов (ETH, BNB, MATIC и т.д.)
payable(paymaster).sendValue(tokenAmount);
} else {
// Для ERC20 токенов
IERC20(tokenAddress).safeTransfer(paymaster, tokenAmount);
}
// Вызываем Paymaster для оплаты газа
IPaymaster(paymaster).validatePaymasterUserOp(
userOp,
keccak256(abi.encode(userOp)),
gasAmount
);
emit GasPaidWithToken(tokenAddress, tokenAmount, gasAmount);
}
// ===== 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++;
}
}
// Нативная монета всегда активна
if (address(this).balance > 0 || supportedTokens[address(0)].isActive) {
activeCount++;
}
// Создаём массив активных токенов
address[] memory activeTokens = new address[](activeCount);
uint256 index = 0;
// Добавляем нативную монету первой, если есть баланс
if (address(this).balance > 0 || supportedTokens[address(0)].isActive) {
activeTokens[index] = address(0);
index++;
}
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) {
// Для нативной монеты возвращаем реальный баланс, если токен не зарегистрирован
if (tokenAddress == address(0) && !supportedTokens[address(0)].isActive) {
return address(this).balance;
}
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) {
// Нативная монета всегда поддерживается
if (tokenAddress == address(0)) {
return true;
}
return supportedTokens[tokenAddress].isActive;
}
/**
* @dev Получить статистику казны
*/
function getTreasuryStats() external view returns (
uint256 totalTokens,
uint256 totalTxs,
uint256 currentChainId,
bool isPaused,
address paymasterAddress,
uint256 gasPaymentTokensCount
) {
// Считаем количество токенов для оплаты газа
uint256 gasTokensCount = 0;
for (uint256 i = 0; i < tokenList.length; i++) {
if (gasPaymentTokens[tokenList[i]]) {
gasTokensCount++;
}
}
return (
totalTokensSupported,
totalTransactions,
chainId,
emergencyPaused,
paymaster,
gasTokensCount
);
}
// ===== ВНУТРЕННИЕ ФУНКЦИИ =====
/**
* @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);
}
}