528 lines
19 KiB
Solidity
528 lines
19 KiB
Solidity
// 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);
|
||
}
|
||
}
|