// SPDX-License-Identifier: PROPRIETARY AND MIT // Copyright (c) 2024-2026 Тарабанов Александр Викторович // 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/VC-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); } }