Files
DLE/backend/contracts/TreasuryModule.sol

528 lines
19 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";
/**
* @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);
}
}