ваше сообщение коммита
This commit is contained in:
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