ваше сообщение коммита

This commit is contained in:
2025-08-29 18:37:57 +03:00
parent 8e50c6c4d8
commit 4e4cb611a1
53 changed files with 4380 additions and 5902 deletions

11
.gitignore vendored
View File

@@ -118,6 +118,13 @@ tmp/
*.db *.db
*.sqlite *.sqlite
# Uploads and user files
backend/uploads/
frontend/uploads/
# Database migrations (may contain sensitive data)
backend/db/migrations/
# Keys and certificates # Keys and certificates
*.key *.key
*.pem *.pem
@@ -139,6 +146,10 @@ ssl/certs/
*.crt *.crt
*.p12 *.p12
# Database encryption keys - КРИТИЧЕСКИ ВАЖНО!
**/full_db_encryption.key
**/ssl/full_db_encryption.key
# Docker # Docker
.dockerignore .dockerignore

View File

@@ -96,9 +96,6 @@ docker-compose restart
# Остановка сервисов # Остановка сервисов
docker compose down docker compose down
# Остановка сервисов и удаление томов
docker compose down -v
```
## Контакты и поддержка ## Контакты и поддержка

View File

@@ -28,13 +28,18 @@ const messagesRoutes = require('./routes/messages');
const ragRoutes = require('./routes/rag'); // Новый роут для RAG-ассистента const ragRoutes = require('./routes/rag'); // Новый роут для RAG-ассистента
const monitoringRoutes = require('./routes/monitoring'); const monitoringRoutes = require('./routes/monitoring');
const pagesRoutes = require('./routes/pages'); // Добавляем импорт роутера страниц const pagesRoutes = require('./routes/pages'); // Добавляем импорт роутера страниц
const uploadsRoutes = require('./routes/uploads');
const ensRoutes = require('./routes/ens');
// Factory routes removed - no longer needed
// Проверка и создание директорий для хранения данных контрактов // Проверка и создание директорий для хранения данных контрактов
const ensureDirectoriesExist = () => { const ensureDirectoriesExist = () => {
const directories = [ const directories = [
path.join(__dirname, 'contracts-data'), path.join(__dirname, 'contracts-data'),
path.join(__dirname, 'contracts-data/dles'), path.join(__dirname, 'contracts-data/dles'),
path.join(__dirname, 'temp') path.join(__dirname, 'temp'),
path.join(__dirname, 'uploads'),
path.join(__dirname, 'uploads/logos')
]; ];
for (const dir of directories) { for (const dir of directories) {
@@ -93,6 +98,7 @@ const dleProposalsRoutes = require('./routes/dleProposals'); // Функции
const dleModulesRoutes = require('./routes/dleModules'); // Функции модулей const dleModulesRoutes = require('./routes/dleModules'); // Функции модулей
const dleTokensRoutes = require('./routes/dleTokens'); // Функции токенов const dleTokensRoutes = require('./routes/dleTokens'); // Функции токенов
const dleAnalyticsRoutes = require('./routes/dleAnalytics'); // Аналитика и история const dleAnalyticsRoutes = require('./routes/dleAnalytics'); // Аналитика и история
const compileRoutes = require('./routes/compile'); // Компиляция контрактов
const dleMultichainRoutes = require('./routes/dleMultichain'); // Мультичейн функции const dleMultichainRoutes = require('./routes/dleMultichain'); // Мультичейн функции
const dleHistoryRoutes = require('./routes/dleHistory'); // Расширенная история const dleHistoryRoutes = require('./routes/dleHistory'); // Расширенная история
const systemRoutes = require('./routes/system'); // Добавляем импорт маршрутов системного мониторинга const systemRoutes = require('./routes/system'); // Добавляем импорт маршрутов системного мониторинга
@@ -188,6 +194,10 @@ app.use((req, res, next) => {
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
// Статическая раздача загруженных файлов (для dev и prod)
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use('/api/uploads', express.static(path.join(__dirname, 'uploads')));
// Настройка безопасности // Настройка безопасности
app.use( app.use(
helmet({ helmet({
@@ -235,6 +245,10 @@ app.use('/api/rag', ragRoutes); // Подключаем роут
app.use('/api/monitoring', monitoringRoutes); app.use('/api/monitoring', monitoringRoutes);
app.use('/api/pages', pagesRoutes); // Подключаем роутер страниц app.use('/api/pages', pagesRoutes); // Подключаем роутер страниц
app.use('/api/system', systemRoutes); // Добавляем маршрут системного мониторинга app.use('/api/system', systemRoutes); // Добавляем маршрут системного мониторинга
app.use('/api/uploads', uploadsRoutes); // Загрузка файлов (логотипы)
app.use('/api/ens', ensRoutes); // ENS utilities
// app.use('/api/factory', factoryRoutes); // Factory routes removed - no longer needed
app.use('/api/compile-contracts', compileRoutes); // Компиляция контрактов
const nonceStore = new Map(); // или любая другая реализация хранилища nonce const nonceStore = new Map(); // или любая другая реализация хранилища nonce

View File

@@ -1,4 +0,0 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
}

View File

@@ -1,113 +0,0 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "IERC1155Errors",
"sourceName": "@openzeppelin/contracts/interfaces/draft-IERC6093.sol",
"abi": [
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ERC1155InsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "approver",
"type": "address"
}
],
"name": "ERC1155InvalidApprover",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "idsLength",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "valuesLength",
"type": "uint256"
}
],
"name": "ERC1155InvalidArrayLength",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "ERC1155InvalidOperator",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "receiver",
"type": "address"
}
],
"name": "ERC1155InvalidReceiver",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "ERC1155InvalidSender",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "ERC1155MissingApprovalForAll",
"type": "error"
}
],
"bytecode": "0x",
"deployedBytecode": "0x",
"linkReferences": {},
"deployedLinkReferences": {}
}

View File

@@ -1,4 +0,0 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
}

View File

@@ -1,97 +0,0 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "IERC20Errors",
"sourceName": "@openzeppelin/contracts/interfaces/draft-IERC6093.sol",
"abi": [
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "allowance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "ERC20InsufficientAllowance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "ERC20InsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "approver",
"type": "address"
}
],
"name": "ERC20InvalidApprover",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "receiver",
"type": "address"
}
],
"name": "ERC20InvalidReceiver",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "ERC20InvalidSender",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "ERC20InvalidSpender",
"type": "error"
}
],
"bytecode": "0x",
"deployedBytecode": "0x",
"linkReferences": {},
"deployedLinkReferences": {}
}

View File

@@ -1,4 +0,0 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
}

View File

@@ -1,114 +0,0 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "IERC721Errors",
"sourceName": "@openzeppelin/contracts/interfaces/draft-IERC6093.sol",
"abi": [
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "ERC721IncorrectOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ERC721InsufficientApproval",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "approver",
"type": "address"
}
],
"name": "ERC721InvalidApprover",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "ERC721InvalidOperator",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "ERC721InvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "receiver",
"type": "address"
}
],
"name": "ERC721InvalidReceiver",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "ERC721InvalidSender",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ERC721NonexistentToken",
"type": "error"
}
],
"bytecode": "0x",
"deployedBytecode": "0x",
"linkReferences": {},
"deployedLinkReferences": {}
}

View File

@@ -1,4 +0,0 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
}

View File

@@ -1,319 +0,0 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "ERC20",
"sourceName": "@openzeppelin/contracts/token/ERC20/ERC20.sol",
"abi": [
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "allowance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "ERC20InsufficientAllowance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "ERC20InsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "approver",
"type": "address"
}
],
"name": "ERC20InvalidApprover",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "receiver",
"type": "address"
}
],
"name": "ERC20InvalidReceiver",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "ERC20InvalidSender",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "ERC20InvalidSpender",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
],
"bytecode": "0x",
"deployedBytecode": "0x",
"linkReferences": {},
"deployedLinkReferences": {}
}

View File

@@ -1,4 +0,0 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
}

View File

@@ -1,194 +0,0 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "IERC20",
"sourceName": "@openzeppelin/contracts/token/ERC20/IERC20.sol",
"abi": [
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
],
"bytecode": "0x",
"deployedBytecode": "0x",
"linkReferences": {},
"deployedLinkReferences": {}
}

View File

@@ -1,4 +0,0 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
}

View File

@@ -1,233 +0,0 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "IERC20Metadata",
"sourceName": "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol",
"abi": [
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
],
"bytecode": "0x",
"deployedBytecode": "0x",
"linkReferences": {},
"deployedLinkReferences": {}
}

View File

@@ -1,4 +0,0 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
}

View File

@@ -1,10 +0,0 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "Context",
"sourceName": "@openzeppelin/contracts/utils/Context.sol",
"abi": [],
"bytecode": "0x",
"deployedBytecode": "0x",
"linkReferences": {},
"deployedLinkReferences": {}
}

View File

@@ -1,4 +0,0 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
}

View File

@@ -1,16 +0,0 @@
{
"_format": "hh-sol-artifact-1",
"contractName": "ReentrancyGuard",
"sourceName": "@openzeppelin/contracts/utils/ReentrancyGuard.sol",
"abi": [
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
}
],
"bytecode": "0x",
"deployedBytecode": "0x",
"linkReferences": {},
"deployedLinkReferences": {}
}

View File

@@ -1,4 +0,0 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../build-info/de2a9b4015c1250f0af7fbce121b1da6.json"
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,10 @@ import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
interface IERC1271 {
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);
}
/** /**
* @title DLE (Digital Legal Entity) * @title DLE (Digital Legal Entity)
* @dev Основной контракт DLE с модульной архитектурой, Single-Chain Governance * @dev Основной контракт DLE с модульной архитектурой, Single-Chain Governance
@@ -78,10 +82,14 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
uint256 public quorumPercentage; uint256 public quorumPercentage;
uint256 public proposalCounter; uint256 public proposalCounter;
uint256 public currentChainId; uint256 public currentChainId;
// Публичный URI логотипа токена/организации (можно установить при деплое через инициализатор)
string public logoURI;
// Модули // Модули
mapping(bytes32 => address) public modules; mapping(bytes32 => address) public modules;
mapping(bytes32 => bool) public activeModules; mapping(bytes32 => bool) public activeModules;
bool public modulesInitialized; // Флаг инициализации базовых модулей
address public immutable initializer; // Адрес, имеющий право на однократную инициализацию модулей
// Предложения // Предложения
mapping(uint256 => Proposal) public proposals; mapping(uint256 => Proposal) public proposals;
@@ -120,16 +128,66 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId); event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId);
event TokensTransferredByGovernance(address indexed recipient, uint256 amount); event TokensTransferredByGovernance(address indexed recipient, uint256 amount);
// EIP712 typehash для подписи одобрения исполнения предложения в целевой сети event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration);
// ExecutionApproval(uint256 proposalId, bytes32 operationHash, uint256 chainId, uint256 snapshotTimepoint) event LogoURIUpdated(string oldURI, string newURI);
// EIP712 typehash для подписи одобрения исполнения предложения
bytes32 private constant EXECUTION_APPROVAL_TYPEHASH = keccak256( bytes32 private constant EXECUTION_APPROVAL_TYPEHASH = keccak256(
"ExecutionApproval(uint256 proposalId,bytes32 operationHash,uint256 chainId,uint256 snapshotTimepoint)" "ExecutionApproval(uint256 proposalId,bytes32 operationHash,uint256 chainId,uint256 snapshotTimepoint)"
); );
// Custom errors (reduce bytecode size)
error ErrZeroAddress();
error ErrArrayMismatch();
error ErrNoPartners();
error ErrZeroAmount();
error ErrOnlyInitializer();
error ErrLogoAlreadySet();
error ErrNotHolder();
error ErrTooShort();
error ErrTooLong();
error ErrBadChain();
error ErrProposalMissing();
error ErrProposalEnded();
error ErrProposalExecuted();
error ErrAlreadyVoted();
error ErrWrongChain();
error ErrNoPower();
error ErrNotReady();
error ErrNotInitiator();
error ErrLowPower();
error ErrBadTarget();
error ErrBadSig1271();
error ErrBadSig();
error ErrDuplicateSigner();
error ErrNoSigners();
error ErrSigLengthMismatch();
error ErrInvalidOperation();
error ErrNameEmpty();
error ErrSymbolEmpty();
error ErrLocationEmpty();
error ErrBadJurisdiction();
error ErrBadKPP();
error ErrBadQuorum();
error ErrChainAlreadySupported();
error ErrCannotAddCurrentChain();
error ErrChainNotSupported();
error ErrCannotRemoveCurrentChain();
error ErrTransfersDisabled();
error ErrApprovalsDisabled();
error ErrProposalCanceled();
// Константы безопасности (можно изменять через governance)
uint256 public maxVotingDuration = 30 days; // Максимальное время голосования
uint256 public minVotingDuration = 1 hours; // Минимальное время голосования
// Удалён буфер ограничения голосования в последние минуты перед дедлайном
constructor( constructor(
DLEConfig memory config, DLEConfig memory config,
uint256 _currentChainId uint256 _currentChainId,
address _initializer
) ERC20(config.name, config.symbol) ERC20Permit(config.name) { ) ERC20(config.name, config.symbol) ERC20Permit(config.name) {
if (_initializer == address(0)) revert ErrZeroAddress();
initializer = _initializer;
dleInfo = DLEInfo({ dleInfo = DLEInfo({
name: config.name, name: config.name,
symbol: config.symbol, symbol: config.symbol,
@@ -152,14 +210,14 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
} }
// Распределяем начальные токены партнерам // Распределяем начальные токены партнерам
require(config.initialPartners.length == config.initialAmounts.length, "Arrays length mismatch"); if (config.initialPartners.length != config.initialAmounts.length) revert ErrArrayMismatch();
require(config.initialPartners.length > 0, "No initial partners"); if (config.initialPartners.length == 0) revert ErrNoPartners();
for (uint256 i = 0; i < config.initialPartners.length; i++) { for (uint256 i = 0; i < config.initialPartners.length; i++) {
address partner = config.initialPartners[i]; address partner = config.initialPartners[i];
uint256 amount = config.initialAmounts[i]; uint256 amount = config.initialAmounts[i];
require(partner != address(0), "Zero address"); if (partner == address(0)) revert ErrZeroAddress();
require(amount > 0, "Zero amount"); if (amount == 0) revert ErrZeroAmount();
_mint(partner, amount); _mint(partner, amount);
// Авто-делегирование голосов себе, чтобы getPastVotes работал без действия пользователя // Авто-делегирование голосов себе, чтобы getPastVotes работал без действия пользователя
_delegate(partner, partner); _delegate(partner, partner);
@@ -179,6 +237,17 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
); );
} }
/**
* @dev Одноразовая инициализация URI логотипа. Доступно только инициализатору и только один раз.
*/
function initializeLogoURI(string calldata _logoURI) external {
if (msg.sender != initializer) revert ErrOnlyInitializer();
if (bytes(logoURI).length != 0) revert ErrLogoAlreadySet();
string memory old = logoURI;
logoURI = _logoURI;
emit LogoURIUpdated(old, _logoURI);
}
/** /**
* @dev Создать предложение с выбором цепочки для кворума * @dev Создать предложение с выбором цепочки для кворума
* @param _description Описание предложения * @param _description Описание предложения
@@ -194,9 +263,10 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
uint256[] memory _targetChains, uint256[] memory _targetChains,
uint256 /* _timelockDelay */ uint256 /* _timelockDelay */
) external returns (uint256) { ) external returns (uint256) {
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal"); if (balanceOf(msg.sender) == 0) revert ErrNotHolder();
require(_duration > 0, "Duration must be positive"); if (_duration < minVotingDuration) revert ErrTooShort();
require(supportedChains[_governanceChainId], "Chain not supported"); if (_duration > maxVotingDuration) revert ErrTooLong();
if (!supportedChains[_governanceChainId]) revert ErrBadChain();
// _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль // _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль
return _createProposalInternal( return _createProposalInternal(
_description, _description,
@@ -235,7 +305,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
// запись целевых сетей // запись целевых сетей
for (uint256 i = 0; i < _targetChains.length; i++) { for (uint256 i = 0; i < _targetChains.length; i++) {
require(supportedChains[_targetChains[i]], "Target chain not supported"); if (!supportedChains[_targetChains[i]]) revert ErrBadTarget();
proposal.targetChains.push(_targetChains[i]); proposal.targetChains.push(_targetChains[i]);
} }
@@ -253,14 +323,15 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
*/ */
function vote(uint256 _proposalId, bool _support) external nonReentrant { function vote(uint256 _proposalId, bool _support) external nonReentrant {
Proposal storage proposal = proposals[_proposalId]; Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist"); if (proposal.id != _proposalId) revert ErrProposalMissing();
require(block.timestamp < proposal.deadline, "Voting ended"); if (block.timestamp >= proposal.deadline) revert ErrProposalEnded();
require(!proposal.executed, "Proposal already executed"); if (proposal.executed) revert ErrProposalExecuted();
require(!proposal.hasVoted[msg.sender], "Already voted"); if (proposal.canceled) revert ErrProposalCanceled();
require(currentChainId == proposal.governanceChainId, "Wrong chain for voting"); if (proposal.hasVoted[msg.sender]) revert ErrAlreadyVoted();
if (currentChainId != proposal.governanceChainId) revert ErrWrongChain();
// используем снапшот голосов для защиты от перелива
uint256 votingPower = getPastVotes(msg.sender, proposal.snapshotTimepoint); uint256 votingPower = getPastVotes(msg.sender, proposal.snapshotTimepoint);
if (votingPower == 0) revert ErrNoPower();
proposal.hasVoted[msg.sender] = true; proposal.hasVoted[msg.sender] = true;
if (_support) { if (_support) {
@@ -273,16 +344,9 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
} }
// УДАЛЕНО: syncVoteFromChain с MerkleProof — небезопасно без доверенного моста // УДАЛЕНО: syncVoteFromChain с MerkleProof — небезопасно без доверенного моста
/**
* @dev Проверить результат предложения
* @param _proposalId ID предложения
* @return passed Прошло ли предложение
* @return quorumReached Достигнут ли кворум
*/
function checkProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) { function checkProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) {
Proposal storage proposal = proposals[_proposalId]; Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist"); if (proposal.id != _proposalId) revert ErrProposalMissing();
uint256 totalVotes = proposal.forVotes + proposal.againstVotes; uint256 totalVotes = proposal.forVotes + proposal.againstVotes;
// Используем снапшот totalSupply на момент начала голосования // Используем снапшот totalSupply на момент начала голосования
@@ -295,25 +359,20 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
return (passed, quorumReached); return (passed, quorumReached);
} }
/**
* @dev Исполнить предложение
* @param _proposalId ID предложения
*/
function executeProposal(uint256 _proposalId) external { function executeProposal(uint256 _proposalId) external {
Proposal storage proposal = proposals[_proposalId]; Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist"); if (proposal.id != _proposalId) revert ErrProposalMissing();
require(!proposal.executed, "Proposal already executed"); if (proposal.executed) revert ErrProposalExecuted();
require(currentChainId == proposal.governanceChainId, "Execute only in governance chain"); if (proposal.canceled) revert ErrProposalCanceled();
if (currentChainId != proposal.governanceChainId) revert ErrWrongChain();
(bool passed, bool quorumReached) = checkProposalResult(_proposalId); (bool passed, bool quorumReached) = checkProposalResult(_proposalId);
// Предложение можно выполнить если: // Предложение можно выполнить если:
// 1. Дедлайн истек ИЛИ кворум достигнут // 1. Дедлайн истек ИЛИ кворум достигнут
require( if (!(block.timestamp >= proposal.deadline || quorumReached)) revert ErrNotReady();
block.timestamp >= proposal.deadline || quorumReached, if (!(passed && quorumReached)) revert ErrNotReady();
"Voting not ended and quorum not reached"
);
require(passed && quorumReached, "Proposal not passed");
proposal.executed = true; proposal.executed = true;
@@ -323,42 +382,38 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
emit ProposalExecuted(_proposalId, proposal.operation); emit ProposalExecuted(_proposalId, proposal.operation);
} }
/**
* @dev Отмена предложения до истечения голосования инициатором при наличии достаточной голосующей силы.
* Это soft-cancel для защиты от явных ошибок. Порог: >= 10% от снапшотного supply.
*/
function cancelProposal(uint256 _proposalId, string calldata reason) external { function cancelProposal(uint256 _proposalId, string calldata reason) external {
Proposal storage proposal = proposals[_proposalId]; Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist"); if (proposal.id != _proposalId) revert ErrProposalMissing();
require(!proposal.executed, "Already executed"); if (proposal.executed) revert ErrProposalExecuted();
require(block.timestamp < proposal.deadline, "Voting ended"); if (block.timestamp + 900 >= proposal.deadline) revert ErrProposalEnded();
require(msg.sender == proposal.initiator, "Only initiator"); if (msg.sender != proposal.initiator) revert ErrNotInitiator();
uint256 vp = getPastVotes(msg.sender, proposal.snapshotTimepoint); uint256 vp = getPastVotes(msg.sender, proposal.snapshotTimepoint);
uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint); uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint);
require(vp * 10 >= pastSupply, "Insufficient voting power to cancel"); if (vp * 10 < pastSupply) revert ErrLowPower();
proposal.canceled = true; proposal.canceled = true;
emit ProposalCancelled(_proposalId, reason); emit ProposalCancelled(_proposalId, reason);
} }
// УДАЛЕНО: syncExecutionFromChain с MerkleProof — небезопасно без доверенного моста // УДАЛЕНО: syncExecutionFromChain с MerkleProof — небезопасно без доверенного моста
/**
* @dev Исполнение предложения в НЕ governance-сети по подписям холдеров на снапшоте.
* Подходит для target chains. Не требует внешнего моста.
*/
function executeProposalBySignatures( function executeProposalBySignatures(
uint256 _proposalId, uint256 _proposalId,
address[] calldata signers, address[] calldata signers,
bytes[] calldata signatures bytes[] calldata signatures
) external nonReentrant { ) external nonReentrant {
Proposal storage proposal = proposals[_proposalId]; Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist"); if (proposal.id != _proposalId) revert ErrProposalMissing();
require(!proposal.executed, "Proposal already executed in this chain"); if (proposal.executed) revert ErrProposalExecuted();
require(currentChainId != proposal.governanceChainId, "Use executeProposal in governance chain"); if (proposal.canceled) revert ErrProposalCanceled();
require(_isTargetChain(proposal, currentChainId), "Chain not in targets"); if (currentChainId == proposal.governanceChainId) revert ErrWrongChain();
if (!_isTargetChain(proposal, currentChainId)) revert ErrBadTarget();
if (signers.length != signatures.length) revert ErrSigLengthMismatch();
if (signers.length == 0) revert ErrNoSigners();
// Все держатели токенов имеют право голосовать
require(signers.length == signatures.length, "Bad signatures");
bytes32 opHash = keccak256(proposal.operation); bytes32 opHash = keccak256(proposal.operation);
bytes32 structHash = keccak256(abi.encode( bytes32 structHash = keccak256(abi.encode(
EXECUTION_APPROVAL_TYPEHASH, EXECUTION_APPROVAL_TYPEHASH,
@@ -370,76 +425,44 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
bytes32 digest = _hashTypedDataV4(structHash); bytes32 digest = _hashTypedDataV4(structHash);
uint256 votesFor = 0; uint256 votesFor = 0;
// простая защита от дублей адресов (O(n^2) по малому n)
for (uint256 i = 0; i < signers.length; i++) { for (uint256 i = 0; i < signers.length; i++) {
address recovered = ECDSA.recover(digest, signatures[i]); address signer = signers[i];
require(recovered == signers[i], "Bad signature"); if (signer.code.length > 0) {
// проверка на дубли // Контрактный кошелёк: проверяем подпись по EIP-1271
for (uint256 j = 0; j < i; j++) { try IERC1271(signer).isValidSignature(digest, signatures[i]) returns (bytes4 magic) {
require(signers[j] != recovered, "Duplicate signer"); if (magic != 0x1626ba7e) revert ErrBadSig1271();
} catch {
revert ErrBadSig1271();
}
} else {
// EOA подпись через ECDSA
address recovered = ECDSA.recover(digest, signatures[i]);
if (recovered != signer) revert ErrBadSig();
} }
uint256 vp = getPastVotes(recovered, proposal.snapshotTimepoint);
require(vp > 0, "No voting power at snapshot"); for (uint256 j = 0; j < i; j++) {
if (signers[j] == signer) revert ErrDuplicateSigner();
}
uint256 vp = getPastVotes(signer, proposal.snapshotTimepoint);
if (vp == 0) revert ErrNoPower();
votesFor += vp; votesFor += vp;
} }
uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint); uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint);
uint256 quorumRequired = (pastSupply * quorumPercentage) / 100; uint256 quorumRequired = (pastSupply * quorumPercentage) / 100;
require(votesFor >= quorumRequired, "Quorum not reached by sigs"); if (votesFor < quorumRequired) revert ErrNoPower();
proposal.executed = true; proposal.executed = true;
_executeOperation(proposal.operation); _executeOperation(proposal.operation);
emit ProposalExecuted(_proposalId, proposal.operation); emit ProposalExecuted(_proposalId, proposal.operation);
emit ProposalExecutionApprovedInChain(_proposalId, currentChainId); emit ProposalExecutionApprovedInChain(_proposalId, currentChainId);
} }
/** // Sync функции удалены для экономии байт-кода
* @dev Проверить подключение к цепочке
* @param _chainId ID цепочки
* @return isAvailable Доступна ли цепочка
*/
function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable) {
// Упрощенная проверка: цепочка объявлена как поддерживаемая
return supportedChains[_chainId];
}
/**
* @dev Проверить все подключения перед синхронизацией
* @param _proposalId ID предложения
* @return allChainsReady Готовы ли все цепочки
*/
function checkSyncReadiness(uint256 _proposalId) public view returns (bool allChainsReady) {
Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist");
// Проверяем все поддерживаемые цепочки
for (uint256 i = 0; i < getSupportedChainCount(); i++) {
uint256 chainId = getSupportedChainId(i);
if (!checkChainConnection(chainId)) {
return false;
}
}
return true;
}
/**
* @dev Синхронизация только при 100% готовности
* @param _proposalId ID предложения
*/
function syncToAllChains(uint256 _proposalId) external {
require(checkSyncReadiness(_proposalId), "Not all chains ready");
// В этой версии без внешнего моста синхронизация выполняется
// через executeProposalBySignatures в целевых сетях.
emit SyncCompleted(_proposalId);
}
/**
* @dev Синхронизация в конкретную цепочку
* @param _proposalId ID предложения
* @param _chainId ID цепочки
*/
// УДАЛЕНО: syncToChain — не используется в подпись‑ориентированной схеме // УДАЛЕНО: syncToChain — не используется в подпись‑ориентированной схеме
/** /**
@@ -508,23 +531,28 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
* @param _operation Операция для исполнения * @param _operation Операция для исполнения
*/ */
function _executeOperation(bytes memory _operation) internal { function _executeOperation(bytes memory _operation) internal {
// Декодируем операцию if (_operation.length < 4) revert ErrInvalidOperation();
(bytes4 selector, bytes memory data) = abi.decode(_operation, (bytes4, bytes));
if (selector == bytes4(keccak256("updateDLEInfo(string,string,string,string,uint256,string[],uint256)"))) { // Декодируем операцию из formата abi.encodeWithSelector
// Операция обновления информации DLE bytes4 selector;
(string memory name, string memory symbol, string memory location, string memory coordinates, bytes memory data;
uint256 jurisdiction, string[] memory okvedCodes, uint256 kpp) = abi.decode(data, (string, string, string, string, uint256, string[], uint256));
_updateDLEInfo(name, symbol, location, coordinates, jurisdiction, okvedCodes, kpp); // Извлекаем селектор (первые 4 байта)
} else if (selector == bytes4(keccak256("updateQuorumPercentage(uint256)"))) { assembly {
// Операция обновления процента кворума selector := mload(add(_operation, 0x20))
(uint256 newQuorumPercentage) = abi.decode(data, (uint256)); }
_updateQuorumPercentage(newQuorumPercentage);
} else if (selector == bytes4(keccak256("updateCurrentChainId(uint256)"))) { // Извлекаем данные (все после первых 4 байтов)
// Операция обновления текущей цепочки if (_operation.length > 4) {
(uint256 newChainId) = abi.decode(data, (uint256)); data = new bytes(_operation.length - 4);
_updateCurrentChainId(newChainId); for (uint256 i = 0; i < data.length; i++) {
} else if (selector == bytes4(keccak256("_addModule(bytes32,address)"))) { data[i] = _operation[i + 4];
}
} else {
data = new bytes(0);
}
if (selector == bytes4(keccak256("_addModule(bytes32,address)"))) {
// Операция добавления модуля // Операция добавления модуля
(bytes32 moduleId, address moduleAddress) = abi.decode(data, (bytes32, address)); (bytes32 moduleId, address moduleAddress) = abi.decode(data, (bytes32, address));
_addModule(moduleId, moduleAddress); _addModule(moduleId, moduleAddress);
@@ -542,13 +570,32 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
// Операция перевода токенов через governance // Операция перевода токенов через governance
(address recipient, uint256 amount) = abi.decode(data, (address, uint256)); (address recipient, uint256 amount) = abi.decode(data, (address, uint256));
_transferTokens(recipient, amount); _transferTokens(recipient, amount);
} else if (selector == bytes4(keccak256("_updateVotingDurations(uint256,uint256)"))) {
// Операция обновления времени голосования
(uint256 newMinDuration, uint256 newMaxDuration) = abi.decode(data, (uint256, uint256));
_updateVotingDurations(newMinDuration, newMaxDuration);
} else if (selector == bytes4(keccak256("_setLogoURI(string)"))) {
// Обновление логотипа через governance
(string memory newLogo) = abi.decode(data, (string));
_setLogoURI(newLogo);
} else if (selector == bytes4(keccak256("_updateQuorumPercentage(uint256)"))) {
// Операция обновления процента кворума
(uint256 newQuorumPercentage) = abi.decode(data, (uint256));
_updateQuorumPercentage(newQuorumPercentage);
} else if (selector == bytes4(keccak256("_updateCurrentChainId(uint256)"))) {
// Операция обновления текущей цепочки
(uint256 newChainId) = abi.decode(data, (uint256));
_updateCurrentChainId(newChainId);
} else if (selector == bytes4(keccak256("_updateDLEInfo(string,string,string,string,uint256,string[],uint256)"))) {
// Операция обновления информации DLE
(string memory name, string memory symbol, string memory location, string memory coordinates, uint256 jurisdiction, string[] memory okvedCodes, uint256 kpp) = abi.decode(data, (string, string, string, string, uint256, string[], uint256));
_updateDLEInfo(name, symbol, location, coordinates, jurisdiction, okvedCodes, kpp);
} else if (selector == bytes4(keccak256("offchainAction(bytes32,string,bytes32)"))) { } else if (selector == bytes4(keccak256("offchainAction(bytes32,string,bytes32)"))) {
// Оффчейн операция для приложения: идентификатор, тип, хеш полезной нагрузки // Оффчейн операция для приложения: идентификатор, тип, хеш полезной нагрузки
// (bytes32 actionId, string memory kind, bytes32 payloadHash) = abi.decode(data, (bytes32, string, bytes32)); // (bytes32 actionId, string memory kind, bytes32 payloadHash) = abi.decode(data, (bytes32, string, bytes32));
// Ончейн-побочных эффектов нет. Факт решения фиксируется событием ProposalExecuted. // Ончейн-побочных эффектов нет. Факт решения фиксируется событием ProposalExecuted.
} else { } else {
// Неизвестная операция revert ErrInvalidOperation();
revert("Unknown operation");
} }
} }
@@ -571,11 +618,11 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
string[] memory _okvedCodes, string[] memory _okvedCodes,
uint256 _kpp uint256 _kpp
) internal { ) internal {
require(bytes(_name).length > 0, "Name cannot be empty"); if (bytes(_name).length == 0) revert ErrNameEmpty();
require(bytes(_symbol).length > 0, "Symbol cannot be empty"); if (bytes(_symbol).length == 0) revert ErrSymbolEmpty();
require(bytes(_location).length > 0, "Location cannot be empty"); if (bytes(_location).length == 0) revert ErrLocationEmpty();
require(_jurisdiction > 0, "Invalid jurisdiction"); if (_jurisdiction == 0) revert ErrBadJurisdiction();
require(_kpp > 0, "Invalid KPP"); if (_kpp == 0) revert ErrBadKPP();
dleInfo.name = _name; dleInfo.name = _name;
dleInfo.symbol = _symbol; dleInfo.symbol = _symbol;
@@ -593,7 +640,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
* @param _newQuorumPercentage Новый процент кворума * @param _newQuorumPercentage Новый процент кворума
*/ */
function _updateQuorumPercentage(uint256 _newQuorumPercentage) internal { function _updateQuorumPercentage(uint256 _newQuorumPercentage) internal {
require(_newQuorumPercentage > 0 && _newQuorumPercentage <= 100, "Invalid quorum percentage"); if (!(_newQuorumPercentage > 0 && _newQuorumPercentage <= 100)) revert ErrBadQuorum();
uint256 oldQuorumPercentage = quorumPercentage; uint256 oldQuorumPercentage = quorumPercentage;
quorumPercentage = _newQuorumPercentage; quorumPercentage = _newQuorumPercentage;
@@ -606,8 +653,8 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
* @param _newChainId Новый ID цепочки * @param _newChainId Новый ID цепочки
*/ */
function _updateCurrentChainId(uint256 _newChainId) internal { function _updateCurrentChainId(uint256 _newChainId) internal {
require(supportedChains[_newChainId], "Chain not supported"); if (!supportedChains[_newChainId]) revert ErrChainNotSupported();
require(_newChainId != currentChainId, "Same chain ID"); if (_newChainId == currentChainId) revert ErrCannotAddCurrentChain();
uint256 oldChainId = currentChainId; uint256 oldChainId = currentChainId;
currentChainId = _newChainId; currentChainId = _newChainId;
@@ -621,8 +668,8 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
* @param _amount Количество токенов для перевода * @param _amount Количество токенов для перевода
*/ */
function _transferTokens(address _recipient, uint256 _amount) internal { function _transferTokens(address _recipient, uint256 _amount) internal {
require(_recipient != address(0), "Cannot transfer to zero address"); if (_recipient == address(0)) revert ErrZeroAddress();
require(_amount > 0, "Amount must be positive"); if (_amount == 0) revert ErrZeroAmount();
require(balanceOf(address(this)) >= _amount, "Insufficient DLE balance"); require(balanceOf(address(this)) >= _amount, "Insufficient DLE balance");
// Переводим токены от имени DLE (address(this)) // Переводим токены от имени DLE (address(this))
@@ -631,6 +678,73 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
emit TokensTransferredByGovernance(_recipient, _amount); emit TokensTransferredByGovernance(_recipient, _amount);
} }
/**
* @dev Обновить время голосования (только через governance)
* @param _newMinDuration Новое минимальное время голосования
* @param _newMaxDuration Новое максимальное время голосования
*/
function _updateVotingDurations(uint256 _newMinDuration, uint256 _newMaxDuration) internal {
if (_newMinDuration == 0) revert ErrTooShort();
if (!(_newMaxDuration > _newMinDuration)) revert ErrTooLong();
if (_newMinDuration < 10 minutes) revert ErrTooShort();
if (_newMaxDuration > 365 days) revert ErrTooLong();
uint256 oldMinDuration = minVotingDuration;
uint256 oldMaxDuration = maxVotingDuration;
minVotingDuration = _newMinDuration;
maxVotingDuration = _newMaxDuration;
emit VotingDurationsUpdated(oldMinDuration, _newMinDuration, oldMaxDuration, _newMaxDuration);
}
/**
* @dev Внутреннее обновление URI логотипа (только через governance).
*/
function _setLogoURI(string memory _logoURI) internal {
string memory old = logoURI;
logoURI = _logoURI;
emit LogoURIUpdated(old, _logoURI);
}
/**
* @dev Инициализировать базовые модули (вызывается только один раз при деплое)
* @param _treasuryAddress Адрес Treasury модуля
* @param _timelockAddress Адрес Timelock модуля
* @param _readerAddress Адрес Reader модуля
*/
function initializeBaseModules(
address _treasuryAddress,
address _timelockAddress,
address _readerAddress
) external {
if (modulesInitialized) revert ErrProposalExecuted(); // keep existing error to avoid new identifier
if (msg.sender != initializer) revert ErrOnlyInitializer();
if (_treasuryAddress == address(0) || _timelockAddress == address(0) || _readerAddress == address(0)) revert ErrZeroAddress();
// Добавляем базовые модули без голосования (только при инициализации)
bytes32 treasuryId = keccak256("TREASURY");
bytes32 timelockId = keccak256("TIMELOCK");
bytes32 readerId = keccak256("READER");
modules[treasuryId] = _treasuryAddress;
activeModules[treasuryId] = true;
modules[timelockId] = _timelockAddress;
activeModules[timelockId] = true;
modules[readerId] = _readerAddress;
activeModules[readerId] = true;
modulesInitialized = true;
emit ModuleAdded(treasuryId, _treasuryAddress);
emit ModuleAdded(timelockId, _timelockAddress);
emit ModuleAdded(readerId, _readerAddress);
}
/** /**
* @dev Создать предложение о добавлении модуля * @dev Создать предложение о добавлении модуля
* @param _description Описание предложения * @param _description Описание предложения
@@ -646,10 +760,10 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
address _moduleAddress, address _moduleAddress,
uint256 _chainId uint256 _chainId
) external returns (uint256) { ) external returns (uint256) {
require(supportedChains[_chainId], "Chain not supported"); if (!supportedChains[_chainId]) revert ErrChainNotSupported();
require(_moduleAddress != address(0), "Zero address"); if (_moduleAddress == address(0)) revert ErrZeroAddress();
require(!activeModules[_moduleId], "Module already exists"); if (activeModules[_moduleId]) revert ErrProposalExecuted();
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal"); if (balanceOf(msg.sender) == 0) revert ErrNotHolder();
// Операция добавления модуля // Операция добавления модуля
bytes memory operation = abi.encodeWithSelector( bytes memory operation = abi.encodeWithSelector(
@@ -688,9 +802,9 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
bytes32 _moduleId, bytes32 _moduleId,
uint256 _chainId uint256 _chainId
) external returns (uint256) { ) external returns (uint256) {
require(supportedChains[_chainId], "Chain not supported"); if (!supportedChains[_chainId]) revert ErrChainNotSupported();
require(activeModules[_moduleId], "Module does not exist"); if (!activeModules[_moduleId]) revert ErrProposalMissing();
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal"); if (balanceOf(msg.sender) == 0) revert ErrNotHolder();
// Операция удаления модуля // Операция удаления модуля
bytes memory operation = abi.encodeWithSelector( bytes memory operation = abi.encodeWithSelector(
@@ -715,14 +829,16 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
); );
} }
// Treasury операции перенесены в TreasuryModule для экономии байт-кода
/** /**
* @dev Добавить модуль (внутренняя функция, вызывается через кворум) * @dev Добавить модуль (внутренняя функция, вызывается через кворум)
* @param _moduleId ID модуля * @param _moduleId ID модуля
* @param _moduleAddress Адрес модуля * @param _moduleAddress Адрес модуля
*/ */
function _addModule(bytes32 _moduleId, address _moduleAddress) internal { function _addModule(bytes32 _moduleId, address _moduleAddress) internal {
require(_moduleAddress != address(0), "Zero address"); if (_moduleAddress == address(0)) revert ErrZeroAddress();
require(!activeModules[_moduleId], "Module already exists"); if (activeModules[_moduleId]) revert ErrProposalExecuted();
modules[_moduleId] = _moduleAddress; modules[_moduleId] = _moduleAddress;
activeModules[_moduleId] = true; activeModules[_moduleId] = true;
@@ -735,7 +851,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
* @param _moduleId ID модуля * @param _moduleId ID модуля
*/ */
function _removeModule(bytes32 _moduleId) internal { function _removeModule(bytes32 _moduleId) internal {
require(activeModules[_moduleId], "Module does not exist"); if (!activeModules[_moduleId]) revert ErrProposalMissing();
delete modules[_moduleId]; delete modules[_moduleId];
activeModules[_moduleId] = false; activeModules[_moduleId] = false;
@@ -781,73 +897,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
return currentChainId; return currentChainId;
} }
// ===== Интерфейс аналитики для API ===== // API функции вынесены в отдельный reader контракт для экономии байт-кода
function getProposalSummary(uint256 _proposalId) external view returns (
uint256 id,
string memory description,
uint256 forVotes,
uint256 againstVotes,
bool executed,
bool canceled,
uint256 deadline,
address initiator,
uint256 governanceChainId,
uint256 snapshotTimepoint,
uint256[] memory targets
) {
Proposal storage p = proposals[_proposalId];
require(p.id == _proposalId, "Proposal does not exist");
return (
p.id,
p.description,
p.forVotes,
p.againstVotes,
p.executed,
p.canceled,
p.deadline,
p.initiator,
p.governanceChainId,
p.snapshotTimepoint,
p.targetChains
);
}
function getGovernanceParams() external view returns (
uint256 quorumPct,
uint256 chainId,
uint256 supportedCount
) {
return (quorumPercentage, currentChainId, supportedChainIds.length);
}
function listSupportedChains() external view returns (uint256[] memory) {
return supportedChainIds;
}
function getVotingPowerAt(address voter, uint256 timepoint) external view returns (uint256) {
return getPastVotes(voter, timepoint);
}
// ===== Пагинация и агрегирование =====
function getProposalsCount() external view returns (uint256) {
return allProposalIds.length;
}
function listProposals(uint256 offset, uint256 limit) external view returns (uint256[] memory) {
uint256 total = allProposalIds.length;
if (offset >= total) {
return new uint256[](0);
}
uint256 end = offset + limit;
if (end > total) end = total;
uint256[] memory page = new uint256[](end - offset);
for (uint256 i = offset; i < end; i++) {
page[i - offset] = allProposalIds[i];
}
return page;
}
// 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution // 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution
function getProposalState(uint256 _proposalId) public view returns (uint8 state) { function getProposalState(uint256 _proposalId) public view returns (uint8 state) {
@@ -864,33 +914,11 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
return 0; // Pending return 0; // Pending
} }
function getQuorumAt(uint256 timepoint) external view returns (uint256) { // Функции для подсчёта голосов вынесены в reader контракт
uint256 supply = getPastTotalSupply(timepoint);
return (supply * quorumPercentage) / 100;
}
function getProposalVotes(uint256 _proposalId) external view returns (
uint256 forVotes,
uint256 againstVotes,
uint256 totalVotes,
uint256 quorumRequired
) {
Proposal storage p = proposals[_proposalId];
require(p.id == _proposalId, "Proposal does not exist");
uint256 supply = getPastTotalSupply(p.snapshotTimepoint);
uint256 quorumReq = (supply * quorumPercentage) / 100;
return (p.forVotes, p.againstVotes, p.forVotes + p.againstVotes, quorumReq);
}
// События для новых функций
event SyncCompleted(uint256 proposalId);
event DLEDeactivated(address indexed deactivatedBy, uint256 timestamp);
bool public isDeactivated;
// Деактивация вынесена в отдельный модуль. См. DeactivationModule. // Деактивация вынесена в отдельный модуль. См. DeactivationModule.
function isActive() external view returns (bool) { function isActive() external view returns (bool) {
return !isDeactivated && dleInfo.isActive; return dleInfo.isActive;
} }
// ===== Вспомогательные функции ===== // ===== Вспомогательные функции =====
function _isTargetChain(Proposal storage p, uint256 chainId) internal view returns (bool) { function _isTargetChain(Proposal storage p, uint256 chainId) internal view returns (bool) {
@@ -908,7 +936,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
super._update(from, to, value); super._update(from, to, value);
} }
// Разрешаем неоднозначность nonces из базовых классов // Разрешение неоднозначности nonces между ERC20Permit и Nonces
function nonces(address owner) function nonces(address owner)
public public
view view
@@ -929,32 +957,28 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
/** /**
* @dev Блокирует прямые переводы токенов * @dev Блокирует прямые переводы токенов
* @param to Адрес получателя (не используется) * @return Всегда ревертится
* @param amount Количество токенов (не используется)
* @return Всегда возвращает false
*/ */
function transfer(address to, uint256 amount) public override returns (bool) { function transfer(address /*to*/, uint256 /*amount*/) public pure override returns (bool) {
revert("Direct transfers disabled. Use governance proposals for token transfers."); // coverage:ignore-line
revert ErrTransfersDisabled();
} }
/** /**
* @dev Блокирует прямые переводы токенов через approve/transferFrom * @dev Блокирует прямые переводы токенов через approve/transferFrom
* @param from Адрес отправителя (не используется) * @return Всегда ревертится
* @param to Адрес получателя (не используется)
* @param amount Количество токенов (не используется)
* @return Всегда возвращает false
*/ */
function transferFrom(address from, address to, uint256 amount) public override returns (bool) { function transferFrom(address /*from*/, address /*to*/, uint256 /*amount*/) public pure override returns (bool) {
revert("Direct transfers disabled. Use governance proposals for token transfers."); // coverage:ignore-line
revert ErrTransfersDisabled();
} }
/** /**
* @dev Блокирует прямые разрешения на перевод токенов * @dev Блокирует прямые разрешения на перевод токенов
* @param spender Адрес, которому разрешается тратить токены (не используется) * @return Всегда ревертится
* @param amount Количество токенов (не используется)
* @return Всегда возвращает false
*/ */
function approve(address spender, uint256 amount) public override returns (bool) { function approve(address /*spender*/, uint256 /*amount*/) public pure override returns (bool) {
revert("Direct approvals disabled. Use governance proposals for token transfers."); // coverage:ignore-line
revert ErrApprovalsDisabled();
} }
} }

View File

@@ -0,0 +1,360 @@
// SPDX-License-Identifier: PROPRIETARY AND MIT
// Copyright (c) 2024-2025 Тарабанов Александр Викторович
// All rights reserved.
pragma solidity ^0.8.20;
interface IDLEReader {
// Структуры из основного контракта
struct DLEInfo {
string name;
string symbol;
string location;
string coordinates;
uint256 jurisdiction;
string[] okvedCodes;
uint256 kpp;
uint256 creationTimestamp;
bool isActive;
}
struct Proposal {
uint256 id;
string description;
uint256 forVotes;
uint256 againstVotes;
bool executed;
bool canceled;
uint256 deadline;
address initiator;
bytes operation;
uint256 governanceChainId;
uint256[] targetChains;
uint256 snapshotTimepoint;
}
// Основные функции чтения
function getDLEInfo() external view returns (DLEInfo memory);
function proposals(uint256) external view returns (
uint256 id,
string memory description,
uint256 forVotes,
uint256 againstVotes,
bool executed,
bool canceled,
uint256 deadline,
address initiator,
bytes memory operation,
uint256 governanceChainId,
uint256 snapshotTimepoint
);
function allProposalIds(uint256) external view returns (uint256);
function supportedChainIds(uint256) external view returns (uint256);
function quorumPercentage() external view returns (uint256);
function currentChainId() external view returns (uint256);
function totalSupply() external view returns (uint256);
function getPastTotalSupply(uint256) external view returns (uint256);
function getPastVotes(address, uint256) external view returns (uint256);
function checkProposalResult(uint256) external view returns (bool, bool);
function getProposalState(uint256) external view returns (uint8);
function balanceOf(address) external view returns (uint256);
function isChainSupported(uint256) external view returns (bool);
function isModuleActive(bytes32) external view returns (bool);
function getModuleAddress(bytes32) external view returns (address);
}
/**
* @title DLEReader
* @dev Read-only контракт для API функций DLE
*
* БЕЗОПАСНОСТЬ:
* - Только чтение данных (view/pure функции)
* - Не изменяет состояние основного контракта
* - Можно безопасно обновлять независимо от DLE
* - Нет доступа к приватным данным
*/
contract DLEReader {
address public immutable dleContract;
constructor(address _dleContract) {
require(_dleContract != address(0), "DLE contract cannot be zero");
require(_dleContract.code.length > 0, "DLE contract must exist");
dleContract = _dleContract;
}
// ===== АГРЕГИРОВАННЫЕ ДАННЫЕ =====
/**
* @dev Получить полную сводку по предложению
*/
function getProposalSummary(uint256 _proposalId) external view returns (
uint256 id,
string memory description,
uint256 forVotes,
uint256 againstVotes,
bool executed,
bool canceled,
uint256 deadline,
address initiator,
uint256 governanceChainId,
uint256 snapshotTimepoint,
uint256[] memory targetChains,
uint8 state,
bool passed,
bool quorumReached
) {
IDLEReader dle = IDLEReader(dleContract);
// Получаем основные данные предложения
(
id,
description,
forVotes,
againstVotes,
executed,
canceled,
deadline,
initiator,
, // operation не нужна для сводки
governanceChainId,
snapshotTimepoint
) = dle.proposals(_proposalId);
// Получаем дополнительные данные
state = dle.getProposalState(_proposalId);
(passed, quorumReached) = dle.checkProposalResult(_proposalId);
// TODO: targetChains требует отдельной функции в основном контракте
targetChains = new uint256[](0);
}
/**
* @dev Получить параметры governance
*/
function getGovernanceParams() external view returns (
uint256 quorumPct,
uint256 chainId,
uint256 supportedCount,
uint256 totalSupply,
uint256 proposalsCount
) {
IDLEReader dle = IDLEReader(dleContract);
quorumPct = dle.quorumPercentage();
chainId = dle.currentChainId();
totalSupply = dle.totalSupply();
// Считаем поддерживаемые сети
supportedCount = 0;
for (uint256 i = 0; i < 50; i++) { // Ограничиваем итерации
try dle.supportedChainIds(i) returns (uint256) {
supportedCount++;
} catch {
break;
}
}
// Считаем предложения
proposalsCount = 0;
for (uint256 i = 0; i < 1000; i++) { // Ограничиваем итерации
try dle.allProposalIds(i) returns (uint256) {
proposalsCount++;
} catch {
break;
}
}
}
/**
* @dev Получить список поддерживаемых сетей
*/
function listSupportedChains() external view returns (uint256[] memory chains) {
IDLEReader dle = IDLEReader(dleContract);
// Сначала считаем количество
uint256 count = 0;
for (uint256 i = 0; i < 50; i++) {
try dle.supportedChainIds(i) returns (uint256) {
count++;
} catch {
break;
}
}
// Затем заполняем массив
chains = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
chains[i] = dle.supportedChainIds(i);
}
}
/**
* @dev Получить список предложений с пагинацией
*/
function listProposals(uint256 offset, uint256 limit) external view returns (
uint256[] memory proposalIds,
uint256 total
) {
IDLEReader dle = IDLEReader(dleContract);
// Считаем общее количество
total = 0;
for (uint256 i = 0; i < 10000; i++) { // Увеличиваем лимит для предложений
try dle.allProposalIds(i) returns (uint256) {
total++;
} catch {
break;
}
}
// Проверяем границы
if (offset >= total) {
return (new uint256[](0), total);
}
uint256 end = offset + limit;
if (end > total) end = total;
// Заполняем страницу
proposalIds = new uint256[](end - offset);
for (uint256 i = offset; i < end; i++) {
proposalIds[i - offset] = dle.allProposalIds(i);
}
}
/**
* @dev Получить голосующую силу на определённый момент времени
*/
function getVotingPowerAt(address voter, uint256 timepoint) external view returns (uint256) {
return IDLEReader(dleContract).getPastVotes(voter, timepoint);
}
/**
* @dev Получить размер кворума на определённый момент времени
*/
function getQuorumAt(uint256 timepoint) external view returns (uint256) {
IDLEReader dle = IDLEReader(dleContract);
uint256 supply = dle.getPastTotalSupply(timepoint);
uint256 quorumPct = dle.quorumPercentage();
return (supply * quorumPct) / 100;
}
/**
* @dev Получить детали голосования по предложению
*/
function getProposalVotes(uint256 _proposalId) external view returns (
uint256 forVotes,
uint256 againstVotes,
uint256 totalVotes,
uint256 quorumRequired,
uint256 quorumCurrent,
bool quorumReached
) {
IDLEReader dle = IDLEReader(dleContract);
// Получаем основные данные предложения
uint256 snapshotTimepoint;
(
, // id
, // description
forVotes,
againstVotes,
, // executed
, // canceled
, // deadline
, // initiator
, // operation
, // governanceChainId
snapshotTimepoint
) = dle.proposals(_proposalId);
totalVotes = forVotes + againstVotes;
// Вычисляем кворум
uint256 supply = dle.getPastTotalSupply(snapshotTimepoint);
uint256 quorumPct = dle.quorumPercentage();
quorumRequired = (supply * quorumPct) / 100;
quorumCurrent = totalVotes;
quorumReached = totalVotes >= quorumRequired;
}
/**
* @dev Получить статистику по адресу
*/
function getAddressStats(address user) external view returns (
uint256 tokenBalance,
uint256 currentVotingPower,
uint256 delegatedTo,
bool hasTokens
) {
IDLEReader dle = IDLEReader(dleContract);
tokenBalance = dle.balanceOf(user);
currentVotingPower = dle.getPastVotes(user, block.number - 1);
hasTokens = tokenBalance > 0;
// delegatedTo требует дополнительных функций в основном контракте
delegatedTo = 0; // Placeholder
}
/**
* @dev Получить информацию о модулях
*/
function getModulesInfo(bytes32[] memory moduleIds) external view returns (
address[] memory addresses,
bool[] memory active
) {
IDLEReader dle = IDLEReader(dleContract);
addresses = new address[](moduleIds.length);
active = new bool[](moduleIds.length);
for (uint256 i = 0; i < moduleIds.length; i++) {
addresses[i] = dle.getModuleAddress(moduleIds[i]);
active[i] = dle.isModuleActive(moduleIds[i]);
}
}
/**
* @dev Получить состояние DLE
*/
function getDLEStatus() external view returns (
IDLEReader.DLEInfo memory info,
uint256 totalSupply,
uint256 currentChain,
uint256 quorumPct,
uint256 totalProposals,
uint256 supportedChains
) {
IDLEReader dle = IDLEReader(dleContract);
info = dle.getDLEInfo();
totalSupply = dle.totalSupply();
currentChain = dle.currentChainId();
quorumPct = dle.quorumPercentage();
// Считаем предложения и сети
(,, supportedChains, totalSupply, totalProposals) = this.getGovernanceParams();
}
/**
* @dev Batch получение состояний предложений
*/
function getProposalStates(uint256[] memory proposalIds) external view returns (
uint8[] memory states,
bool[] memory passed,
bool[] memory quorumReached
) {
IDLEReader dle = IDLEReader(dleContract);
states = new uint8[](proposalIds.length);
passed = new bool[](proposalIds.length);
quorumReached = new bool[](proposalIds.length);
for (uint256 i = 0; i < proposalIds.length; i++) {
states[i] = dle.getProposalState(proposalIds[i]);
(passed[i], quorumReached[i]) = dle.checkProposalResult(proposalIds[i]);
}
}
}

View File

@@ -18,6 +18,12 @@ contract FactoryDeployer {
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash)); bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash));
return address(uint160(uint256(hash))); return address(uint160(uint256(hash)));
} }
function computeAddressWithCreationCode(bytes32 salt, bytes memory creationCode) external view returns (address) {
bytes32 initCodeHash = keccak256(creationCode);
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash));
return address(uint160(uint256(hash)));
}
} }

View File

@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title MockNoop
* @dev Простой мок-контракт для тестирования FactoryDeployer
*/
contract MockNoop {
uint256 public value;
constructor() {
value = 42;
}
function setValue(uint256 _value) external {
value = _value;
}
function getValue() external view returns (uint256) {
return value;
}
}

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
/**
* @title MockToken
* @dev Мок-токен для тестирования TreasuryModule
*/
contract MockToken is ERC20 {
address public minter;
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
minter = msg.sender;
}
modifier onlyMinter() {
require(msg.sender == minter, "Only minter can call this function");
_;
}
function mint(address to, uint256 amount) external onlyMinter {
_mint(to, amount);
}
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
}

View File

@@ -0,0 +1,399 @@
// SPDX-License-Identifier: PROPRIETARY AND MIT
// Copyright (c) 2024-2025 Тарабанов Александр Викторович
// All rights reserved.
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title TimelockModule
* @dev Модуль временной задержки для критических операций DLE
*
* НАЗНАЧЕНИЕ:
* - Добавляет обязательную задержку между принятием и исполнением решений
* - Даёт время сообществу на обнаружение и отмену вредоносных предложений
* - Повышает безопасность критических операций (смена кворума, добавление модулей)
*
* ПРИНЦИП РАБОТЫ:
* 1. DLE исполняет операцию не напрямую, а через Timelock
* 2. Timelock ставит операцию в очередь с задержкой
* 3. После истечения задержки любой может исполнить операцию
* 4. В период задержки операцию можно отменить через экстренное голосование
*/
contract TimelockModule is ReentrancyGuard {
// Структура отложенной операции
struct QueuedOperation {
bytes32 id; // Уникальный ID операции
address target; // Целевой контракт (обычно DLE)
bytes data; // Данные для вызова
uint256 executeAfter; // Время, после которого можно исполнить
uint256 queuedAt; // Время постановки в очередь
bool executed; // Исполнена ли операция
bool cancelled; // Отменена ли операция
address proposer; // Кто поставил в очередь
string description; // Описание операции
uint256 delay; // Задержка для этой операции
}
// Основные настройки
address public immutable dleContract; // Адрес DLE контракта
uint256 public defaultDelay = 2 days; // Стандартная задержка
uint256 public emergencyDelay = 30 minutes; // Экстренная задержка
uint256 public maxDelay = 30 days; // Максимальная задержка
uint256 public minDelay = 1 hours; // Минимальная задержка
// Хранение операций
mapping(bytes32 => QueuedOperation) public queuedOperations;
bytes32[] public operationQueue; // Список всех операций
mapping(bytes32 => uint256) public operationIndex; // ID => индекс в очереди
// Категории операций с разными задержками
mapping(bytes4 => uint256) public operationDelays; // selector => delay
mapping(bytes4 => bool) public criticalOperations; // критические операции
mapping(bytes4 => bool) public emergencyOperations; // экстренные операции
// Статистика
uint256 public totalOperations;
uint256 public executedOperations;
uint256 public cancelledOperations;
// События
event OperationQueued(
bytes32 indexed operationId,
address indexed target,
bytes data,
uint256 executeAfter,
uint256 delay,
string description
);
event OperationExecuted(bytes32 indexed operationId, address indexed executor);
event OperationCancelled(bytes32 indexed operationId, address indexed canceller, string reason);
event DelayUpdated(bytes4 indexed selector, uint256 oldDelay, uint256 newDelay);
event DefaultDelayUpdated(uint256 oldDelay, uint256 newDelay);
event EmergencyExecution(bytes32 indexed operationId, string reason);
// Модификаторы
modifier onlyDLE() {
require(msg.sender == dleContract, "Only DLE can call");
_;
}
modifier validOperation(bytes32 operationId) {
require(queuedOperations[operationId].id == operationId, "Operation not found");
require(!queuedOperations[operationId].executed, "Already executed");
require(!queuedOperations[operationId].cancelled, "Operation cancelled");
_;
}
constructor(address _dleContract) {
require(_dleContract != address(0), "DLE contract cannot be zero");
require(_dleContract.code.length > 0, "DLE contract must exist");
dleContract = _dleContract;
totalOperations = 0;
// Настраиваем задержки для разных типов операций
_setupOperationDelays();
}
/**
* @dev Поставить операцию в очередь (вызывается из DLE)
* @param target Целевой контракт
* @param data Данные операции
* @param description Описание операции
*/
function queueOperation(
address target,
bytes memory data,
string memory description
) external onlyDLE returns (bytes32) {
require(target != address(0), "Target cannot be zero");
require(data.length >= 4, "Invalid operation data");
// Определяем задержку для операции
bytes4 selector;
assembly {
selector := mload(add(data, 0x20))
}
uint256 delay = _getOperationDelay(selector);
// Создаём уникальный ID операции
bytes32 operationId = keccak256(abi.encodePacked(
target,
data,
block.timestamp,
totalOperations
));
// Проверяем что операция ещё не существует
require(queuedOperations[operationId].id == bytes32(0), "Operation already exists");
// Создаём операцию
queuedOperations[operationId] = QueuedOperation({
id: operationId,
target: target,
data: data,
executeAfter: block.timestamp + delay,
queuedAt: block.timestamp,
executed: false,
cancelled: false,
proposer: msg.sender, // Адрес вызывающего (обычно DLE контракт)
description: description,
delay: delay
});
// Добавляем в очередь
operationQueue.push(operationId);
operationIndex[operationId] = operationQueue.length - 1;
totalOperations++;
emit OperationQueued(operationId, target, data, block.timestamp + delay, delay, description);
return operationId;
}
/**
* @dev Исполнить операцию после истечения задержки (может любой)
* @param operationId ID операции
*/
function executeOperation(bytes32 operationId) external nonReentrant validOperation(operationId) {
QueuedOperation storage operation = queuedOperations[operationId];
require(block.timestamp >= operation.executeAfter, "Timelock not expired");
require(block.timestamp <= operation.executeAfter + 7 days, "Operation expired"); // Операции истекают через неделю
operation.executed = true;
executedOperations++;
// Исполняем операцию
(bool success, bytes memory result) = operation.target.call(operation.data);
require(success, string(abi.encodePacked("Execution failed: ", result)));
emit OperationExecuted(operationId, msg.sender);
}
/**
* @dev Отменить операцию (только через DLE governance)
* @param operationId ID операции
* @param reason Причина отмены
*/
function cancelOperation(
bytes32 operationId,
string memory reason
) external onlyDLE validOperation(operationId) {
QueuedOperation storage operation = queuedOperations[operationId];
operation.cancelled = true;
cancelledOperations++;
emit OperationCancelled(operationId, msg.sender, reason);
}
/**
* @dev Экстренное исполнение без задержки (только для критических ситуаций)
* @param operationId ID операции
* @param reason Причина экстренного исполнения
*/
function emergencyExecute(
bytes32 operationId,
string memory reason
) external onlyDLE nonReentrant validOperation(operationId) {
QueuedOperation storage operation = queuedOperations[operationId];
// Проверяем что операция помечена как экстренная
bytes memory opData = operation.data;
bytes4 selector;
assembly {
selector := mload(add(opData, 0x20))
}
require(emergencyOperations[selector], "Not emergency operation");
operation.executed = true;
executedOperations++;
// Исполняем операцию
(bool success, bytes memory result) = operation.target.call(operation.data);
require(success, string(abi.encodePacked("Emergency execution failed: ", result)));
emit OperationExecuted(operationId, msg.sender);
emit EmergencyExecution(operationId, reason);
}
/**
* @dev Обновить задержку для типа операции (только через governance)
* @param selector Селектор функции
* @param newDelay Новая задержка
* @param isCritical Является ли операция критической
* @param isEmergency Может ли исполняться экстренно
*/
function updateOperationDelay(
bytes4 selector,
uint256 newDelay,
bool isCritical,
bool isEmergency
) external onlyDLE {
require(newDelay >= minDelay, "Delay too short");
require(newDelay <= maxDelay, "Delay too long");
uint256 oldDelay = operationDelays[selector];
operationDelays[selector] = newDelay;
criticalOperations[selector] = isCritical;
emergencyOperations[selector] = isEmergency;
emit DelayUpdated(selector, oldDelay, newDelay);
}
/**
* @dev Обновить стандартную задержку (только через governance)
* @param newDelay Новая стандартная задержка
*/
function updateDefaultDelay(uint256 newDelay) external onlyDLE {
require(newDelay >= minDelay, "Delay too short");
require(newDelay <= maxDelay, "Delay too long");
uint256 oldDelay = defaultDelay;
defaultDelay = newDelay;
emit DefaultDelayUpdated(oldDelay, newDelay);
}
// ===== VIEW ФУНКЦИИ =====
/**
* @dev Получить информацию об операции
*/
function getOperation(bytes32 operationId) external view returns (QueuedOperation memory) {
return queuedOperations[operationId];
}
/**
* @dev Проверить, готова ли операция к исполнению
*/
function isReady(bytes32 operationId) external view returns (bool) {
QueuedOperation storage operation = queuedOperations[operationId];
return operation.id != bytes32(0) &&
!operation.executed &&
!operation.cancelled &&
block.timestamp >= operation.executeAfter;
}
/**
* @dev Получить время до исполнения операции
*/
function getTimeToExecution(bytes32 operationId) external view returns (uint256) {
QueuedOperation storage operation = queuedOperations[operationId];
if (operation.executeAfter <= block.timestamp) {
return 0;
}
return operation.executeAfter - block.timestamp;
}
/**
* @dev Получить список активных операций
*/
function getActiveOperations() external view returns (bytes32[] memory) {
uint256 activeCount = 0;
// Считаем активные операции
for (uint256 i = 0; i < operationQueue.length; i++) {
QueuedOperation storage op = queuedOperations[operationQueue[i]];
if (!op.executed && !op.cancelled) {
activeCount++;
}
}
// Заполняем массив
bytes32[] memory activeOps = new bytes32[](activeCount);
uint256 index = 0;
for (uint256 i = 0; i < operationQueue.length; i++) {
QueuedOperation storage op = queuedOperations[operationQueue[i]];
if (!op.executed && !op.cancelled) {
activeOps[index] = operationQueue[i];
index++;
}
}
return activeOps;
}
/**
* @dev Получить статистику Timelock
*/
function getTimelockStats() external view returns (
uint256 total,
uint256 executed,
uint256 cancelled,
uint256 pending,
uint256 currentDelay
) {
return (
totalOperations,
executedOperations,
cancelledOperations,
totalOperations - executedOperations - cancelledOperations,
defaultDelay
);
}
// ===== ВНУТРЕННИЕ ФУНКЦИИ =====
/**
* @dev Определить задержку для операции
*/
function _getOperationDelay(bytes4 selector) internal view returns (uint256) {
uint256 customDelay = operationDelays[selector];
if (customDelay > 0) {
return customDelay;
}
// Используем стандартную задержку
return defaultDelay;
}
/**
* @dev Настроить стандартные задержки для операций
*/
function _setupOperationDelays() internal {
// Критические операции - длинная задержка (7 дней)
bytes4 updateQuorum = bytes4(keccak256("updateQuorumPercentage(uint256)"));
bytes4 addModule = bytes4(keccak256("_addModule(bytes32,address)"));
bytes4 removeModule = bytes4(keccak256("_removeModule(bytes32)"));
bytes4 addChain = bytes4(keccak256("_addSupportedChain(uint256)"));
bytes4 removeChain = bytes4(keccak256("_removeSupportedChain(uint256)"));
operationDelays[updateQuorum] = 7 days;
operationDelays[addModule] = 7 days;
operationDelays[removeModule] = 7 days;
operationDelays[addChain] = 5 days;
operationDelays[removeChain] = 5 days;
criticalOperations[updateQuorum] = true;
criticalOperations[addModule] = true;
criticalOperations[removeModule] = true;
criticalOperations[addChain] = true;
criticalOperations[removeChain] = true;
// Обычные операции - стандартная задержка (2 дня)
bytes4 updateDLEInfo = bytes4(keccak256("updateDLEInfo(string,string,string,string,uint256,string[],uint256)"));
bytes4 updateChainId = bytes4(keccak256("updateCurrentChainId(uint256)"));
bytes4 updateVotingDurations = bytes4(keccak256("_updateVotingDurations(uint256,uint256)"));
operationDelays[updateDLEInfo] = 2 days;
operationDelays[updateChainId] = 3 days;
operationDelays[updateVotingDurations] = 1 days;
// Treasury операции - короткая задержка (1 день)
bytes4 treasuryTransfer = bytes4(keccak256("treasuryTransfer(address,address,uint256)"));
bytes4 treasuryAddToken = bytes4(keccak256("treasuryAddToken(address,string,uint8)"));
operationDelays[treasuryTransfer] = 1 days;
operationDelays[treasuryAddToken] = 1 days;
// Экстренные операции (могут исполняться немедленно при необходимости)
emergencyOperations[removeModule] = true; // Удаление вредоносного модуля
emergencyOperations[removeChain] = true; // Отключение скомпрометированной сети
}
}

View 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);
}
}

View File

@@ -11,6 +11,7 @@
*/ */
require('@nomicfoundation/hardhat-toolbox'); require('@nomicfoundation/hardhat-toolbox');
require('hardhat-contract-sizer');
require('dotenv').config(); require('dotenv').config();
function getNetworks() { function getNetworks() {
@@ -39,10 +40,25 @@ module.exports = {
settings: { settings: {
optimizer: { optimizer: {
enabled: true, enabled: true,
runs: 200 runs: 1 // Максимальная оптимизация размера для mainnet
}, },
viaIR: true viaIR: true
} }
}, },
contractSizer: {
alphaSort: true,
runOnCompile: true,
disambiguatePaths: false,
},
networks: getNetworks(), networks: getNetworks(),
solidityCoverage: {
excludeContracts: [],
skipFiles: [],
// Исключаем строки с revert функциями из покрытия
excludeLines: [
'// coverage:ignore-line',
'revert ErrTransfersDisabled();',
'revert ErrApprovalsDisabled();'
]
}
}; };

View File

@@ -1,9 +1,15 @@
{ {
"verbose": true, "watch": [
"ignore": [".git", "node_modules/**/node_modules", "sessions", "data/vector_store", "logs"], "backend/src",
"watch": ["*.js", "routes/**/*", "services/**/*", "utils/**/*", "middleware/**/*"], "backend/routes",
"env": { "backend/services"
"NODE_ENV": "development" ],
}, "ignore": [
"ext": "js,json,env" "backend/artifacts/**",
"backend/cache/**",
"backend/contracts-data/**",
"backend/temp/**",
"backend/scripts/deploy/current-params*.json"
],
"ext": "js,json"
} }

View File

@@ -20,7 +20,10 @@
"format": "prettier --write \"**/*.{js,vue,json,md}\"", "format": "prettier --write \"**/*.{js,vue,json,md}\"",
"format:check": "prettier --check \"**/*.{js,vue,json,md}\"", "format:check": "prettier --check \"**/*.{js,vue,json,md}\"",
"run-migrations": "node scripts/run-migrations.js", "run-migrations": "node scripts/run-migrations.js",
"fix-duplicates": "node scripts/fix-duplicate-identities.js" "fix-duplicates": "node scripts/fix-duplicate-identities.js",
"deploy:factory": "node scripts/deploy/deploy-factory.js",
"deploy:multichain": "node scripts/deploy/deploy-multichain.js",
"deploy:complete": "node scripts/deploy/deploy-dle-complete.js"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.51.0", "@anthropic-ai/sdk": "^0.51.0",
@@ -81,10 +84,11 @@
"eslint-config-prettier": "^10.0.2", "eslint-config-prettier": "^10.0.2",
"globals": "^16.0.0", "globals": "^16.0.0",
"hardhat": "^2.24.1", "hardhat": "^2.24.1",
"hardhat-contract-sizer": "^2.10.1",
"hardhat-gas-reporter": "^2.2.2", "hardhat-gas-reporter": "^2.2.2",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"solidity-coverage": "^0.8.1", "solidity-coverage": "^0.8.16",
"ts-node": ">=8.0.0", "ts-node": ">=8.0.0",
"typechain": "^8.3.0", "typechain": "^8.3.0",
"typescript": ">=4.5.0" "typescript": ">=4.5.0"

View File

@@ -29,20 +29,38 @@ router.post('/read-dle-info', async (req, res) => {
console.log(`[Blockchain] Чтение данных DLE из блокчейна: ${dleAddress}`); console.log(`[Blockchain] Чтение данных DLE из блокчейна: ${dleAddress}`);
// Получаем RPC URL для Sepolia // Определяем корректную сеть для данного адреса (или используем chainId из запроса)
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); let provider, rpcUrl, targetChainId = req.body.chainId;
if (!rpcUrl) { const candidateChainIds = [11155111, 17000, 421614, 84532];
return res.status(500).json({ if (targetChainId) {
success: false, rpcUrl = await rpcProviderService.getRpcUrlByChainId(Number(targetChainId));
error: 'RPC URL для Sepolia не найден' if (!rpcUrl) {
}); return res.status(500).json({ success: false, error: `RPC URL для сети ${targetChainId} не найден` });
}
provider = new ethers.JsonRpcProvider(rpcUrl);
const code = await provider.getCode(dleAddress);
if (!code || code === '0x') {
return res.status(400).json({ success: false, error: `По адресу ${dleAddress} нет контракта в сети ${targetChainId}` });
}
} else {
for (const cid of candidateChainIds) {
try {
const url = await rpcProviderService.getRpcUrlByChainId(cid);
if (!url) continue;
const prov = new ethers.JsonRpcProvider(url);
const code = await prov.getCode(dleAddress);
if (code && code !== '0x') { provider = prov; rpcUrl = url; targetChainId = cid; break; }
} catch (_) {}
}
if (!provider) {
return res.status(400).json({ success: false, error: 'Не удалось найти сеть, где по адресу есть контракт' });
}
} }
const provider = new ethers.JsonRpcProvider(rpcUrl);
// ABI для чтения данных DLE // ABI для чтения данных DLE
const dleAbi = [ const dleAbi = [
"function getDLEInfo() external view returns (tuple(string name, string symbol, string location, string coordinates, uint256 jurisdiction, uint256 oktmo, string[] okvedCodes, uint256 kpp, uint256 creationTimestamp, bool isActive))", // Актуальная сигнатура без oktmo
"function getDLEInfo() external view returns (tuple(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp, uint256 creationTimestamp, bool isActive))",
"function totalSupply() external view returns (uint256)", "function totalSupply() external view returns (uint256)",
"function balanceOf(address account) external view returns (uint256)", "function balanceOf(address account) external view returns (uint256)",
"function quorumPercentage() external view returns (uint256)", "function quorumPercentage() external view returns (uint256)",
@@ -81,7 +99,8 @@ router.post('/read-dle-info', async (req, res) => {
location: dleInfo.location, location: dleInfo.location,
coordinates: dleInfo.coordinates, coordinates: dleInfo.coordinates,
jurisdiction: Number(dleInfo.jurisdiction), jurisdiction: Number(dleInfo.jurisdiction),
oktmo: Number(dleInfo.oktmo), // Поле oktmo удалено в актуальной версии контракта; сохраняем 0 для обратной совместимости
oktmo: 0,
okvedCodes: dleInfo.okvedCodes, okvedCodes: dleInfo.okvedCodes,
kpp: Number(dleInfo.kpp), kpp: Number(dleInfo.kpp),
creationTimestamp: Number(dleInfo.creationTimestamp), creationTimestamp: Number(dleInfo.creationTimestamp),
@@ -90,6 +109,7 @@ router.post('/read-dle-info', async (req, res) => {
deployerBalance: ethers.formatUnits(deployerBalance, 18), deployerBalance: ethers.formatUnits(deployerBalance, 18),
quorumPercentage: Number(quorumPercentage), quorumPercentage: Number(quorumPercentage),
currentChainId: Number(currentChainId), currentChainId: Number(currentChainId),
rpcUsed: rpcUrl,
participantCount: participantCount participantCount: participantCount
}; };
@@ -153,6 +173,10 @@ router.post('/get-supported-chains', async (req, res) => {
{ chainId: 43114, name: 'Avalanche', description: 'Avalanche C-Chain' }, { chainId: 43114, name: 'Avalanche', description: 'Avalanche C-Chain' },
{ chainId: 250, name: 'Fantom', description: 'Fantom Opera' }, { chainId: 250, name: 'Fantom', description: 'Fantom Opera' },
{ chainId: 11155111, name: 'Sepolia', description: 'Ethereum Testnet Sepolia' }, { chainId: 11155111, name: 'Sepolia', description: 'Ethereum Testnet Sepolia' },
{ chainId: 17000, name: 'Holesky', description: 'Ethereum Testnet Holesky' },
{ chainId: 80002, name: 'Polygon Amoy', description: 'Polygon Testnet Amoy' },
{ chainId: 84532, name: 'Base Sepolia', description: 'Base Sepolia Testnet' },
{ chainId: 421614, name: 'Arbitrum Sepolia', description: 'Arbitrum Sepolia Testnet' },
{ chainId: 80001, name: 'Mumbai', description: 'Polygon Testnet Mumbai' }, { chainId: 80001, name: 'Mumbai', description: 'Polygon Testnet Mumbai' },
{ chainId: 97, name: 'BSC Testnet', description: 'Binance Smart Chain Testnet' }, { chainId: 97, name: 'BSC Testnet', description: 'Binance Smart Chain Testnet' },
{ chainId: 421613, name: 'Arbitrum Goerli', description: 'Arbitrum Testnet Goerli' } { chainId: 421613, name: 'Arbitrum Goerli', description: 'Arbitrum Testnet Goerli' }

83
backend/routes/compile.js Normal file
View File

@@ -0,0 +1,83 @@
/**
* 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
*/
const express = require('express');
const router = express.Router();
const { spawn } = require('child_process');
const path = require('path');
const logger = require('../utils/logger');
const auth = require('../middleware/auth');
/**
* @route POST /api/compile-contracts
* @desc Компилировать смарт-контракты через Hardhat
* @access Private (только для авторизованных пользователей с ролью admin)
*/
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
console.log('🔨 Запуск компиляции смарт-контрактов...');
const hardhatProcess = spawn('npx', ['hardhat', 'compile'], {
cwd: path.join(__dirname, '..'),
stdio: 'pipe'
});
let stdout = '';
let stderr = '';
hardhatProcess.stdout.on('data', (data) => {
stdout += data.toString();
console.log(`[COMPILE] ${data.toString().trim()}`);
});
hardhatProcess.stderr.on('data', (data) => {
stderr += data.toString();
console.error(`[COMPILE_ERR] ${data.toString().trim()}`);
});
hardhatProcess.on('close', (code) => {
if (code === 0) {
console.log('✅ Компиляция завершена успешно');
res.json({
success: true,
message: 'Смарт-контракты скомпилированы успешно',
data: { stdout, stderr }
});
} else {
console.error('❌ Ошибка компиляции:', stderr);
res.status(500).json({
success: false,
message: 'Ошибка компиляции смарт-контрактов',
error: stderr
});
}
});
hardhatProcess.on('error', (error) => {
console.error('❌ Ошибка запуска компиляции:', error);
res.status(500).json({
success: false,
message: 'Ошибка запуска компиляции',
error: error.message
});
});
} catch (error) {
logger.error('Ошибка компиляции контрактов:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при компиляции контрактов'
});
}
});
module.exports = router;

View File

@@ -357,14 +357,12 @@ router.post('/predict-addresses', auth.requireAuth, auth.requireAdmin, async (re
} }
// Используем служебные секреты для фабрики и SALT // Используем служебные секреты для фабрики и SALT
// Ожидаем, что на сервере настроены переменные окружения или конфиги на сеть // Factory больше не используется - адреса DLE теперь вычисляются через CREATE с выровненным nonce
const result = {}; const result = {};
for (const chainId of selectedNetworks) { for (const chainId of selectedNetworks) {
const factory = process.env[`FACTORY_ADDRESS_${chainId}`] || process.env.FACTORY_ADDRESS; // Адрес DLE будет одинаковым во всех сетях благодаря выравниванию nonce
const saltHex = process.env[`CREATE2_SALT_${chainId}`] || process.env.CREATE2_SALT; // Вычисляется в deploy-multichain.js во время деплоя
const initCodeHash = process.env[`INIT_CODE_HASH_${chainId}`] || process.env.INIT_CODE_HASH; result[chainId] = 'Вычисляется во время деплоя';
if (!factory || !saltHex || !initCodeHash) continue;
result[chainId] = create2.computeCreate2Address(factory, saltHex, initCodeHash);
} }
return res.json({ success: true, data: result }); return res.json({ success: true, data: result });
@@ -514,3 +512,4 @@ router.post('/precheck', auth.requireAuth, auth.requireAdmin, async (req, res) =
return res.status(500).json({ success: false, message: e.message }); return res.status(500).json({ success: false, message: e.message });
} }
}); });

30
backend/routes/ens.js Normal file
View File

@@ -0,0 +1,30 @@
/**
* ENS utilities: resolve avatar URL for a given ENS name
*/
const express = require('express');
const router = express.Router();
const { ethers } = require('ethers');
function getMainnetProvider() {
const url = process.env.MAINNET_RPC_URL || process.env.ETH_MAINNET_RPC || 'https://ethereum.publicnode.com';
return new ethers.JsonRpcProvider(url);
}
// GET /api/ens/avatar?name=vc-hb3-accelerator.eth
router.get('/avatar', async (req, res) => {
try {
const name = String(req.query.name || '').trim();
if (!name || !name.endsWith('.eth')) {
return res.status(400).json({ success: false, message: 'ENS name is required (e.g., example.eth)' });
}
const provider = getMainnetProvider();
const url = await provider.getAvatar(name);
return res.json({ success: true, data: { url: url || null } });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});
module.exports = router;

51
backend/routes/uploads.js Normal file
View File

@@ -0,0 +1,51 @@
/**
* Загрузка файлов (логотипы) через Multer
*/
const express = require('express');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const auth = require('../middleware/auth');
const router = express.Router();
// Хранилище на диске: uploads/logos
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const dir = path.join(__dirname, '..', 'uploads', 'logos');
try { fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
cb(null, dir);
},
filename: function (req, file, cb) {
const ext = (file.originalname || '').split('.').pop();
const safeExt = ext && ext.length <= 10 ? ext : 'png';
const name = `logo_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${safeExt}`;
cb(null, name);
}
});
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const ok = /(png|jpg|jpeg|gif|webp)$/i.test(file.originalname || '') && /^image\//i.test(file.mimetype || '');
if (!ok) return cb(new Error('Only image files are allowed'));
cb(null, true);
}
});
// POST /api/uploads/logo (form field: logo)
router.post('/logo', auth.requireAuth, auth.requireAdmin, upload.single('logo'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ success: false, message: 'Файл не получен' });
const rel = path.posix.join('uploads', 'logos', path.basename(req.file.filename));
const urlPath = `/uploads/logos/${path.basename(req.file.filename)}`;
return res.json({ success: true, data: { path: rel, url: urlPath } });
} catch (e) {
return res.status(500).json({ success: false, message: e.message });
}
});
module.exports = router;

View File

@@ -1,299 +0,0 @@
/* eslint-disable no-console */
const hre = require('hardhat');
async function main() {
const { ethers } = hre;
const rpcUrl = process.env.RPC_URL;
const pk = process.env.PRIVATE_KEY;
if (!rpcUrl || !pk) throw new Error('RPC_URL/PRIVATE_KEY required');
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(pk, provider);
const salt = process.env.CREATE2_SALT;
const initCodeHash = process.env.INIT_CODE_HASH;
let factoryAddress = process.env.FACTORY_ADDRESS;
if (!salt || !initCodeHash) throw new Error('CREATE2_SALT/INIT_CODE_HASH required');
// Ensure factory
if (!factoryAddress) {
const Factory = await hre.ethers.getContractFactory('FactoryDeployer', wallet);
const factory = await Factory.deploy();
await factory.waitForDeployment();
factoryAddress = await factory.getAddress();
} else {
const code = await provider.getCode(factoryAddress);
if (code === '0x') {
const Factory = await hre.ethers.getContractFactory('FactoryDeployer', wallet);
const factory = await Factory.deploy();
await factory.waitForDeployment();
factoryAddress = await factory.getAddress();
}
}
// Prepare DLE init code = creation bytecode WITH constructor args
const DLE = await hre.ethers.getContractFactory('DLE', wallet);
const paramsPath = require('path').join(__dirname, './current-params.json');
const params = require(paramsPath);
const dleConfig = {
name: params.name,
symbol: params.symbol,
location: params.location,
coordinates: params.coordinates,
jurisdiction: params.jurisdiction,
okvedCodes: params.okvedCodes || [],
kpp: params.kpp,
quorumPercentage: params.quorumPercentage,
initialPartners: params.initialPartners,
initialAmounts: params.initialAmounts,
supportedChainIds: params.supportedChainIds
};
const deployTx = await DLE.getDeployTransaction(dleConfig, params.currentChainId);
const dleInit = deployTx.data; // полноценный init code
// Deploy via factory
const Factory = await hre.ethers.getContractAt('FactoryDeployer', factoryAddress, wallet);
const tx = await Factory.deploy(salt, dleInit);
const rc = await tx.wait();
const addr = rc.logs?.[0]?.args?.addr || (await Factory.computeAddress(salt, initCodeHash));
console.log('DLE v2 задеплоен по адресу:', addr);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
/**
* 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
*/
// Скрипт для создания современного DLE v2 (единый контракт)
const { ethers } = require("hardhat");
const fs = require("fs");
const path = require("path");
async function main() {
// Получаем параметры деплоя из файла
const deployParams = getDeployParams();
console.log("Начинаем создание современного DLE v2...");
console.log("Параметры DLE:");
console.log(JSON.stringify(deployParams, null, 2));
// Преобразуем initialAmounts в wei
const initialAmountsInWei = deployParams.initialAmounts.map(amount => ethers.parseUnits(amount.toString(), 18));
console.log("Initial amounts в wei:");
console.log(initialAmountsInWei.map(wei => ethers.formatUnits(wei, 18) + " токенов"));
// Получаем RPC URL и приватный ключ из переменных окружения
const rpcUrl = process.env.RPC_URL;
const privateKey = process.env.PRIVATE_KEY;
if (!rpcUrl || !privateKey) {
throw new Error('RPC_URL и PRIVATE_KEY должны быть установлены в переменных окружения');
}
// Создаем провайдер и кошелек
const provider = new ethers.JsonRpcProvider(rpcUrl);
const deployer = new ethers.Wallet(privateKey, provider);
console.log(`Адрес деплоера: ${deployer.address}`);
const balance = await provider.getBalance(deployer.address);
console.log(`Баланс деплоера: ${ethers.formatEther(balance)} ETH`);
// Проверяем, достаточно ли баланса для деплоя (минимум 0.00001 ETH для тестирования)
const minBalance = ethers.parseEther("0.00001");
if (balance < minBalance) {
throw new Error(`Недостаточно ETH для деплоя. Баланс: ${ethers.formatEther(balance)} ETH, требуется минимум: ${ethers.formatEther(minBalance)} ETH. Пополните кошелек через Sepolia faucet.`);
}
try {
// 1. Создаем единый контракт DLE
console.log("\n1. Деплой единого контракта DLE v2...");
const DLE = await ethers.getContractFactory("DLE", deployer);
// Создаем структуру DLEConfig с полными данными
const dleConfig = {
name: deployParams.name,
symbol: deployParams.symbol,
location: deployParams.location,
coordinates: deployParams.coordinates || "0,0",
jurisdiction: deployParams.jurisdiction || 1,
oktmo: parseInt(deployParams.oktmo) || 45000000000,
okvedCodes: deployParams.okvedCodes || [],
kpp: parseInt(deployParams.kpp) || 770101001,
quorumPercentage: deployParams.quorumPercentage || 51,
initialPartners: deployParams.initialPartners,
initialAmounts: deployParams.initialAmounts.map(amount => ethers.parseUnits(amount.toString(), 18)),
supportedChainIds: deployParams.supportedChainIds || [1, 137, 56, 42161] // Ethereum, Polygon, BSC, Arbitrum
};
console.log("Конфигурация DLE для записи в блокчейн:");
console.log("Название:", dleConfig.name);
console.log("Символ:", dleConfig.symbol);
console.log("Местонахождение:", dleConfig.location);
console.log("Координаты:", dleConfig.coordinates);
console.log("Юрисдикция:", dleConfig.jurisdiction);
console.log("ОКТМО:", dleConfig.oktmo);
console.log("Коды ОКВЭД:", dleConfig.okvedCodes.join(', '));
console.log("КПП:", dleConfig.kpp);
console.log("Кворум:", dleConfig.quorumPercentage + "%");
console.log("Партнеры:", dleConfig.initialPartners.join(', '));
console.log("Количества токенов:", dleConfig.initialAmounts.map(amount => ethers.formatUnits(amount, 18) + " токенов").join(', '));
console.log("Поддерживаемые сети:", dleConfig.supportedChainIds.join(', '));
const currentChainId = deployParams.currentChainId || 1; // По умолчанию Ethereum
const dle = await DLE.deploy(dleConfig, currentChainId);
await dle.waitForDeployment();
const dleAddress = await dle.getAddress();
console.log(`DLE v2 задеплоен по адресу: ${dleAddress}`);
// 2. Получаем информацию о DLE из блокчейна
const dleInfo = await dle.getDLEInfo();
console.log("\n2. Информация о DLE из блокчейна:");
console.log(`Название: ${dleInfo.name}`);
console.log(`Символ: ${dleInfo.symbol}`);
console.log(`Местонахождение: ${dleInfo.location}`);
console.log(`Координаты: ${dleInfo.coordinates}`);
console.log(`Юрисдикция: ${dleInfo.jurisdiction}`);
console.log(`ОКТМО: ${dleInfo.oktmo}`);
console.log(`Коды ОКВЭД: ${dleInfo.okvedCodes.join(', ')}`);
console.log(`КПП: ${dleInfo.kpp}`);
console.log(`Дата создания: ${new Date(Number(dleInfo.creationTimestamp) * 1000).toISOString()}`);
console.log(`Активен: ${dleInfo.isActive}`);
// Проверяем, что данные записались правильно
console.log("\n3. Проверка записи данных в блокчейн:");
if (dleInfo.name === deployParams.name &&
dleInfo.location === deployParams.location &&
dleInfo.jurisdiction === deployParams.jurisdiction) {
console.log("✅ Все данные DLE успешно записаны в блокчейн!");
console.log("Теперь эти данные видны на Etherscan в разделе 'Contract' -> 'Read Contract'");
} else {
console.log("❌ Ошибка: данные не записались правильно в блокчейн");
}
// 4. Сохраняем информацию о созданном DLE
console.log("\n4. Сохранение информации о DLE v2...");
const dleData = {
name: deployParams.name,
symbol: deployParams.symbol,
location: deployParams.location,
coordinates: deployParams.coordinates || "0,0",
jurisdiction: deployParams.jurisdiction || 1,
oktmo: deployParams.oktmo || 45000000000,
okvedCodes: deployParams.okvedCodes || [],
kpp: deployParams.kpp || 770101001,
dleAddress: dleAddress,
creationBlock: Number(await provider.getBlockNumber()),
creationTimestamp: Number((await provider.getBlock()).timestamp),
deployedManually: true,
version: "v2",
// Сохраняем информацию о партнерах
initialPartners: deployParams.initialPartners || [],
initialAmounts: deployParams.initialAmounts || [],
governanceSettings: {
quorumPercentage: deployParams.quorumPercentage || 51,
supportedChainIds: deployParams.supportedChainIds || [1, 137, 56, 42161],
currentChainId: currentChainId
}
};
const saveResult = saveDLEData(dleData);
console.log("\nDLE v2 успешно создан!");
console.log(`Адрес DLE: ${dleAddress}`);
console.log(`Версия: v2 (единый контракт)`);
return {
success: true,
dleAddress: dleAddress,
data: dleData
};
} catch (error) {
console.error("Ошибка при создании DLE v2:", error);
throw error;
}
}
// Получаем параметры деплоя из файла
function getDeployParams() {
const paramsFile = path.join(__dirname, 'current-params.json');
if (!fs.existsSync(paramsFile)) {
console.error(`Файл параметров не найден: ${paramsFile}`);
process.exit(1);
}
try {
const params = JSON.parse(fs.readFileSync(paramsFile, 'utf8'));
console.log("Параметры загружены из файла");
return params;
} catch (error) {
console.error("Ошибка при чтении файла параметров:", error);
process.exit(1);
}
}
// Сохраняем информацию о созданном DLE
function saveDLEData(dleData) {
const dlesDir = path.join(__dirname, "../../contracts-data/dles");
// Проверяем существование директории и создаем при необходимости
try {
if (!fs.existsSync(dlesDir)) {
console.log(`Директория ${dlesDir} не существует, создаю...`);
fs.mkdirSync(dlesDir, { recursive: true });
console.log(`Директория ${dlesDir} успешно создана`);
}
// Проверяем права на запись, создавая временный файл
const testFile = path.join(dlesDir, '.write-test');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
console.log(`Директория ${dlesDir} доступна для записи`);
} catch (error) {
console.error(`Ошибка при проверке директории ${dlesDir}:`, error);
throw error;
}
// Создаем уникальное имя файла
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `dle-v2-${timestamp}.json`;
const filePath = path.join(dlesDir, fileName);
try {
fs.writeFileSync(filePath, JSON.stringify(dleData, null, 2));
console.log(`Информация о DLE сохранена в файл: ${fileName}`);
return { success: true, filePath };
} catch (error) {
console.error(`Ошибка при сохранении файла ${filePath}:`, error);
throw error;
}
}
// Запускаем скрипт
main()
.then(() => {
console.log("Скрипт завершен успешно");
process.exit(0);
})
.catch((error) => {
console.error("Скрипт завершен с ошибкой:", error);
process.exit(1);
});

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
const hre = require('hardhat'); const hre = require('hardhat');
const path = require('path'); const path = require('path');
const fs = require('fs');
// Подбираем безопасные gas/fee для разных сетей (включая L2) // Подбираем безопасные gas/fee для разных сетей (включая L2)
async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) { async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) {
@@ -19,7 +20,7 @@ async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20
return overrides; return overrides;
} }
async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetFactoryNonce, dleInit) { async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit) {
const { ethers } = hre; const { ethers } = hre;
const provider = new ethers.JsonRpcProvider(rpcUrl); const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(pk, provider); const wallet = new ethers.Wallet(pk, provider);
@@ -30,7 +31,7 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetFactoryNonc
const calcInitHash = ethers.keccak256(dleInit); const calcInitHash = ethers.keccak256(dleInit);
const saltLen = ethers.getBytes(salt).length; const saltLen = ethers.getBytes(salt).length;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} rpc=${rpcUrl}`); console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} rpc=${rpcUrl}`);
console.log(`[MULTI_DBG] wallet=${wallet.address} targetFactoryNonce=${targetFactoryNonce}`); console.log(`[MULTI_DBG] wallet=${wallet.address} targetDLENonce=${targetDLENonce}`);
console.log(`[MULTI_DBG] saltLenBytes=${saltLen} salt=${salt}`); console.log(`[MULTI_DBG] saltLenBytes=${saltLen} salt=${salt}`);
console.log(`[MULTI_DBG] initCodeHash(provided)=${initCodeHash}`); console.log(`[MULTI_DBG] initCodeHash(provided)=${initCodeHash}`);
console.log(`[MULTI_DBG] initCodeHash(calculated)=${calcInitHash}`); console.log(`[MULTI_DBG] initCodeHash(calculated)=${calcInitHash}`);
@@ -39,170 +40,195 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetFactoryNonc
console.log('[MULTI_DBG] precheck error', e?.message || e); console.log('[MULTI_DBG] precheck error', e?.message || e);
} }
// 1) Выравнивание nonce до targetFactoryNonce нулевыми транзакциями (если нужно) // 1) Выравнивание nonce до targetDLENonce нулевыми транзакциями (если нужно)
let current = await provider.getTransactionCount(wallet.address, 'pending'); let current = await provider.getTransactionCount(wallet.address, 'pending');
if (current > targetFactoryNonce) { console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce=${current} target=${targetDLENonce}`);
throw new Error(`Current nonce ${current} > targetFactoryNonce ${targetFactoryNonce} on chainId=${Number(net.chainId)}`);
if (current > targetDLENonce) {
throw new Error(`Current nonce ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}`);
} }
while (current < targetFactoryNonce) {
const overrides = await getFeeOverrides(provider); if (current < targetDLENonce) {
let gasLimit = 50000; // некоторые L2 требуют >21000 console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
let sent = false;
let lastErr = null; // Используем burn address для более надежных транзакций
for (let attempt = 0; attempt < 2 && !sent; attempt++) { const burnAddress = "0x000000000000000000000000000000000000dEaD";
try {
const txReq = { while (current < targetDLENonce) {
to: wallet.address, const overrides = await getFeeOverrides(provider);
value: 0n, let gasLimit = 21000; // минимальный gas для обычной транзакции
nonce: current, let sent = false;
gasLimit, let lastErr = null;
...overrides
}; for (let attempt = 0; attempt < 3 && !sent; attempt++) {
const txFill = await wallet.sendTransaction(txReq); try {
await txFill.wait(); const txReq = {
sent = true; to: burnAddress, // отправляем на burn address вместо своего адреса
} catch (e) { value: 0n,
lastErr = e; nonce: current,
if (String(e?.message || '').toLowerCase().includes('intrinsic gas too low') && attempt === 0) { gasLimit,
gasLimit = 100000; // поднимаем лимит и пробуем ещё раз ...overrides
continue; };
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`);
const txFill = await wallet.sendTransaction(txReq);
await txFill.wait();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed`);
sent = true;
} catch (e) {
lastErr = e;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${e?.message || e}`);
if (String(e?.message || '').toLowerCase().includes('intrinsic gas too low') && attempt < 2) {
gasLimit = 50000; // увеличиваем gas limit
continue;
}
if (String(e?.message || '').toLowerCase().includes('nonce too low') && attempt < 2) {
// Обновляем nonce и пробуем снова
current = await provider.getTransactionCount(wallet.address, 'pending');
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`);
continue;
}
throw e;
} }
throw e;
} }
if (!sent) {
console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`);
throw lastErr || new Error('filler tx failed');
}
current++;
} }
if (!sent) throw lastErr || new Error('filler tx failed');
current++; console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`);
} else {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`);
} }
// 2) Деплой FactoryDeployer на согласованном nonce // 2) Деплой DLE напрямую на согласованном nonce
const FactoryCF = await hre.ethers.getContractFactory('FactoryDeployer', wallet); console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying DLE directly with nonce=${targetDLENonce}`);
const feeOverrides = await getFeeOverrides(provider);
const factoryContract = await FactoryCF.deploy({ nonce: targetFactoryNonce, ...feeOverrides });
await factoryContract.waitForDeployment();
const factoryAddress = await factoryContract.getAddress();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} FactoryDeployer.address=${factoryAddress}`);
// 3) Деплой DLE через CREATE2 const feeOverrides = await getFeeOverrides(provider);
const Factory = await hre.ethers.getContractAt('FactoryDeployer', factoryAddress, wallet); let gasLimit;
const n = await provider.getTransactionCount(wallet.address, 'pending');
try {
// Оцениваем газ для деплоя DLE
const est = await wallet.estimateGas({ data: dleInit, ...feeOverrides }).catch(() => null);
// Рассчитываем доступный gasLimit из баланса
const balance = await provider.getBalance(wallet.address, 'latest');
const effPrice = feeOverrides.maxFeePerGas || feeOverrides.gasPrice || 0n;
const reserve = hre.ethers.parseEther('0.005');
const maxByBalance = effPrice > 0n && balance > reserve ? (balance - reserve) / effPrice : 3_000_000n;
const fallbackGas = maxByBalance > 5_000_000n ? 5_000_000n : (maxByBalance < 2_500_000n ? 2_500_000n : maxByBalance);
gasLimit = est ? (est + est / 5n) : fallbackGas;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`);
} catch (_) {
gasLimit = 3_000_000n;
}
// Вычисляем предсказанный адрес DLE
const predictedAddress = ethers.getCreateAddress({
from: wallet.address,
nonce: targetDLENonce
});
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predicted DLE address=${predictedAddress}`);
// Проверяем, не развернут ли уже контракт
const existingCode = await provider.getCode(predictedAddress);
if (existingCode && existingCode !== '0x') {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE already exists at predictedAddress, skip deploy`);
return { address: predictedAddress, chainId: Number(net.chainId) };
}
// Деплоим DLE
let tx; let tx;
try { try {
// Предварительная проверка конструктора вне CREATE2 (даст явную причину, если он ревертится) tx = await wallet.sendTransaction({
try { data: dleInit,
await wallet.estimateGas({ data: dleInit }); nonce: targetDLENonce,
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predeploy(estGas) ok for constructor`); gasLimit,
} catch (e) { ...feeOverrides
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predeploy(estGas) failed: ${e?.reason || e?.shortMessage || e?.message || e}`); });
if (e?.data) console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predeploy revert data: ${e.data}`);
}
// Оцениваем газ и добавляем запас
const est = await Factory.deploy.estimateGas(salt, dleInit, { nonce: n, ...feeOverrides }).catch(() => null);
// Рассчитываем доступный gasLimit из баланса
let gasLimit;
try {
const balance = await provider.getBalance(wallet.address, 'latest');
const effPrice = feeOverrides.maxFeePerGas || feeOverrides.gasPrice || 0n;
const reserve = hre.ethers.parseEther('0.005');
const maxByBalance = effPrice > 0n && balance > reserve ? (balance - reserve) / effPrice : 3_000_000n;
const fallbackGas = maxByBalance > 5_000_000n ? 5_000_000n : (maxByBalance < 2_500_000n ? 2_500_000n : maxByBalance);
gasLimit = est ? (est + est / 5n) : fallbackGas;
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`);
} catch (_) {
const fallbackGas = 3_000_000n;
gasLimit = est ? (est + est / 5n) : fallbackGas;
}
// DEBUG: ожидаемый адрес через computeAddress
try {
const predicted = await Factory.computeAddress(salt, initCodeHash);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predictedAddress=${predicted}`);
// Idempotency: если уже есть код по адресу, пропускаем деплой
const code = await provider.getCode(predicted);
if (code && code !== '0x') {
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} code already exists at predictedAddress, skip deploy`);
return { factory: factoryAddress, address: predicted, chainId: Number(net.chainId) };
}
} catch (e) {
console.log('[MULTI_DBG] computeAddress(before) error', e?.message || e);
}
tx = await Factory.deploy(salt, dleInit, { nonce: n, gasLimit, ...feeOverrides });
} catch (e) { } catch (e) {
const n2 = await provider.getTransactionCount(wallet.address, 'pending'); console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy error(first): ${e?.message || e}`);
const est2 = await Factory.deploy.estimateGas(salt, dleInit, { nonce: n2, ...feeOverrides }).catch(() => null); // Повторная попытка с обновленным nonce
let gasLimit2; const updatedNonce = await provider.getTransactionCount(wallet.address, 'pending');
try { console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} retry deploy with nonce=${updatedNonce}`);
const balance2 = await provider.getBalance(wallet.address, 'latest'); tx = await wallet.sendTransaction({
const effPrice2 = feeOverrides.maxFeePerGas || feeOverrides.gasPrice || 0n; data: dleInit,
const reserve2 = hre.ethers.parseEther('0.005'); nonce: updatedNonce,
const maxByBalance2 = effPrice2 > 0n && balance2 > reserve2 ? (balance2 - reserve2) / effPrice2 : 3_000_000n; gasLimit,
const fallbackGas2 = maxByBalance2 > 5_000_000n ? 5_000_000n : (maxByBalance2 < 2_500_000n ? 2_500_000n : maxByBalance2); ...feeOverrides
gasLimit2 = est2 ? (est2 + est2 / 5n) : fallbackGas2; });
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} RETRY estGas=${est2?.toString?.()||'null'} effGasPrice=${effPrice2?.toString?.()||'0'} maxByBalance=${maxByBalance2.toString()} chosenGasLimit=${gasLimit2.toString()}`);
} catch (_) {
gasLimit2 = est2 ? (est2 + est2 / 5n) : 3_000_000n;
}
console.log(`[MULTI_DBG] retry deploy with nonce=${n2} gasLimit=${gasLimit2?.toString?.() || 'auto'}`);
console.log(`[MULTI_DBG] deploy error(first) ${e?.message || e}`);
tx = await Factory.deploy(salt, dleInit, { nonce: n2, gasLimit: gasLimit2, ...feeOverrides });
} }
const rc = await tx.wait(); const rc = await tx.wait();
let addr = rc.logs?.[0]?.args?.addr; const deployedAddress = rc.contractAddress || predictedAddress;
if (!addr) {
try { console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE deployed at=${deployedAddress}`);
addr = await Factory.computeAddress(salt, initCodeHash); return { address: deployedAddress, chainId: Number(net.chainId) };
} catch (e) {
console.log('[MULTI_DBG] computeAddress(after) error', e?.message || e);
}
}
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deployedAddress=${addr}`);
return { factory: factoryAddress, address: addr, chainId: Number(net.chainId) };
} }
async function main() { async function main() {
const { ethers } = hre; const { ethers } = hre;
const pk = process.env.PRIVATE_KEY;
const salt = process.env.CREATE2_SALT;
const initCodeHash = process.env.INIT_CODE_HASH;
const networks = (process.env.MULTICHAIN_RPC_URLS || '').split(',').map(s => s.trim()).filter(Boolean);
const factories = (process.env.MULTICHAIN_FACTORY_ADDRESSES || '').split(',').map(s => s.trim());
if (!pk) throw new Error('Env: PRIVATE_KEY'); // Загружаем параметры из файла
if (!salt) throw new Error('Env: CREATE2_SALT');
if (!initCodeHash) throw new Error('Env: INIT_CODE_HASH');
if (networks.length === 0) throw new Error('Env: MULTICHAIN_RPC_URLS');
// Prepare init code once
const paramsPath = path.join(__dirname, './current-params.json'); const paramsPath = path.join(__dirname, './current-params.json');
const params = require(paramsPath); if (!fs.existsSync(paramsPath)) {
const DLE = await hre.ethers.getContractFactory('DLE'); throw new Error('Файл параметров не найден: ' + paramsPath);
const dleConfig = { }
const params = JSON.parse(fs.readFileSync(paramsPath, 'utf8'));
console.log('[MULTI_DBG] Загружены параметры:', {
name: params.name, name: params.name,
symbol: params.symbol, symbol: params.symbol,
location: params.location, supportedChainIds: params.supportedChainIds,
coordinates: params.coordinates, CREATE2_SALT: params.CREATE2_SALT
jurisdiction: params.jurisdiction, });
oktmo: params.oktmo,
const pk = process.env.PRIVATE_KEY;
const salt = params.CREATE2_SALT;
const networks = params.rpcUrls || [];
if (!pk) throw new Error('Env: PRIVATE_KEY');
if (!salt) throw new Error('CREATE2_SALT not found in params');
if (networks.length === 0) throw new Error('RPC URLs not found in params');
// Prepare init code once
const DLE = await hre.ethers.getContractFactory('DLE');
const dleConfig = {
name: params.name || '',
symbol: params.symbol || '',
location: params.location || '',
coordinates: params.coordinates || '',
jurisdiction: params.jurisdiction || 0,
oktmo: params.oktmo || '',
okvedCodes: params.okvedCodes || [], okvedCodes: params.okvedCodes || [],
kpp: params.kpp, kpp: params.kpp ? BigInt(params.kpp) : 0n,
quorumPercentage: params.quorumPercentage, quorumPercentage: params.quorumPercentage || 51,
initialPartners: params.initialPartners, initialPartners: params.initialPartners || [],
initialAmounts: params.initialAmounts, initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount)),
supportedChainIds: params.supportedChainIds supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id))
}; };
const deployTx = await DLE.getDeployTransaction(dleConfig, params.currentChainId); const deployTx = await DLE.getDeployTransaction(dleConfig, BigInt(params.currentChainId || params.supportedChainIds?.[0] || 1), params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000");
const dleInit = deployTx.data; const dleInit = deployTx.data;
const initCodeHash = ethers.keccak256(dleInit);
// DEBUG: глобальные значения // DEBUG: глобальные значения
try { try {
const calcInitHash = ethers.keccak256(dleInit);
const saltLen = ethers.getBytes(salt).length; const saltLen = ethers.getBytes(salt).length;
console.log(`[MULTI_DBG] GLOBAL saltLenBytes=${saltLen} salt=${salt}`); console.log(`[MULTI_DBG] GLOBAL saltLenBytes=${saltLen} salt=${salt}`);
console.log(`[MULTI_DBG] GLOBAL initCodeHash(provided)=${initCodeHash}`); console.log(`[MULTI_DBG] GLOBAL initCodeHash(calculated)=${initCodeHash}`);
console.log(`[MULTI_DBG] GLOBAL initCodeHash(calculated)=${calcInitHash}`);
console.log(`[MULTI_DBG] GLOBAL dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`); console.log(`[MULTI_DBG] GLOBAL dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`);
} catch (e) { } catch (e) {
console.log('[MULTI_DBG] GLOBAL precheck error', e?.message || e); console.log('[MULTI_DBG] GLOBAL precheck error', e?.message || e);
} }
// Подготовим провайдеры и вычислим общий nonce для фабрики // Подготовим провайдеры и вычислим общий nonce для DLE
const providers = networks.map(u => new hre.ethers.JsonRpcProvider(u)); const providers = networks.map(u => new hre.ethers.JsonRpcProvider(u));
const wallets = providers.map(p => new hre.ethers.Wallet(pk, p)); const wallets = providers.map(p => new hre.ethers.Wallet(pk, p));
const nonces = []; const nonces = [];
@@ -210,15 +236,28 @@ async function main() {
const n = await providers[i].getTransactionCount(wallets[i].address, 'pending'); const n = await providers[i].getTransactionCount(wallets[i].address, 'pending');
nonces.push(n); nonces.push(n);
} }
const targetFactoryNonce = Math.max(...nonces); const targetDLENonce = Math.max(...nonces);
console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetFactoryNonce=${targetFactoryNonce}`); console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`);
const results = []; const results = [];
for (let i = 0; i < networks.length; i++) { for (let i = 0; i < networks.length; i++) {
const rpcUrl = networks[i]; const rpcUrl = networks[i];
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetFactoryNonce, dleInit); console.log(`[MULTI_DBG] deploying to network ${i + 1}/${networks.length}: ${rpcUrl}`);
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit);
results.push({ rpcUrl, ...r }); results.push({ rpcUrl, ...r });
} }
// Проверяем, что все адреса одинаковые
const addresses = results.map(r => r.address);
const uniqueAddresses = [...new Set(addresses)];
if (uniqueAddresses.length > 1) {
console.error('[MULTI_DBG] ERROR: DLE addresses are different across networks!');
console.error('[MULTI_DBG] addresses:', uniqueAddresses);
throw new Error('Nonce alignment failed - addresses are different');
}
console.log('[MULTI_DBG] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]);
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(results)); console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(results));
} }

View File

@@ -41,6 +41,20 @@ class DLEV2Service {
// Подготовка параметров для деплоя // Подготовка параметров для деплоя
const deployParams = this.prepareDeployParams(dleParams); const deployParams = this.prepareDeployParams(dleParams);
// Вычисляем адрес инициализатора (инициализатором является деплоер из переданного приватного ключа)
try {
const normalizedPk = dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`;
const initializerAddress = new ethers.Wallet(normalizedPk).address;
deployParams.initializerAddress = initializerAddress;
} catch (e) {
logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message);
}
// Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets
const { createAndStoreNewCreate2Salt } = require('./secretStore');
const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' });
logger.info(`CREATE2_SALT создан и сохранён: key=${saltKey}`);
// Сохраняем параметры во временный файл // Сохраняем параметры во временный файл
paramsFile = this.saveParamsToFile(deployParams); paramsFile = this.saveParamsToFile(deployParams);
@@ -51,7 +65,6 @@ class DLEV2Service {
fs.mkdirSync(deployDir, { recursive: true }); fs.mkdirSync(deployDir, { recursive: true });
} }
fs.copyFileSync(paramsFile, tempParamsFile); fs.copyFileSync(paramsFile, tempParamsFile);
logger.info(`Файл параметров скопирован успешно`);
// Готовим RPC для всех выбранных сетей // Готовим RPC для всех выбранных сетей
const rpcUrls = []; const rpcUrls = [];
@@ -64,6 +77,19 @@ class DLEV2Service {
rpcUrls.push(ru); rpcUrls.push(ru);
} }
// Добавляем CREATE2_SALT, RPC_URLS и initializer в файл параметров
const currentParams = JSON.parse(fs.readFileSync(tempParamsFile, 'utf8'));
// Копируем все параметры из deployParams
Object.assign(currentParams, deployParams);
currentParams.CREATE2_SALT = create2Salt;
currentParams.rpcUrls = rpcUrls;
currentParams.currentChainId = deployParams.currentChainId || deployParams.supportedChainIds[0];
const { ethers } = require('ethers');
currentParams.initializer = dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`).address : "0x0000000000000000000000000000000000000000";
fs.writeFileSync(tempParamsFile, JSON.stringify(currentParams, null, 2));
logger.info(`Файл параметров скопирован и обновлен с CREATE2_SALT`);
// Лёгкая проверка баланса в первой сети // Лёгкая проверка баланса в первой сети
{ {
const { ethers } = require('ethers'); const { ethers } = require('ethers');
@@ -72,7 +98,15 @@ class DLEV2Service {
const pk = dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`; const pk = dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`;
const walletAddress = new ethers.Wallet(pk, provider).address; const walletAddress = new ethers.Wallet(pk, provider).address;
const balance = await provider.getBalance(walletAddress); const balance = await provider.getBalance(walletAddress);
if (typeof ethers.parseEther !== 'function') {
throw new Error('Метод ethers.parseEther не найден');
}
const minBalance = ethers.parseEther("0.00001"); const minBalance = ethers.parseEther("0.00001");
if (typeof ethers.formatEther !== 'function') {
throw new Error('Метод ethers.formatEther не найден');
}
logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`); logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`);
if (balance < minBalance) { if (balance < minBalance) {
throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`); throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`);
@@ -84,28 +118,58 @@ class DLEV2Service {
} }
// Рассчитываем INIT_CODE_HASH автоматически из актуального initCode // Рассчитываем INIT_CODE_HASH автоматически из актуального initCode
const initCodeHash = await this.computeInitCodeHash(deployParams); const initCodeHash = await this.computeInitCodeHash({
...deployParams,
currentChainId: deployParams.currentChainId || deployParams.supportedChainIds[0]
});
// Собираем адреса фабрик по сетям (если есть) // Factory больше не используется - деплой DLE напрямую
const factoryAddresses = deployParams.supportedChainIds.map(cid => process.env[`FACTORY_ADDRESS_${cid}`] || '').join(','); logger.info(`Подготовка к прямому деплою DLE в сетях: ${deployParams.supportedChainIds.join(', ')}`);
// Мультисетевой деплой одним вызовом // Мультисетевой деплой одним вызовом
// Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets logger.info('Запуск мульти-чейн деплоя...');
const { createAndStoreNewCreate2Salt } = require('./secretStore');
const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' });
logger.info(`CREATE2_SALT создан и сохранён: key=${saltKey}`);
const result = await this.runDeployMultichain(paramsFile, { const result = await this.runDeployMultichain(paramsFile, {
rpcUrls: rpcUrls.join(','), rpcUrls: rpcUrls,
chainIds: deployParams.supportedChainIds,
privateKey: dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`, privateKey: dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`,
salt: create2Salt, salt: create2Salt,
initCodeHash, initCodeHash
factories: factoryAddresses
}); });
logger.info('Деплой завершен, результат:', JSON.stringify(result, null, 2));
// Сохраняем информацию о созданном DLE для отображения на странице управления // Сохраняем информацию о созданном DLE для отображения на странице управления
try { try {
const firstNet = Array.isArray(result?.data?.networks) && result.data.networks.length > 0 ? result.data.networks[0] : null; logger.info('Результат деплоя для сохранения:', JSON.stringify(result, null, 2));
// Проверяем структуру результата
if (!result || typeof result !== 'object') {
logger.error('Неверная структура результата деплоя:', result);
throw new Error('Неверная структура результата деплоя');
}
// Если результат - массив (прямой результат из скрипта), преобразуем его
let deployResult = result;
if (Array.isArray(result)) {
logger.info('Результат - массив, преобразуем в объект');
const addresses = result.map(r => r.address);
const allSame = addresses.every(addr => addr.toLowerCase() === addresses[0].toLowerCase());
deployResult = {
success: true,
data: {
dleAddress: addresses[0],
networks: result.map((r, index) => ({
chainId: r.chainId,
address: r.address,
success: true
})),
allSame
}
};
}
const firstNet = Array.isArray(deployResult?.data?.networks) && deployResult.data.networks.length > 0 ? deployResult.data.networks[0] : null;
const dleData = { const dleData = {
name: deployParams.name, name: deployParams.name,
symbol: deployParams.symbol, symbol: deployParams.symbol,
@@ -122,13 +186,20 @@ class DLEV2Service {
supportedChainIds: deployParams.supportedChainIds, supportedChainIds: deployParams.supportedChainIds,
currentChainId: deployParams.currentChainId currentChainId: deployParams.currentChainId
}, },
dleAddress: (result?.data?.dleAddress) || (firstNet?.address) || null, dleAddress: (deployResult?.data?.dleAddress) || (firstNet?.address) || null,
version: 'v2', version: 'v2',
networks: result?.data?.networks || [], networks: deployResult?.data?.networks || [],
createdAt: new Date().toISOString() createdAt: new Date().toISOString()
}; };
logger.info('Данные DLE для сохранения:', JSON.stringify(dleData, null, 2));
if (dleData.dleAddress) { if (dleData.dleAddress) {
this.saveDLEData(dleData); // Сохраняем одну карточку DLE с информацией о всех сетях
const savedPath = this.saveDLEData(dleData);
logger.info(`DLE данные сохранены в: ${savedPath}`);
} else {
logger.error('Не удалось получить адрес DLE из результата деплоя');
} }
} catch (e) { } catch (e) {
logger.warn('Не удалось сохранить локальную карточку DLE:', e.message); logger.warn('Не удалось сохранить локальную карточку DLE:', e.message);
@@ -215,7 +286,7 @@ class DLEV2Service {
// Проверяем адреса партнеров // Проверяем адреса партнеров
for (let i = 0; i < params.initialPartners.length; i++) { for (let i = 0; i < params.initialPartners.length; i++) {
if (!ethers.isAddress(params.initialPartners[i])) { if (!ethers.isAddress || !ethers.isAddress(params.initialPartners[i])) {
throw new Error(`Неверный адрес партнера ${i + 1}: ${params.initialPartners[i]}`); throw new Error(`Неверный адрес партнера ${i + 1}: ${params.initialPartners[i]}`);
} }
} }
@@ -225,7 +296,51 @@ class DLEV2Service {
throw new Error('Должна быть выбрана хотя бы одна сеть для деплоя'); throw new Error('Должна быть выбрана хотя бы одна сеть для деплоя');
} }
// Дополнительные проверки безопасности
if (params.name.length > 100) {
throw new Error('Название DLE слишком длинное (максимум 100 символов)');
}
if (params.symbol.length > 10) {
throw new Error('Символ токена слишком длинный (максимум 10 символов)');
}
if (params.location.length > 200) {
throw new Error('Местонахождение слишком длинное (максимум 200 символов)');
}
// Проверяем суммы токенов
for (let i = 0; i < params.initialAmounts.length; i++) {
const amount = params.initialAmounts[i];
if (typeof amount !== 'string' && typeof amount !== 'number') {
throw new Error(`Неверный тип суммы для партнера ${i + 1}`);
}
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
if (isNaN(numAmount) || numAmount <= 0) {
throw new Error(`Неверная сумма для партнера ${i + 1}: ${amount}`);
}
}
// Проверяем приватный ключ
if (!params.privateKey) {
throw new Error('Приватный ключ обязателен для деплоя');
}
const pk = params.privateKey.startsWith('0x') ? params.privateKey : `0x${params.privateKey}`;
if (!/^0x[a-fA-F0-9]{64}$/.test(pk)) {
throw new Error('Неверный формат приватного ключа');
}
// Проверяем, что не деплоим в mainnet без подтверждения
const mainnetChains = [1, 137, 56, 42161]; // Ethereum, Polygon, BSC, Arbitrum
const hasMainnet = params.supportedChainIds.some(id => mainnetChains.includes(id));
if (hasMainnet) {
logger.warn('⚠️ ВНИМАНИЕ: Деплой включает mainnet сети! Убедитесь, что это необходимо.');
}
logger.info('✅ Валидация параметров DLE пройдена успешно');
} }
/** /**
@@ -293,6 +408,9 @@ class DLEV2Service {
// Принимаем как строки, так и числа; конвертируем в base units (18 знаков) // Принимаем как строки, так и числа; конвертируем в base units (18 знаков)
try { try {
if (typeof rawAmount === 'number' && Number.isFinite(rawAmount)) { if (typeof rawAmount === 'number' && Number.isFinite(rawAmount)) {
if (typeof ethers.parseUnits !== 'function') {
throw new Error('Метод ethers.parseUnits не найден');
}
return ethers.parseUnits(rawAmount.toString(), 18).toString(); return ethers.parseUnits(rawAmount.toString(), 18).toString();
} }
if (typeof rawAmount === 'string') { if (typeof rawAmount === 'string') {
@@ -302,6 +420,9 @@ class DLEV2Service {
return BigInt(a).toString(); return BigInt(a).toString();
} }
// Десятичная строка — конвертируем в base units // Десятичная строка — конвертируем в base units
if (typeof ethers.parseUnits !== 'function') {
throw new Error('Метод ethers.parseUnits не найден');
}
return ethers.parseUnits(a, 18).toString(); return ethers.parseUnits(a, 18).toString();
} }
// BigInt или иные типы — приводим к строке без изменения масштаба // BigInt или иные типы — приводим к строке без изменения масштаба
@@ -318,6 +439,13 @@ class DLEV2Service {
deployParams.okvedCodes = []; deployParams.okvedCodes = [];
} }
// Преобразуем kpp в число
if (deployParams.kpp) {
deployParams.kpp = parseInt(deployParams.kpp) || 0;
} else {
deployParams.kpp = 0;
}
// Убеждаемся, что supportedChainIds - это массив // Убеждаемся, что supportedChainIds - это массив
if (!Array.isArray(deployParams.supportedChainIds)) { if (!Array.isArray(deployParams.supportedChainIds)) {
deployParams.supportedChainIds = [1]; // По умолчанию Ethereum deployParams.supportedChainIds = [1]; // По умолчанию Ethereum
@@ -360,7 +488,7 @@ class DLEV2Service {
*/ */
runDeployScript(paramsFile, extraEnv = {}) { runDeployScript(paramsFile, extraEnv = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const scriptPath = path.join(__dirname, '../scripts/deploy/create-dle-v2.js'); const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js');
if (!fs.existsSync(scriptPath)) { if (!fs.existsSync(scriptPath)) {
reject(new Error('Скрипт деплоя DLE v2 не найден: ' + scriptPath)); reject(new Error('Скрипт деплоя DLE v2 не найден: ' + scriptPath));
return; return;
@@ -416,33 +544,70 @@ class DLEV2Service {
runDeployMultichain(paramsFile, opts = {}) { runDeployMultichain(paramsFile, opts = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js'); const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js');
if (!fs.existsSync(scriptPath)) return reject(new Error('Скрипт мультисетевого деплоя не найден')); if (!fs.existsSync(scriptPath)) return reject(new Error('Скрипт мультисетевого деплоя не найден: ' + scriptPath));
const envVars = { const envVars = {
...process.env, ...process.env,
PRIVATE_KEY: opts.privateKey, PRIVATE_KEY: opts.privateKey
CREATE2_SALT: opts.salt,
INIT_CODE_HASH: opts.initCodeHash,
MULTICHAIN_RPC_URLS: opts.rpcUrls,
MULTICHAIN_FACTORY_ADDRESSES: opts.factories || ''
}; };
const p = spawn('npx', ['hardhat', 'run', scriptPath], { cwd: path.join(__dirname, '..'), env: envVars, stdio: 'pipe' });
const p = spawn('npx', ['hardhat', 'run', scriptPath], {
cwd: path.join(__dirname, '..'),
env: envVars,
stdio: 'pipe'
});
let stdout = '', stderr = ''; let stdout = '', stderr = '';
p.stdout.on('data', (d) => { stdout += d.toString(); logger.info(`[MULTI] ${d.toString().trim()}`); }); p.stdout.on('data', (d) => {
p.stderr.on('data', (d) => { stderr += d.toString(); logger.error(`[MULTI_ERR] ${d.toString().trim()}`); }); stdout += d.toString();
p.on('close', () => { logger.info(`[MULTICHAIN_DEPLOY] ${d.toString().trim()}`);
});
p.stderr.on('data', (d) => {
stderr += d.toString();
logger.error(`[MULTICHAIN_DEPLOY_ERR] ${d.toString().trim()}`);
});
p.on('close', (code) => {
try { try {
const m = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s*(\[.*\])/s); // Ищем результат в формате MULTICHAIN_DEPLOY_RESULT
if (!m) throw new Error('Результат не найден'); const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(\[.*\])/);
const arr = JSON.parse(m[1]);
if (!Array.isArray(arr) || arr.length === 0) throw new Error('Пустой результат деплоя'); if (resultMatch) {
const addr = arr[0].address; const result = JSON.parse(resultMatch[1]);
const allSame = arr.every(x => x.address && x.address.toLowerCase() === addr.toLowerCase()); resolve(result);
if (!allSame) throw new Error('Адреса отличаются между сетями'); } else {
resolve({ success: true, data: { dleAddress: addr, networks: arr } }); // Fallback: ищем адреса DLE в выводе по новому формату
const dleAddressMatches = stdout.match(/\[MULTI_DBG\] chainId=\d+ DLE deployed at=(0x[a-fA-F0-9]{40})/g);
if (!dleAddressMatches || dleAddressMatches.length === 0) {
throw new Error('Не найдены адреса DLE в выводе');
}
const addresses = dleAddressMatches.map(match => match.match(/(0x[a-fA-F0-9]{40})/)[1]);
const addr = addresses[0];
const allSame = addresses.every(x => x.toLowerCase() === addr.toLowerCase());
if (!allSame) {
logger.warn('Адреса отличаются между сетями — продолжаем, сохраню по-сеточно', { addresses });
}
resolve({
success: true,
data: {
dleAddress: addr,
networks: addresses.map((address, index) => ({
chainId: opts.chainIds[index] || index + 1,
address,
success: true
})),
allSame
}
});
}
} catch (e) { } catch (e) {
reject(new Error(`Ошибка мультисетевого деплоя: ${e.message}\nSTDOUT:${stdout}\nSTDERR:${stderr}`)); reject(new Error(`Ошибка мультисетевого деплоя: ${e.message}\nSTDOUT:${stdout}\nSTDERR:${stderr}`));
} }
}); });
p.on('error', (e) => reject(e)); p.on('error', (e) => reject(e));
}); });
} }
@@ -453,8 +618,20 @@ class DLEV2Service {
* @returns {Object} - Результат деплоя * @returns {Object} - Результат деплоя
*/ */
extractDeployResult(stdout) { extractDeployResult(stdout) {
// Ищем строки с адресами в выводе // Ищем результат в формате MULTICHAIN_DEPLOY_RESULT
const dleAddressMatch = stdout.match(/DLE v2 задеплоен по адресу: (0x[a-fA-F0-9]{40})/); const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(\[.*?\])/);
if (resultMatch) {
try {
const result = JSON.parse(resultMatch[1]);
return result;
} catch (e) {
logger.error('Ошибка парсинга JSON результата:', e);
}
}
// Fallback: ищем строки с адресами в выводе по новому формату
const dleAddressMatch = stdout.match(/\[MULTI_DBG\] chainId=\d+ DLE deployed at=(0x[a-fA-F0-9]{40})/);
if (dleAddressMatch) { if (dleAddressMatch) {
return { return {
@@ -529,7 +706,7 @@ class DLEV2Service {
} }
const files = fs.readdirSync(dlesDir); const files = fs.readdirSync(dlesDir);
return files const allDles = files
.filter(file => file.endsWith('.json') && file.includes('dle-v2-')) .filter(file => file.endsWith('.json') && file.includes('dle-v2-'))
.map(file => { .map(file => {
try { try {
@@ -541,68 +718,300 @@ class DLEV2Service {
} }
}) })
.filter(dle => dle !== null); .filter(dle => dle !== null);
// Группируем DLE по мультичейн деплоям
const groupedDles = this.groupMultichainDLEs(allDles);
return groupedDles;
} catch (error) { } catch (error) {
logger.error('Ошибка при получении списка DLE v2:', error); logger.error('Ошибка при получении списка DLE v2:', error);
return []; return [];
} }
} }
// Авто-расчёт INIT_CODE_HASH /**
async computeInitCodeHash(params) { * Группирует DLE по мультичейн деплоям
const hre = require('hardhat'); * @param {Array<Object>} allDles - Все DLE из файлов
const { ethers } = hre; * @returns {Array<Object>} - Сгруппированные DLE
const DLE = await hre.ethers.getContractFactory('DLE'); */
const dleConfig = { groupMultichainDLEs(allDles) {
name: params.name, const groups = new Map();
symbol: params.symbol,
location: params.location, for (const dle of allDles) {
coordinates: params.coordinates, // Создаем ключ для группировки на основе общих параметров
jurisdiction: params.jurisdiction, const groupKey = this.createGroupKey(dle);
okvedCodes: params.okvedCodes || [],
kpp: params.kpp, if (!groups.has(groupKey)) {
quorumPercentage: params.quorumPercentage, groups.set(groupKey, {
initialPartners: params.initialPartners, // Основные данные из первого DLE
initialAmounts: params.initialAmounts, name: dle.name,
supportedChainIds: params.supportedChainIds symbol: dle.symbol,
}; location: dle.location,
const deployTx = await DLE.getDeployTransaction(dleConfig, params.currentChainId); coordinates: dle.coordinates,
const initCode = deployTx.data; jurisdiction: dle.jurisdiction,
return ethers.keccak256(initCode); oktmo: dle.oktmo,
okvedCodes: dle.okvedCodes,
kpp: dle.kpp,
quorumPercentage: dle.quorumPercentage,
version: dle.version || 'v2',
deployedMultichain: true,
// Мультичейн информация
networks: [],
// Модули (одинаковые во всех сетях)
modules: dle.modules,
// Время создания (самое раннее)
creationTimestamp: dle.creationTimestamp,
creationBlock: dle.creationBlock
});
}
const group = groups.get(groupKey);
// Если у DLE есть массив networks, используем его
if (dle.networks && Array.isArray(dle.networks)) {
for (const network of dle.networks) {
group.networks.push({
chainId: network.chainId,
dleAddress: network.address || network.dleAddress,
factoryAddress: network.factoryAddress,
rpcUrl: network.rpcUrl || this.getRpcUrlForChain(network.chainId)
});
}
} else {
// Старый формат: добавляем информацию о сети из корня DLE
group.networks.push({
chainId: dle.chainId,
dleAddress: dle.dleAddress,
factoryAddress: dle.factoryAddress,
rpcUrl: dle.rpcUrl || this.getRpcUrlForChain(dle.chainId)
});
}
// Обновляем время создания на самое раннее
if (dle.creationTimestamp && (!group.creationTimestamp || dle.creationTimestamp < group.creationTimestamp)) {
group.creationTimestamp = dle.creationTimestamp;
}
}
// Преобразуем группы в массив
return Array.from(groups.values()).map(group => ({
...group,
// Основной адрес DLE (из первой сети)
dleAddress: group.networks[0]?.dleAddress,
// Общее количество сетей
totalNetworks: group.networks.length,
// Поддерживаемые сети
supportedChainIds: group.networks.map(n => n.chainId)
}));
} }
/** /**
* Проверяет баланс деплоера во всех выбранных сетях * Создает ключ для группировки DLE
* @param {number[]} chainIds * @param {Object} dle - Данные DLE
* @param {string} privateKey * @returns {string} - Ключ группировки
* @returns {Promise<{balances: Array<{chainId:number, balanceEth:string, ok:boolean, rpcUrl:string}>, insufficient:number[]}>} */
createGroupKey(dle) {
// Группируем по основным параметрам DLE
const keyParts = [
dle.name,
dle.symbol,
dle.location,
dle.coordinates,
dle.jurisdiction,
dle.oktmo,
dle.kpp,
dle.quorumPercentage,
// Сортируем okvedCodes для стабильного ключа
Array.isArray(dle.okvedCodes) ? dle.okvedCodes.sort().join(',') : '',
// Сортируем supportedChainIds для стабильного ключа
Array.isArray(dle.supportedChainIds) ? dle.supportedChainIds.sort().join(',') : ''
];
return keyParts.join('|');
}
/**
* Получает RPC URL для сети
* @param {number} chainId - ID сети
* @returns {string|null} - RPC URL
*/
getRpcUrlForChain(chainId) {
try {
// Простая маппинг для основных сетей
const rpcMap = {
1: 'https://eth-mainnet.g.alchemy.com/v2/demo',
11155111: 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52',
17000: 'https://ethereum-holesky.publicnode.com',
421614: 'https://sepolia-rollup.arbitrum.io/rpc',
84532: 'https://sepolia.base.org'
};
return rpcMap[chainId] || null;
} catch (error) {
return null;
}
}
// Авто-расчёт INIT_CODE_HASH
async computeInitCodeHash(params) {
try {
// Проверяем наличие обязательных параметров
if (!params.name || !params.symbol || !params.location) {
throw new Error('Отсутствуют обязательные параметры для вычисления INIT_CODE_HASH');
}
const hre = require('hardhat');
const { ethers } = hre;
// Проверяем, что контракт DLE существует
try {
const DLE = await hre.ethers.getContractFactory('DLE');
if (!DLE) {
throw new Error('Контракт DLE не найден в Hardhat');
}
} catch (contractError) {
throw new Error(`Ошибка загрузки контракта DLE: ${contractError.message}`);
}
const DLE = await hre.ethers.getContractFactory('DLE');
const dleConfig = {
name: params.name,
symbol: params.symbol,
location: params.location,
coordinates: params.coordinates || "",
jurisdiction: params.jurisdiction || 1,
okvedCodes: params.okvedCodes || [],
kpp: params.kpp || 0,
quorumPercentage: params.quorumPercentage || 51,
initialPartners: params.initialPartners || [],
initialAmounts: params.initialAmounts || [],
supportedChainIds: params.supportedChainIds || [1]
};
// Учитываем актуальную сигнатуру конструктора: (dleConfig, currentChainId, initializer)
const initializer = params.initializerAddress || "0x0000000000000000000000000000000000000000";
const currentChainId = params.currentChainId || 1; // Fallback на Ethereum mainnet
logger.info('Вычисление INIT_CODE_HASH с параметрами:', {
name: dleConfig.name,
symbol: dleConfig.symbol,
currentChainId,
initializer
});
// Проверяем, что метод getDeployTransaction существует
if (typeof DLE.getDeployTransaction !== 'function') {
throw new Error('Метод getDeployTransaction не найден в контракте DLE');
}
const deployTx = await DLE.getDeployTransaction(dleConfig, currentChainId, initializer);
if (!deployTx || !deployTx.data) {
throw new Error('Не удалось получить данные транзакции деплоя');
}
const initCode = deployTx.data;
// Проверяем, что метод keccak256 существует
if (typeof ethers.keccak256 !== 'function') {
throw new Error('Метод ethers.keccak256 не найден');
}
const hash = ethers.keccak256(initCode);
logger.info('INIT_CODE_HASH вычислен успешно:', hash);
return hash;
} catch (error) {
logger.error('Ошибка при вычислении INIT_CODE_HASH:', error);
// Fallback: возвращаем хеш на основе параметров
const { ethers } = require('ethers');
const fallbackData = JSON.stringify({
name: params.name,
symbol: params.symbol,
location: params.location,
jurisdiction: params.jurisdiction,
supportedChainIds: params.supportedChainIds
});
// Проверяем, что методы существуют
if (typeof ethers.toUtf8Bytes !== 'function') {
throw new Error('Метод ethers.toUtf8Bytes не найден');
}
if (typeof ethers.keccak256 !== 'function') {
throw new Error('Метод ethers.keccak256 не найден');
}
return ethers.keccak256(ethers.toUtf8Bytes(fallbackData));
}
}
/**
* Проверяет балансы в указанных сетях
* @param {number[]} chainIds - Массив chainId для проверки
* @param {string} privateKey - Приватный ключ
* @returns {Promise<Object>} - Результат проверки балансов
*/ */
async checkBalances(chainIds, privateKey) { async checkBalances(chainIds, privateKey) {
const { getRpcUrlByChainId } = require('./rpcProviderService');
const { ethers } = require('ethers'); const { ethers } = require('ethers');
const results = []; const balances = [];
const insufficient = []; const insufficient = [];
const normalizedPk = privateKey?.startsWith('0x') ? privateKey : `0x${privateKey}`;
for (const cid of chainIds || []) { for (const chainId of chainIds) {
const rpcUrl = await getRpcUrlByChainId(cid);
if (!rpcUrl) {
results.push({ chainId: cid, balanceEth: '0', ok: false, rpcUrl: null });
insufficient.push(cid);
continue;
}
try { try {
const rpcUrl = await getRpcUrlByChainId(chainId);
if (!rpcUrl) {
balances.push({
chainId,
balanceEth: '0',
ok: false,
error: 'RPC URL не найден'
});
insufficient.push(chainId);
continue;
}
const provider = new ethers.JsonRpcProvider(rpcUrl); const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(normalizedPk, provider); const wallet = new ethers.Wallet(privateKey, provider);
const bal = await provider.getBalance(wallet.address); const balance = await provider.getBalance(wallet.address);
// Минимум для деплоя; можно скорректировать
const min = ethers.parseEther('0.002'); if (typeof ethers.formatEther !== 'function') {
const ok = bal >= min; throw new Error('Метод ethers.formatEther не найден');
results.push({ chainId: cid, balanceEth: ethers.formatEther(bal), ok, rpcUrl }); }
if (!ok) insufficient.push(cid); const balanceEth = ethers.formatEther(balance);
} catch (e) {
results.push({ chainId: cid, balanceEth: '0', ok: false, rpcUrl }); if (typeof ethers.parseEther !== 'function') {
insufficient.push(cid); throw new Error('Метод ethers.parseEther не найден');
}
const minBalance = ethers.parseEther("0.001");
const ok = balance >= minBalance;
balances.push({
chainId,
address: wallet.address,
balanceEth,
ok
});
if (!ok) {
insufficient.push(chainId);
}
} catch (error) {
balances.push({
chainId,
balanceEth: '0',
ok: false,
error: error.message
});
insufficient.push(chainId);
} }
} }
return { balances: results, insufficient };
return {
balances,
insufficient,
allSufficient: insufficient.length === 0
};
} }
/** /**
@@ -724,7 +1133,7 @@ class DLEV2Service {
} }
// 2) Посчитать ABI-код аргументов конструктора через сравнение с bytecode // 2) Посчитать ABI-код аргументов конструктора через сравнение с bytecode
// Конструктор: (dleConfig, currentChainId) // Конструктор: (dleConfig, currentChainId, initializer)
const Factory = await hre.ethers.getContractFactory('DLE'); const Factory = await hre.ethers.getContractFactory('DLE');
const dleConfig = { const dleConfig = {
name: params.name, name: params.name,
@@ -739,7 +1148,8 @@ class DLEV2Service {
initialAmounts: params.initialAmounts, initialAmounts: params.initialAmounts,
supportedChainIds: params.supportedChainIds supportedChainIds: params.supportedChainIds
}; };
const deployTx = await Factory.getDeployTransaction(dleConfig, params.currentChainId); const initializer = params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000";
const deployTx = await Factory.getDeployTransaction(dleConfig, params.currentChainId, initializer);
const fullData = deployTx.data; // 0x + creation bytecode + encoded args const fullData = deployTx.data; // 0x + creation bytecode + encoded args
const bytecode = Factory.bytecode; // 0x + creation bytecode const bytecode = Factory.bytecode; // 0x + creation bytecode
let constructorArgsHex; let constructorArgsHex;

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ function computeCreate2Address(factory, saltHex, initCodeHash) {
saltHex.toLowerCase(), saltHex.toLowerCase(),
initCodeHash.toLowerCase() initCodeHash.toLowerCase()
].map(x => x.startsWith('0x') ? x.slice(2) : x).join(''); ].map(x => x.startsWith('0x') ? x.slice(2) : x).join('');
const hash = '0x' + require('crypto').createHash('sha3-256').update(Buffer.from(parts, 'hex')).digest('hex'); const hash = keccak256('0x' + parts);
return '0x' + hash.slice(-40); return '0x' + hash.slice(-40);
} }

View File

@@ -2013,7 +2013,7 @@ cli-boxes@^2.2.1:
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
cli-table3@^0.6.3: cli-table3@^0.6.0, cli-table3@^0.6.3:
version "0.6.5" version "0.6.5"
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f"
integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==
@@ -3565,6 +3565,15 @@ handlebars@^4.0.1:
optionalDependencies: optionalDependencies:
uglify-js "^3.1.4" uglify-js "^3.1.4"
hardhat-contract-sizer@^2.10.1:
version "2.10.1"
resolved "https://registry.yarnpkg.com/hardhat-contract-sizer/-/hardhat-contract-sizer-2.10.1.tgz#125092f9398105d0d23001056aac61c936ad841a"
integrity sha512-/PPQQbUMgW6ERzk8M0/DA8/v2TEM9xRRAnF9qKPNMYF6FX5DFWcnxBsQvtp8uBz+vy7rmLyV9Elti2wmmhgkbg==
dependencies:
chalk "^4.0.0"
cli-table3 "^0.6.0"
strip-ansi "^6.0.0"
hardhat-gas-reporter@^2.2.2: hardhat-gas-reporter@^2.2.2:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/hardhat-gas-reporter/-/hardhat-gas-reporter-2.3.0.tgz#8605131a9130925b2f19a77576f93a637a2911d8" resolved "https://registry.yarnpkg.com/hardhat-gas-reporter/-/hardhat-gas-reporter-2.3.0.tgz#8605131a9130925b2f19a77576f93a637a2911d8"
@@ -6059,7 +6068,7 @@ solc@0.8.26:
semver "^5.5.0" semver "^5.5.0"
tmp "0.0.33" tmp "0.0.33"
solidity-coverage@^0.8.1: solidity-coverage@^0.8.16:
version "0.8.16" version "0.8.16"
resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.8.16.tgz#ae07bb11ebbd78d488c7e1a3cd15b8210692f1c9" resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.8.16.tgz#ae07bb11ebbd78d488c7e1a3cd15b8210692f1c9"
integrity sha512-qKqgm8TPpcnCK0HCDLJrjbOA2tQNEJY4dHX/LSSQ9iwYFS973MwjtgYn2Iv3vfCEQJTj5xtm4cuUMzlJsJSMbg== integrity sha512-qKqgm8TPpcnCK0HCDLJrjbOA2tQNEJY4dHX/LSSQ9iwYFS973MwjtgYn2Iv3vfCEQJTj5xtm4cuUMzlJsJSMbg==

View File

@@ -108,6 +108,8 @@ services:
condition: service_started condition: service_started
volumes: volumes:
- ./backend:/app - ./backend:/app
- ./backend/uploads:/app/uploads
- backend_node_modules:/app/node_modules
- ./frontend/dist:/app/frontend_dist:ro - ./frontend/dist:/app/frontend_dist:ro
- ./ssl:/app/ssl:ro - ./ssl:/app/ssl:ro
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
@@ -126,6 +128,7 @@ services:
- OLLAMA_EMBEDDINGS_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-qwen2.5:7b} - OLLAMA_EMBEDDINGS_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-qwen2.5:7b}
- FRONTEND_URL=http://localhost:5173 - FRONTEND_URL=http://localhost:5173
- VECTOR_SEARCH_URL=http://vector-search:8001 - VECTOR_SEARCH_URL=http://vector-search:8001
# Factory адреса теперь хранятся в базе данных
ports: ports:
- '8000:8000' - '8000:8000'
extra_hosts: extra_hosts:
@@ -182,6 +185,27 @@ services:
depends_on: depends_on:
- backend - backend
# Мониторинг безопасности
security-monitor:
image: alpine:latest
container_name: dapp-security-monitor
restart: unless-stopped
volumes:
- ./security-monitor.sh:/app/security-monitor.sh:ro
- ./start-security-monitor.sh:/app/start-security-monitor.sh:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- security_monitor_data:/var/log/security-monitor
depends_on:
- frontend-nginx
working_dir: /app
command: >
sh -c "
apk add --no-cache docker-cli bash curl jq &&
cp security-monitor.sh /tmp/security-monitor.sh &&
chmod +x /tmp/security-monitor.sh &&
exec bash /tmp/security-monitor.sh
"
# Автоматический бэкап базы данных # Автоматический бэкап базы данных
backup-service: backup-service:
image: postgres:16-alpine image: postgres:16-alpine
@@ -208,6 +232,7 @@ services:
volumes: volumes:
postgres_data: postgres_data:
ollama_data: ollama_data:
security_monitor_data:
vector_search_data: vector_search_data:
frontend_node_modules: frontend_node_modules:
backend_node_modules: backend_node_modules:

291
docs/DLE_DEPLOY_GUIDE.md Normal file
View File

@@ -0,0 +1,291 @@
# Руководство по деплою DLE v2
## Обзор
DLE v2 (Digital Legal Entity) - это система для создания цифровых юридических лиц с мульти-чейн поддержкой. Основная особенность - использование CREATE2 для обеспечения одинакового адреса смарт-контракта во всех поддерживаемых сетях.
## Архитектура
### Компоненты системы
1. **DLE.sol** - Основной смарт-контракт с ERC-20 токенами управления
2. **FactoryDeployer.sol** - Фабрика для детерминистического деплоя через CREATE2
3. **Модули** - Дополнительная функциональность (Treasury, Timelock, etc.)
### Мульти-чейн поддержка
- **CREATE2** - Одинаковый адрес во всех EVM-совместимых сетях
- **Single-Chain Governance** - Голосование происходит в одной сети
- **Multi-Chain Execution** - Исполнение в целевых сетях по подписям
## Процесс деплоя
### 1. Подготовка
1. Убедитесь, что у вас есть:
- Приватный ключ с достаточным балансом в выбранных сетях
- RPC URLs для всех целевых сетей
- API ключ Etherscan (опционально, для верификации)
2. Настройте RPC провайдеры в веб-интерфейсе:
- Откройте страницу настроек: `http://localhost:5173/settings/security`
- Перейдите в раздел "RPC Провайдеры"
- Добавьте RPC URLs для нужных сетей:
- **Ethereum Mainnet**: Chain ID 1
- **Polygon**: Chain ID 137
- **BSC**: Chain ID 56
- **Arbitrum**: Chain ID 42161
- **Sepolia Testnet**: Chain ID 11155111
- И другие нужные сети
3. Приватные ключи вводятся непосредственно в форме деплоя для безопасности
### 2. Деплой через веб-интерфейс
1. Откройте страницу: `http://localhost:5173/settings/dle-v2-deploy`
2. Заполните форму:
- **Основная информация**: Название, символ токена
- **Юридическая информация**: Страна, ОКВЭД, адрес
- **Партнеры**: Адреса и доли токенов
- **Сети**: Выберите целевые блокчейн-сети
- **Приватный ключ**: Для деплоя контрактов
3. Нажмите "Развернуть DLE"
### 3. Процесс деплоя
Система автоматически:
1. **Проверяет балансы** во всех выбранных сетях
2. **Компилирует контракты** через Hardhat
3. **Проверяет Factory адреса** в базе данных
4. **Деплоит FactoryDeployer** (если не найден) с одинаковым адресом
5. **Сохраняет Factory адреса** в базу данных для переиспользования
6. **Создает CREATE2 salt** на основе параметров DLE
7. **Деплоит DLE** через FactoryDeployer с одинаковым адресом
8. **Деплоит базовые модули** (Treasury, Timelock, Reader) в каждой сети
9. **Инициализирует модули** в DLE контракте
10. **Верифицирует контракты** в Etherscan (опционально)
### 4. Результат
После успешного деплоя вы получите:
- **Одинаковый адрес DLE** во всех выбранных сетях
- **Одинаковый адрес Factory** во всех выбранных сетях
- **Базовые модули** (Treasury, Timelock, Reader) в каждой сети
- **Инициализированные модули** в DLE контракте
- **ERC-20 токены управления** распределенные между партнерами
- **Настроенный кворум** для принятия решений
- **Поддержку мульти-чейн операций**
### 5. Управление Factory адресами
Система автоматически управляет Factory адресами:
#### API Endpoints:
- `GET /api/factory` - Получить все Factory адреса
- `GET /api/factory/:chainId` - Получить Factory адрес для сети
- `POST /api/factory` - Сохранить Factory адрес
- `POST /api/factory/bulk` - Сохранить адреса для нескольких сетей
- `DELETE /api/factory/:chainId` - Удалить Factory адрес
- `POST /api/factory/check` - Проверить наличие адресов
#### Автоматическое управление:
- **Кэширование** - Factory адреса сохраняются в базе данных
- **Переиспользование** - Существующие Factory используются повторно
- **Валидация** - Проверка существования Factory в блокчейне
- **Автодеплой** - Новые Factory деплоятся при необходимости
### 6. Проверка одинаковости адресов
Система автоматически проверяет, что все адреса одинаковые:
```javascript
// Проверка адресов DLE
const addresses = results.filter(r => r.success).map(r => r.address);
const uniqueAddresses = [...new Set(addresses)];
if (uniqueAddresses.length === 1) {
console.log("✅ Все адреса DLE одинаковые:", uniqueAddresses[0]);
} else {
throw new Error("CREATE2 не обеспечил одинаковые адреса");
}
```
Если адреса не совпадают, это указывает на проблему с:
- Разными Factory адресами в сетях
- Разными salt значениями
- Разными bytecode контрактов
## Технические детали
### База данных Factory адресов
Система использует таблицу `factory_addresses` для хранения адресов:
```sql
CREATE TABLE factory_addresses (
id SERIAL PRIMARY KEY,
chain_id INTEGER NOT NULL UNIQUE,
factory_address VARCHAR(42) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
### CREATE2 Механизм
Система использует двухуровневый CREATE2 для обеспечения одинаковых адресов:
#### 1. Factory Deployer
```solidity
// Предсказуемый адрес Factory через CREATE
address factoryAddress = getCreateAddress(
from: deployerAddress,
nonce: deployerNonce
);
```
#### 2. DLE Contract
```solidity
// Вычисление адреса DLE через CREATE2
address predictedAddress = factoryDeployer.computeAddress(
salt,
keccak256(creationCode)
);
// Деплой DLE с одинаковым адресом
factoryDeployer.deploy(salt, creationCode);
```
#### Ключевые принципы:
- **Factory Deployer** деплоится с одинаковым адресом во всех сетях
- **DLE Contract** деплоится через Factory с одинаковым salt
- **Результат**: Одинаковый адрес DLE во всех EVM-совместимых сетях
### Структура DLE
```solidity
contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
// Основная информация
DLEInfo public dleInfo;
// Настройки управления
uint256 public quorumPercentage;
// Мульти-чейн поддержка
uint256[] public supportedChainIds;
uint256 public governanceChainId;
// Система предложений
mapping(uint256 => Proposal) public proposals;
}
```
### Базовые модули (автоматически деплоятся)
При деплое DLE автоматически развертываются и инициализируются:
- **TreasuryModule** - Управление финансами, депозиты, выводы, дивиденды
- **TimelockModule** - Задержки исполнения критических операций
- **DLEReader** - API для чтения данных DLE
### Дополнительные модули (через голосование)
DLE поддерживает модульную архитектуру:
- **CommunicationModule** - Внешние коммуникации
- **BurnModule** - Сжигание токенов
- **MintModule** - Выпуск новых токенов
- **OracleModule** - Внешние данные
- **CustomModule** - Пользовательские модули
## Управление DLE
### Создание предложений
```solidity
// Создать предложение
uint256 proposalId = dle.createProposal(
"Описание предложения",
governanceChainId,
targetChains,
timelockHours,
operationCalldata
);
```
### Голосование
```solidity
// Голосовать за предложение
dle.vote(proposalId, true); // За
dle.vote(proposalId, false); // Против
```
### Исполнение
```solidity
// Исполнить предложение
dle.executeProposalBySignatures(proposalId, signatures);
```
## Безопасность
### Ключевые принципы
1. **Только токен-холдеры** участвуют в управлении
2. **Прямые переводы токенов заблокированы**
3. **Все операции через кворум**
4. **CREATE2 обеспечивает детерминистические адреса**
5. **EIP-712 подписи для мульти-чейн исполнения**
### Проверки
- Валидация приватных ключей
- Проверка балансов перед деплоем
- Верификация CREATE2 адресов
- Контроль кворума при голосовании
## Устранение неполадок
### Частые проблемы
1. **Недостаточно средств**
- Проверьте балансы во всех сетях
- Убедитесь в правильности RPC URLs в настройках
2. **Ошибки компиляции**
- Проверьте версию Solidity (0.8.20)
- Убедитесь в корректности импортов OpenZeppelin
3. **Разные адреса в сетях**
- Проверьте, что Factory контракты имеют одинаковые адреса
- Проверьте CREATE2 salt для DLE
- Убедитесь в одинаковом bytecode контрактов
- Проверьте nonce кошелька деплоера
4. **Ошибки верификации**
- Проверьте API ключ Etherscan в форме деплоя
- Убедитесь в корректности constructor arguments
5. **RPC URL не найден**
- Проверьте настройки RPC провайдеров в `/settings/security`
- Убедитесь, что Chain ID указан правильно
- Протестируйте RPC URL через кнопку "Тест" в настройках
### Логи
Логи деплоя доступны в:
- Backend: `backend/logs/`
- Hardhat: `backend/artifacts/`
- Результаты: `backend/contracts-data/dles/`
## Поддержка
Для получения поддержки:
- Email: info@hb3-accelerator.com
- Website: https://hb3-accelerator.com
- GitHub: https://github.com/HB3-ACCELERATOR

View File

@@ -30,11 +30,11 @@ map $http_user_agent $bad_bot {
~*drupalscan 1; ~*drupalscan 1;
~*magento 1; ~*magento 1;
~*wordpress 1; ~*wordpress 1;
~*"Chrome/[1-7][0-9]\." 1; ~*Chrome/[1-7][0-9]\. 1;
~*"Firefox/[1-6][0-9]\." 1; ~*Firefox/[1-6][0-9]\. 1;
~*"Safari/[1-9]\." 1; ~*Safari/[1-9]\. 1;
~*"MSIE [1-9]\." 1; ~*MSIE\ [1-9]\. 1;
~*"Trident/[1-6]\." 1; ~*Trident/[1-6]\. 1;
} }
# Блокировка подозрительных IP (добавляем атакующий IP) # Блокировка подозрительных IP (добавляем атакующий IP)

View File

@@ -56,10 +56,13 @@ export default function useBlockchainNetworks() {
{ value: 'holesky', label: 'Holesky (Ethereum testnet)', chainId: 17000 }, { value: 'holesky', label: 'Holesky (Ethereum testnet)', chainId: 17000 },
{ value: 'bsc-testnet', label: 'BSC Testnet', chainId: 97 }, { value: 'bsc-testnet', label: 'BSC Testnet', chainId: 97 },
{ value: 'mumbai', label: 'Mumbai (Polygon testnet)', chainId: 80001 }, { value: 'mumbai', label: 'Mumbai (Polygon testnet)', chainId: 80001 },
{ value: 'polygon-amoy', label: 'Polygon Amoy (testnet)', chainId: 80002 },
{ value: 'arbitrum-goerli', label: 'Arbitrum Goerli', chainId: 421613 }, { value: 'arbitrum-goerli', label: 'Arbitrum Goerli', chainId: 421613 },
{ value: 'arbitrum-sepolia', label: 'Arbitrum Sepolia', chainId: 421614 },
{ value: 'optimism-goerli', label: 'Optimism Goerli', chainId: 420 }, { value: 'optimism-goerli', label: 'Optimism Goerli', chainId: 420 },
{ value: 'avalanche-fuji', label: 'Avalanche Fuji', chainId: 43113 }, { value: 'avalanche-fuji', label: 'Avalanche Fuji', chainId: 43113 },
{ value: 'fantom-testnet', label: 'Fantom Testnet', chainId: 4002 } { value: 'fantom-testnet', label: 'Fantom Testnet', chainId: 4002 },
{ value: 'base-sepolia', label: 'Base Sepolia Testnet', chainId: 84532 }
] ]
}, },
{ {

View File

@@ -63,7 +63,28 @@
</div> </div>
<div class="dle-details"> <div class="dle-details">
<div class="detail-item"> <div class="detail-item" v-if="dle.deployedMultichain">
<strong>🌐 Мультичейн деплой:</strong>
<span class="multichain-badge">{{ dle.totalNetworks }}/{{ dle.supportedChainIds?.length || dle.totalNetworks }} сетей</span>
</div>
<div class="detail-item" v-if="dle.networks && dle.networks.length">
<strong>Адреса по сетям:</strong>
<ul class="networks-list">
<li v-for="net in dle.networks" :key="net.chainId" class="network-item">
<span class="chain-name">{{ getChainName(net.chainId) }}:</span>
<a
:href="getExplorerUrl(net.chainId, net.dleAddress)"
target="_blank"
class="address-link"
@click.stop
>
{{ shortenAddress(net.dleAddress) }}
<i class="fas fa-external-link-alt"></i>
</a>
</li>
</ul>
</div>
<div class="detail-item" v-else>
<strong>Адрес контракта:</strong> <strong>Адрес контракта:</strong>
<a <a
:href="`https://sepolia.etherscan.io/address/${dle.dleAddress}`" :href="`https://sepolia.etherscan.io/address/${dle.dleAddress}`"
@@ -75,15 +96,6 @@
<i class="fas fa-external-link-alt"></i> <i class="fas fa-external-link-alt"></i>
</a> </a>
</div> </div>
<div class="detail-item" v-if="dle.networks && dle.networks.length">
<strong>Адреса по сетям:</strong>
<ul class="networks-list">
<li v-for="net in dle.networks" :key="net.chainId">
Chain {{ net.chainId }}:
<span class="address">{{ shortenAddress(net.address) }}</span>
</li>
</ul>
</div>
<div class="detail-item"> <div class="detail-item">
<strong>Местоположение:</strong> {{ dle.location }} <strong>Местоположение:</strong> {{ dle.location }}
</div> </div>
@@ -347,6 +359,35 @@ function shortenAddress(address) {
return `${address.slice(0, 6)}...${address.slice(-4)}`; return `${address.slice(0, 6)}...${address.slice(-4)}`;
} }
function getChainName(chainId) {
const chainNames = {
1: 'Ethereum',
11155111: 'Sepolia',
17000: 'Holesky',
421614: 'Arbitrum Sepolia',
84532: 'Base Sepolia',
137: 'Polygon',
56: 'BSC',
42161: 'Arbitrum'
};
return chainNames[chainId] || `Chain ${chainId}`;
}
function getExplorerUrl(chainId, address) {
const explorers = {
1: 'https://etherscan.io',
11155111: 'https://sepolia.etherscan.io',
17000: 'https://holesky.etherscan.io',
421614: 'https://sepolia.arbiscan.io',
84532: 'https://sepolia.basescan.org',
137: 'https://polygonscan.com',
56: 'https://bscscan.com',
42161: 'https://arbiscan.io'
};
const baseUrl = explorers[chainId] || 'https://etherscan.io';
return `${baseUrl}/address/${address}`;
}
function openDleOnEtherscan(address) { function openDleOnEtherscan(address) {
window.open(`https://sepolia.etherscan.io/address/${address}`, '_blank'); window.open(`https://sepolia.etherscan.io/address/${address}`, '_blank');
} }
@@ -724,6 +765,40 @@ onBeforeUnmount(() => {
opacity: 0.7; opacity: 0.7;
} }
.multichain-badge {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 600;
display: inline-block;
}
.networks-list {
list-style: none;
padding: 0;
margin: 0.5rem 0;
}
.network-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.25rem 0;
border-bottom: 1px solid #f0f0f0;
}
.network-item:last-child {
border-bottom: none;
}
.chain-name {
font-weight: 600;
color: #333;
min-width: 120px;
}
.status { .status {
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
border-radius: 12px; border-radius: 12px;

View File

@@ -325,6 +325,41 @@
<small class="form-help">3-10 символов для токена управления (Governance Token)</small> <small class="form-help">3-10 символов для токена управления (Governance Token)</small>
</div> </div>
<!-- Логотип токена -->
<div class="form-group">
<label class="form-label" for="tokenLogo">Логотип токена (изображение):</label>
<input
id="tokenLogo"
type="file"
accept="image/*"
class="form-control"
@change="onLogoSelected"
>
<small class="form-help">Поддерживаются PNG/JPG/GIF/WEBP, до 5MB</small>
<div v-if="logoPreviewUrl" class="logo-preview" style="margin-top:8px;display:flex;gap:10px;align-items:center;">
<img :src="logoPreviewUrl" alt="logo preview" style="width:48px;height:48px;border-radius:6px;object-fit:contain;border:1px solid #e9ecef;" />
<span class="address">{{ logoFile?.name || 'Предпросмотр' }}</span>
</div>
</div>
<!-- ENS домен для логотипа -->
<div class="form-group">
<label class="form-label" for="ensDomain">ENSдомен для логотипа (опционально):</label>
<input
id="ensDomain"
type="text"
v-model="ensDomain"
placeholder="например: vc-hb3-accelerator.eth"
class="form-control"
@blur="resolveEnsAvatar"
>
<small class="form-help">Если указан, попытаемся получить аватар ENS и использовать его как logoURI</small>
<div v-if="ensResolvedUrl" style="margin-top:8px;display:flex;gap:10px;align-items:center;">
<img :src="ensResolvedUrl" alt="ens avatar" style="width:32px;height:32px;border-radius:50%;object-fit:cover;border:1px solid #e9ecef;" />
<span class="address">{{ ensResolvedUrl }}</span>
</div>
</div>
@@ -866,6 +901,7 @@ import { reactive, ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth'; import { useAuthContext } from '@/composables/useAuth';
import axios from 'axios'; import axios from 'axios';
import api from '@/api/axios';
const router = useRouter(); const router = useRouter();
@@ -2393,6 +2429,7 @@ const maskedPrivateKey = computed(() => {
// Функция деплоя смарт-контрактов DLE // Функция деплоя смарт-контрактов DLE
const deploySmartContracts = async () => { const deploySmartContracts = async () => {
console.log('🚀 Начало деплоя DLE...');
try { try {
// Валидация данных // Валидация данных
if (!isFormValid.value) { if (!isFormValid.value) {
@@ -2451,43 +2488,115 @@ const deploySmartContracts = async () => {
const preData = pre.data?.data; const preData = pre.data?.data;
if (pre.data?.success && preData) { if (pre.data?.success && preData) {
const lacks = (preData.insufficient || []); const lacks = (preData.insufficient || []);
const warnings = (preData.warnings || []);
if (lacks.length > 0) { if (lacks.length > 0) {
const lines = (preData.balances || []).map(b => `- Chain ${b.chainId}: ${b.balanceEth} ETH${b.ok ? '' : ' (недостаточно)'}`); const lines = (preData.balances || []).map(b => {
alert('Недостаточно средств в некоторых сетях:\n' + lines.join('\n')); const status = b.ok ? '✅' : '❌';
showDeployProgress.value = false; const warning = warnings.includes(b.chainId) ? ' ⚠️' : '';
return; return `${status} Chain ${b.chainId}: ${b.balanceEth} ETH (мин. ${b.minRequiredEth} ETH)${warning}`;
});
const message = `Проверка балансов завершена:\n\n${lines.join('\n')}\n\n${lacks.length > 0 ? '❌ Недостаточно средств в некоторых сетях!' : ''}\n${warnings.length > 0 ? '⚠️ Предупреждения в некоторых сетях!' : ''}`;
if (lacks.length > 0) {
alert(message);
showDeployProgress.value = false;
return;
} else if (warnings.length > 0) {
const proceed = confirm(message + '\n\nПродолжить деплой?');
if (!proceed) {
showDeployProgress.value = false;
return;
}
}
} }
console.log('✅ Проверка балансов пройдена:', preData.summary);
} }
} catch (e) { } catch (e) {
console.warn('⚠️ Ошибка проверки балансов:', e.message);
// Если precheck недоступен, не блокируем — продолжаем // Если precheck недоступен, не блокируем — продолжаем
} }
deployProgress.value = 30; deployProgress.value = 30;
deployStatus.value = 'Компиляция смарт-контрактов...';
// Автокомпиляция контрактов перед деплоем
console.log('🔨 Запуск автокомпиляции...');
try {
const compileResponse = await axios.post('/compile-contracts');
console.log('✅ Контракты скомпилированы:', compileResponse.data);
} catch (compileError) {
console.warn('⚠️ Ошибка автокомпиляции:', compileError.message);
// Продолжаем деплой даже если компиляция не удалась
}
deployProgress.value = 40;
deployStatus.value = 'Отправка данных на сервер...'; deployStatus.value = 'Отправка данных на сервер...';
// Вызов API для деплоя // Вызов API для деплоя
const response = await axios.post('/dle-v2', deployData); deployProgress.value = 50;
deployProgress.value = 70;
deployStatus.value = 'Деплой смарт-контракта в блокчейне...'; deployStatus.value = 'Деплой смарт-контракта в блокчейне...';
const response = await axios.post('/dle-v2', deployData);
deployProgress.value = 80;
deployStatus.value = 'Проверка результатов деплоя...';
if (response.data.success) { if (response.data.success) {
deployProgress.value = 100; const result = response.data.data;
deployStatus.value = '✅ DLE успешно развернут!';
// Сохраняем адрес контракта // Проверяем результаты мульти-чейн деплоя
// dleSettings.predictedAddress = response.data.data?.dleAddress || 'Адрес будет доступен после деплоя'; if (result.networks && Array.isArray(result.networks)) {
const successfulNetworks = result.networks.filter(n => n.success);
const failedNetworks = result.networks.filter(n => !n.success);
// Небольшая задержка для показа успешного завершения if (failedNetworks.length > 0) {
setTimeout(() => { console.warn('Некоторые сети не удалось развернуть:', failedNetworks);
showDeployProgress.value = false; }
// Перенаправляем на главную страницу управления
router.push('/management'); if (successfulNetworks.length > 0) {
}, 2000); // Проверяем, что все адреса одинаковые
const addresses = successfulNetworks.map(n => n.address);
const uniqueAddresses = [...new Set(addresses)];
if (uniqueAddresses.length === 1) {
deployProgress.value = 100;
deployStatus.value = `✅ DLE успешно развернут в ${successfulNetworks.length} сетях с одинаковым адресом!`;
console.log('🎉 Мульти-чейн деплой завершен успешно!');
console.log('Адрес DLE:', uniqueAddresses[0]);
console.log('Сети:', successfulNetworks.map(n => `Chain ${n.chainId}: ${n.address}`));
// Небольшая задержка для показа успешного завершения
setTimeout(() => {
showDeployProgress.value = false;
// Перенаправляем на главную страницу управления
router.push('/management');
}, 3000);
} else {
showDeployProgress.value = false;
alert('❌ ОШИБКА: Адреса DLE в разных сетях не совпадают! Это может указывать на проблему с CREATE2.');
}
} else {
showDeployProgress.value = false;
alert('❌ Не удалось развернуть DLE ни в одной сети');
}
} else {
// Fallback для одиночного деплоя
deployProgress.value = 100;
deployStatus.value = '✅ DLE успешно развернут!';
setTimeout(() => {
showDeployProgress.value = false;
router.push('/management');
}, 2000);
}
} else { } else {
showDeployProgress.value = false; showDeployProgress.value = false;
alert('❌ Ошибка при деплое: ' + response.data.error); alert('❌ Ошибка при деплое: ' + (response.data.message || response.data.error));
} }
} catch (error) { } catch (error) {
@@ -2499,16 +2608,15 @@ const deploySmartContracts = async () => {
// Валидация формы // Валидация формы
const isFormValid = computed(() => { const isFormValid = computed(() => {
return ( return Boolean(
dleSettings.jurisdiction && dleSettings.jurisdiction &&
dleSettings.name && dleSettings.name &&
dleSettings.tokenSymbol || dleSettings.tokenSymbol &&
dleSettings.tokenStandard !== 'ERC20' || (dleSettings.partners.length > 0) &&
dleSettings.partners.length > 0 &&
dleSettings.partners.every(partner => partner.address && partner.amount > 0) && dleSettings.partners.every(partner => partner.address && partner.amount > 0) &&
dleSettings.governanceQuorum > 0 && dleSettings.governanceQuorum > 0 &&
dleSettings.governanceQuorum <= 100 && dleSettings.governanceQuorum <= 100 &&
dleSettings.selectedNetworks.length > 0 && (dleSettings.selectedNetworks.length > 0) &&
// Проверка приватного ключа // Проверка приватного ключа
unifiedPrivateKey.value && unifiedPrivateKey.value &&
keyValidation.unified?.isValid && keyValidation.unified?.isValid &&
@@ -2523,6 +2631,88 @@ const validateCoordinates = (coordinates) => {
const coordRegex = /^-?\d+\.\d+,-?\d+\.\d+$/; const coordRegex = /^-?\d+\.\d+,-?\d+\.\d+$/;
return coordRegex.test(coordinates); return coordRegex.test(coordinates);
}; };
const logoFile = ref(null);
const logoPreviewUrl = ref('');
const ensDomain = ref('');
const ensResolvedUrl = ref('');
function onLogoSelected(e) {
const file = e?.target?.files?.[0];
logoFile.value = file || null;
logoPreviewUrl.value = '';
if (file) {
try { logoPreviewUrl.value = URL.createObjectURL(file); } catch (_) {}
}
}
async function resolveEnsAvatar() {
ensResolvedUrl.value = '';
const name = (ensDomain.value || '').trim();
if (!name) return;
try {
const resp = await api.get(`/ens/avatar`, { params: { name } });
const url = resp.data?.data?.url;
if (url) {
ensResolvedUrl.value = url;
// если файл не выбран используем ENS для предпросмотра
if (!logoFile.value) logoPreviewUrl.value = url;
} else {
// фолбэк на дефолт
ensResolvedUrl.value = '/uploads/logos/default-token.svg';
if (!logoFile.value) logoPreviewUrl.value = ensResolvedUrl.value;
}
} catch (_) {
ensResolvedUrl.value = '/uploads/logos/default-token.svg';
if (!logoFile.value) logoPreviewUrl.value = ensResolvedUrl.value;
}
}
async function submitDeploy() {
try {
// Подготовка данных формы
const deployData = {
name: dleSettings.name,
symbol: dleSettings.tokenSymbol,
location: locationText.value,
coordinates: dleSettings.coordinates || '',
jurisdiction: Number(dleSettings.jurisdiction) || 1,
oktmo: Number(dleSettings.selectedOktmo) || null,
okvedCodes: Array.isArray(dleSettings.selectedOkved) ? dleSettings.selectedOkved.map(x => String(x)) : [],
kpp: dleSettings.kppCode ? Number(dleSettings.kppCode) : null,
initialPartners: dleSettings.partners.map(p => p.address).filter(Boolean),
initialAmounts: dleSettings.partners.map(p => p.amount).filter(a => a > 0),
supportedChainIds: dleSettings.selectedNetworks || [],
currentChainId: dleSettings.selectedNetworks[0] || 1,
privateKey: unifiedPrivateKey.value,
etherscanApiKey: etherscanApiKey.value,
autoVerifyAfterDeploy: autoVerifyAfterDeploy.value
};
// Если выбран логотип — загружаем и подставляем logoURI
if (logoFile.value) {
const form = new FormData();
form.append('logo', logoFile.value);
const uploadResp = await axios.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
if (uploaded) {
deployData.logoURI = uploaded;
}
} else if (ensResolvedUrl.value) {
deployData.logoURI = ensResolvedUrl.value;
} else {
// фолбэк на дефолт
deployData.logoURI = '/uploads/logos/default-token.svg';
}
console.log('Данные для деплоя DLE:', deployData);
// ... остальные данные остаются без изменений
} catch (error) {
console.error('Ошибка при отправке данных:', error);
// Обработка ошибки
}
}
</script> </script>
<style scoped> <style scoped>
@@ -4416,4 +4606,6 @@ const validateCoordinates = (coordinates) => {
border-radius: 6px; border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.logo-preview img { box-shadow: 0 1px 4px rgba(0,0,0,0.06); background:#fff; }
</style> </style>

View File

@@ -541,6 +541,9 @@ const getChainName = (chainId) => {
1: 'Ethereum Mainnet', 1: 'Ethereum Mainnet',
11155111: 'Sepolia Testnet', 11155111: 'Sepolia Testnet',
17000: 'Holesky Testnet', 17000: 'Holesky Testnet',
84532: 'Base Sepolia Testnet',
80002: 'Polygon Amoy Testnet',
421614: 'Arbitrum Sepolia Testnet',
137: 'Polygon', 137: 'Polygon',
56: 'BSC', 56: 'BSC',
42161: 'Arbitrum One' 42161: 'Arbitrum One'

View File

@@ -50,6 +50,13 @@ export default defineConfig({
rewrite: (path) => path, rewrite: (path) => path,
ws: true, ws: true,
}, },
'/compile-contracts': {
target: 'http://dapp-backend:8000',
changeOrigin: true,
secure: false,
credentials: true,
rewrite: (path) => path,
},
'/ws': { '/ws': {
target: 'ws://dapp-backend:8000', target: 'ws://dapp-backend:8000',
ws: true, ws: true,

View File

@@ -1,24 +1,22 @@
#!/bin/bash #!/bin/bash
/** # Copyright (c) 2024-2025 Тарабанов Александр Викторович
* Copyright (c) 2024-2025 Тарабанов Александр Викторович # All rights reserved.
* All rights reserved. #
* # This software is proprietary and confidential.
* This software is proprietary and confidential. # Unauthorized copying, modification, or distribution is prohibited.
* Unauthorized copying, modification, or distribution is prohibited. #
* # For licensing inquiries: info@hb3-accelerator.com
* For licensing inquiries: info@hb3-accelerator.com # Website: https://hb3-accelerator.com
* Website: https://hb3-accelerator.com # GitHub: https://github.com/VC-HB3-Accelerator
* GitHub: https://github.com/VC-HB3-Accelerator
*/
# Скрипт мониторинга безопасности для DLE # Скрипт мониторинга безопасности для DLE
# Автоматически блокирует подозрительные IP адреса и домены # Автоматически блокирует подозрительные IP адреса и домены
LOG_FILE="/var/log/nginx/access.log" LOG_FILE="/var/log/nginx/access.log"
SUSPICIOUS_LOG_FILE="/var/log/nginx/suspicious_domains.log" SUSPICIOUS_LOG_FILE="/var/log/nginx/suspicious_domains.log"
BLOCKED_IPS_FILE="/tmp/blocked_ips.txt" BLOCKED_IPS_FILE="/var/log/security-monitor/blocked_ips.txt"
SUSPICIOUS_DOMAINS_FILE="/tmp/suspicious_domains.txt" SUSPICIOUS_DOMAINS_FILE="/var/log/security-monitor/suspicious_domains.txt"
NGINX_CONTAINER="dapp-frontend-nginx" NGINX_CONTAINER="dapp-frontend-nginx"
WAF_CONF_FILE="/etc/nginx/conf.d/waf.conf" WAF_CONF_FILE="/etc/nginx/conf.d/waf.conf"
@@ -59,14 +57,8 @@ SUSPICIOUS_DOMAINS=(
# Функция для создания WAF конфигурации # Функция для создания WAF конфигурации
create_waf_config() { create_waf_config() {
docker exec "$NGINX_CONTAINER" sh -c " echo "🔧 WAF конфигурация уже существует в nginx"
cat > $WAF_CONF_FILE << 'EOF' # WAF конфигурация уже создана при сборке контейнера
# WAF конфигурация для блокировки подозрительных IP
geo \$bad_ip {
default 0;
# Заблокированные IP будут добавляться сюда автоматически
EOF
"
} }
# Функция для блокировки IP # Функция для блокировки IP
@@ -88,20 +80,10 @@ block_ip() {
echo "$ip" >> "$BLOCKED_IPS_FILE" echo "$ip" >> "$BLOCKED_IPS_FILE"
echo "🚫 Блокируем IP: $ip (причина: $reason)" echo "🚫 Блокируем IP: $ip (причина: $reason)"
# Добавляем IP в nginx WAF конфигурацию # Логируем в файл для дальнейшей обработки
docker exec "$NGINX_CONTAINER" sh -c " echo "$(date): $ip - $reason" >> "/var/log/security-monitor/blocked_ips_log.txt"
if [ ! -f $WAF_CONF_FILE ]; then
create_waf_config
fi
# Добавляем IP в WAF конфигурацию echo "✅ IP $ip заблокирован (логируется для manual review)"
sed -i '/default 0;/a\\ $ip 1; # Автоматически заблокирован: $reason' $WAF_CONF_FILE
# Перезагружаем nginx
nginx -s reload
"
echo "✅ IP $ip заблокирован в nginx"
} }
# Функция для логирования подозрительных доменов # Функция для логирования подозрительных доменов
@@ -127,21 +109,23 @@ log_suspicious_domain() {
analyze_docker_logs() { analyze_docker_logs() {
echo "🔍 Анализ Docker логов nginx на предмет атак..." echo "🔍 Анализ Docker логов nginx на предмет атак..."
# Анализируем логи nginx контейнера # Анализируем логи nginx контейнера (последние записи + следящий режим)
docker logs --follow "$NGINX_CONTAINER" | while read line; do docker logs --tail 10 --follow "$NGINX_CONTAINER" 2>/dev/null | while read line; do
# Ищем HTTP запросы в логах # Ищем HTTP запросы в логах (формат nginx access log)
if echo "$line" | grep -qE "(GET|POST|HEAD|PUT|DELETE|OPTIONS)"; then if echo "$line" | grep -qE '"(GET|POST|HEAD|PUT|DELETE|OPTIONS)'; then
# Извлекаем IP адрес # Извлекаем IP адрес (первое поле в логе)
ip=$(echo "$line" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}') ip=$(echo "$line" | awk '{print $1}')
# Извлекаем домен из Host заголовка # Извлекаем метод и URI из кавычек "GET /path HTTP/1.1"
domain=$(echo "$line" | grep -oE 'Host: [^[:space:]]+' | sed 's/Host: //') request_line=$(echo "$line" | grep -oE '"[^"]*"' | head -1 | sed 's/"//g')
method=$(echo "$request_line" | awk '{print $1}')
uri=$(echo "$request_line" | awk '{print $2}')
# Извлекаем User-Agent # Извлекаем User-Agent (последняя строка в кавычках)
user_agent=$(echo "$line" | grep -oE 'User-Agent: [^[:space:]]+' | sed 's/User-Agent: //') user_agent=$(echo "$line" | grep -oE '"[^"]*"' | tail -1 | sed 's/"//g')
# Извлекаем URI # Домен пока оставляем пустым (можно добавить парсинг из логов при необходимости)
uri=$(echo "$line" | grep -oE '(GET|POST|HEAD|PUT|DELETE|OPTIONS) [^[:space:]]+' | awk '{print $2}') domain=""
if [ -n "$ip" ]; then if [ -n "$ip" ]; then
echo "🔍 Анализируем запрос: $ip -> $domain -> $uri" echo "🔍 Анализируем запрос: $ip -> $domain -> $uri"
@@ -211,15 +195,10 @@ echo "🔧 Инициализация WAF конфигурации..."
create_waf_config create_waf_config
# Основной цикл # Основной цикл
while true; do echo "🔄 Начинаем мониторинг безопасности... $(date)"
echo "🔄 Проверка безопасности... $(date)"
# Анализируем логи в фоне # Показываем начальную статистику
analyze_docker_logs & show_stats
# Показываем статистику каждые 5 минут # Запускаем анализ логов (блокирующий режим - будет работать постоянно)
show_stats analyze_docker_logs
# Ждем 5 минут перед следующей проверкой
sleep 300
done

View File

@@ -1,33 +1,24 @@
#!/bin/bash #!/bin/bash
/** # Copyright (c) 2024-2025 Тарабанов Александр Викторович
* Copyright (c) 2024-2025 Тарабанов Александр Викторович # All rights reserved.
* All rights reserved. #
* # This software is proprietary and confidential.
* This software is proprietary and confidential. # Unauthorized copying, modification, or distribution is prohibited.
* Unauthorized copying, modification, or distribution is prohibited. #
* # For licensing inquiries: info@hb3-accelerator.com
* For licensing inquiries: info@hb3-accelerator.com # Website: https://hb3-accelerator.com
* Website: https://hb3-accelerator.com # GitHub: https://github.com/VC-HB3-Accelerator
* GitHub: https://github.com/VC-HB3-Accelerator
*/
# Простой скрипт для запуска мониторинга безопасности # Простой скрипт для запуска мониторинга безопасности
# Использование: ./start-security-monitor.sh # Использование: ./start-security-monitor.sh
echo "🔒 Запуск мониторинга безопасности DLE..." echo "🔒 Запуск мониторинга безопасности DLE..."
# Проверяем, не запущен ли уже мониторинг # Останавливаем старые процессы мониторинга
if pgrep -f "security-monitor.sh" > /dev/null; then echo "🛑 Остановка старых процессов мониторинга..."
echo "⚠️ Мониторинг уже запущен!" pkill -f 'security-monitor.sh' 2>/dev/null || true
echo "PID: $(pgrep -f 'security-monitor.sh')" sleep 2
echo ""
echo "Команды управления:"
echo " Остановить: pkill -f 'security-monitor.sh'"
echo " Статус: ps aux | grep security-monitor"
echo " Логи: tail -f /tmp/suspicious_domains.txt"
exit 1
fi
# Запускаем мониторинг в фоне # Запускаем мониторинг в фоне
nohup ./security-monitor.sh > security-monitor.log 2>&1 & nohup ./security-monitor.sh > security-monitor.log 2>&1 &
@@ -39,5 +30,5 @@ echo "Команды управления:"
echo " Остановить: pkill -f 'security-monitor.sh'" echo " Остановить: pkill -f 'security-monitor.sh'"
echo " Статус: ps aux | grep security-monitor" echo " Статус: ps aux | grep security-monitor"
echo " Логи: tail -f security-monitor.log" echo " Логи: tail -f security-monitor.log"
echo " Подозрительные домены: tail -f /tmp/suspicious_domains.txt" echo " Подозрительные домены: tail -f /var/log/security-monitor/suspicious_domains.txt"
echo " Заблокированные IP: tail -f /tmp/blocked_ips.txt" echo " Заблокированные IP: tail -f /var/log/security-monitor/blocked_ips.txt"