ваше сообщение коммита
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -96,9 +96,6 @@ docker-compose restart
|
|||||||
# Остановка сервисов
|
# Остановка сервисов
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
# Остановка сервисов и удаление томов
|
|
||||||
docker compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Контакты и поддержка
|
## Контакты и поддержка
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"_format": "hh-sol-dbg-1",
|
|
||||||
"buildInfo": "../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
|
|
||||||
}
|
|
||||||
@@ -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": {}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"_format": "hh-sol-dbg-1",
|
|
||||||
"buildInfo": "../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
|
|
||||||
}
|
|
||||||
@@ -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": {}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"_format": "hh-sol-dbg-1",
|
|
||||||
"buildInfo": "../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
|
|
||||||
}
|
|
||||||
@@ -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": {}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"_format": "hh-sol-dbg-1",
|
|
||||||
"buildInfo": "../../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
|
|
||||||
}
|
|
||||||
@@ -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": {}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"_format": "hh-sol-dbg-1",
|
|
||||||
"buildInfo": "../../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
|
|
||||||
}
|
|
||||||
@@ -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": {}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"_format": "hh-sol-dbg-1",
|
|
||||||
"buildInfo": "../../../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
|
|
||||||
}
|
|
||||||
@@ -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": {}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"_format": "hh-sol-dbg-1",
|
|
||||||
"buildInfo": "../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"_format": "hh-sol-artifact-1",
|
|
||||||
"contractName": "Context",
|
|
||||||
"sourceName": "@openzeppelin/contracts/utils/Context.sol",
|
|
||||||
"abi": [],
|
|
||||||
"bytecode": "0x",
|
|
||||||
"deployedBytecode": "0x",
|
|
||||||
"linkReferences": {},
|
|
||||||
"deployedLinkReferences": {}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"_format": "hh-sol-dbg-1",
|
|
||||||
"buildInfo": "../../../../build-info/ab387c71734b3d3e5e7817d328027586.json"
|
|
||||||
}
|
|
||||||
@@ -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": {}
|
|
||||||
}
|
|
||||||
@@ -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
1137
backend/cache/solidity-files-cache.json
vendored
1137
backend/cache/solidity-files-cache.json
vendored
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
360
backend/contracts/DLEReader.sol
Normal file
360
backend/contracts/DLEReader.sol
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
22
backend/contracts/MockNoop.sol
Normal file
22
backend/contracts/MockNoop.sol
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
backend/contracts/MockToken.sol
Normal file
29
backend/contracts/MockToken.sol
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
399
backend/contracts/TimelockModule.sol
Normal file
399
backend/contracts/TimelockModule.sol
Normal 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; // Отключение скомпрометированной сети
|
||||||
|
}
|
||||||
|
}
|
||||||
527
backend/contracts/TreasuryModule.sol
Normal file
527
backend/contracts/TreasuryModule.sol
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
// SPDX-License-Identifier: PROPRIETARY AND MIT
|
||||||
|
// Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// This software is proprietary and confidential.
|
||||||
|
// Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
//
|
||||||
|
// For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
// Website: https://hb3-accelerator.com
|
||||||
|
// GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
|
||||||
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||||
|
import "@openzeppelin/contracts/utils/Address.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title TreasuryModule
|
||||||
|
* @dev Модуль казны для управления активами DLE
|
||||||
|
*
|
||||||
|
* ОСНОВНЫЕ ФУНКЦИИ:
|
||||||
|
* - Управление различными ERC20 токенами
|
||||||
|
* - Хранение и перевод нативных монет (ETH, BNB, MATIC и т.д.)
|
||||||
|
* - Интеграция с DLE governance для авторизации операций
|
||||||
|
* - Поддержка мульти-чейн операций
|
||||||
|
* - Batch операции для оптимизации газа
|
||||||
|
*
|
||||||
|
* БЕЗОПАСНОСТЬ:
|
||||||
|
* - Только DLE контракт может выполнять операции
|
||||||
|
* - Защита от реентерабельности
|
||||||
|
* - Валидация всех входных параметров
|
||||||
|
* - Поддержка emergency pause
|
||||||
|
*/
|
||||||
|
contract TreasuryModule is ReentrancyGuard {
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
using Address for address payable;
|
||||||
|
|
||||||
|
// Структура для информации о токене
|
||||||
|
struct TokenInfo {
|
||||||
|
address tokenAddress; // Адрес токена (0x0 для нативной монеты)
|
||||||
|
string symbol; // Символ токена
|
||||||
|
uint8 decimals; // Количество знаков после запятой
|
||||||
|
bool isActive; // Активен ли токен
|
||||||
|
bool isNative; // Является ли нативной монетой
|
||||||
|
uint256 addedTimestamp; // Время добавления
|
||||||
|
uint256 balance; // Кэшированный баланс (обновляется при операциях)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Структура для batch операции
|
||||||
|
struct BatchTransfer {
|
||||||
|
address tokenAddress; // Адрес токена (0x0 для нативной монеты)
|
||||||
|
address recipient; // Получатель
|
||||||
|
uint256 amount; // Количество
|
||||||
|
}
|
||||||
|
|
||||||
|
// Основные переменные
|
||||||
|
address public immutable dleContract; // Адрес основного DLE контракта
|
||||||
|
uint256 public immutable chainId; // ID текущей сети
|
||||||
|
|
||||||
|
// Хранение токенов
|
||||||
|
mapping(address => TokenInfo) public supportedTokens; // tokenAddress => TokenInfo
|
||||||
|
address[] public tokenList; // Список всех добавленных токенов
|
||||||
|
mapping(address => uint256) public tokenIndex; // tokenAddress => index в tokenList
|
||||||
|
|
||||||
|
// Статистика
|
||||||
|
uint256 public totalTokensSupported;
|
||||||
|
uint256 public totalTransactions;
|
||||||
|
mapping(address => uint256) public tokenTransactionCount; // tokenAddress => count
|
||||||
|
|
||||||
|
// Система экстренного останова
|
||||||
|
bool public emergencyPaused;
|
||||||
|
address public emergencyAdmin;
|
||||||
|
|
||||||
|
// События
|
||||||
|
event TokenAdded(
|
||||||
|
address indexed tokenAddress,
|
||||||
|
string symbol,
|
||||||
|
uint8 decimals,
|
||||||
|
bool isNative,
|
||||||
|
uint256 timestamp
|
||||||
|
);
|
||||||
|
event TokenRemoved(address indexed tokenAddress, string symbol, uint256 timestamp);
|
||||||
|
event TokenStatusUpdated(address indexed tokenAddress, bool newStatus);
|
||||||
|
event FundsDeposited(
|
||||||
|
address indexed tokenAddress,
|
||||||
|
address indexed from,
|
||||||
|
uint256 amount,
|
||||||
|
uint256 newBalance
|
||||||
|
);
|
||||||
|
event FundsTransferred(
|
||||||
|
address indexed tokenAddress,
|
||||||
|
address indexed to,
|
||||||
|
uint256 amount,
|
||||||
|
uint256 remainingBalance,
|
||||||
|
bytes32 indexed proposalId
|
||||||
|
);
|
||||||
|
event BatchTransferExecuted(
|
||||||
|
uint256 transferCount,
|
||||||
|
uint256 totalAmount,
|
||||||
|
bytes32 indexed proposalId
|
||||||
|
);
|
||||||
|
event EmergencyPauseToggled(bool isPaused, address admin);
|
||||||
|
event BalanceUpdated(address indexed tokenAddress, uint256 oldBalance, uint256 newBalance);
|
||||||
|
|
||||||
|
// Модификаторы
|
||||||
|
modifier onlyDLE() {
|
||||||
|
require(msg.sender == dleContract, "Only DLE contract can call this");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier whenNotPaused() {
|
||||||
|
require(!emergencyPaused, "Treasury is paused");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier onlyEmergencyAdmin() {
|
||||||
|
require(msg.sender == emergencyAdmin, "Only emergency admin");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier validToken(address tokenAddress) {
|
||||||
|
require(supportedTokens[tokenAddress].isActive, "Token not supported or inactive");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(address _dleContract, uint256 _chainId, address _emergencyAdmin) {
|
||||||
|
require(_dleContract != address(0), "DLE contract cannot be zero");
|
||||||
|
require(_emergencyAdmin != address(0), "Emergency admin cannot be zero");
|
||||||
|
require(_chainId > 0, "Chain ID must be positive");
|
||||||
|
|
||||||
|
dleContract = _dleContract;
|
||||||
|
chainId = _chainId;
|
||||||
|
emergencyAdmin = _emergencyAdmin;
|
||||||
|
|
||||||
|
// Автоматически добавляем нативную монету сети
|
||||||
|
_addNativeToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Получить средства (может вызывать кто угодно для пополнения казны)
|
||||||
|
*/
|
||||||
|
receive() external payable {
|
||||||
|
if (msg.value > 0) {
|
||||||
|
_updateTokenBalance(address(0), supportedTokens[address(0)].balance + msg.value);
|
||||||
|
emit FundsDeposited(address(0), msg.sender, msg.value, supportedTokens[address(0)].balance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Добавить новый токен в казну (только через DLE governance)
|
||||||
|
* @param tokenAddress Адрес токена (0x0 для нативной монеты)
|
||||||
|
* @param symbol Символ токена
|
||||||
|
* @param decimals Количество знаков после запятой
|
||||||
|
*/
|
||||||
|
function addToken(
|
||||||
|
address tokenAddress,
|
||||||
|
string memory symbol,
|
||||||
|
uint8 decimals
|
||||||
|
) external onlyDLE whenNotPaused {
|
||||||
|
require(!supportedTokens[tokenAddress].isActive, "Token already supported");
|
||||||
|
require(bytes(symbol).length > 0, "Symbol cannot be empty");
|
||||||
|
require(bytes(symbol).length <= 20, "Symbol too long");
|
||||||
|
|
||||||
|
// Для ERC20 токенов проверяем, что контракт существует
|
||||||
|
if (tokenAddress != address(0)) {
|
||||||
|
require(tokenAddress.code.length > 0, "Token contract does not exist");
|
||||||
|
|
||||||
|
// Проверяем базовые ERC20 функции
|
||||||
|
try IERC20(tokenAddress).totalSupply() returns (uint256) {
|
||||||
|
// Token contract is valid
|
||||||
|
} catch {
|
||||||
|
revert("Invalid ERC20 token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем токен
|
||||||
|
supportedTokens[tokenAddress] = TokenInfo({
|
||||||
|
tokenAddress: tokenAddress,
|
||||||
|
symbol: symbol,
|
||||||
|
decimals: decimals,
|
||||||
|
isActive: true,
|
||||||
|
isNative: tokenAddress == address(0),
|
||||||
|
addedTimestamp: block.timestamp,
|
||||||
|
balance: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
tokenList.push(tokenAddress);
|
||||||
|
tokenIndex[tokenAddress] = tokenList.length - 1;
|
||||||
|
totalTokensSupported++;
|
||||||
|
|
||||||
|
// Обновляем баланс
|
||||||
|
_refreshTokenBalance(tokenAddress);
|
||||||
|
|
||||||
|
emit TokenAdded(tokenAddress, symbol, decimals, tokenAddress == address(0), block.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Удалить токен из казны (только через DLE governance)
|
||||||
|
* @param tokenAddress Адрес токена для удаления
|
||||||
|
*/
|
||||||
|
function removeToken(address tokenAddress) external onlyDLE whenNotPaused validToken(tokenAddress) {
|
||||||
|
require(tokenAddress != address(0), "Cannot remove native token");
|
||||||
|
|
||||||
|
TokenInfo memory tokenInfo = supportedTokens[tokenAddress];
|
||||||
|
require(tokenInfo.balance == 0, "Token balance must be zero before removal");
|
||||||
|
|
||||||
|
// Удаляем из массива
|
||||||
|
uint256 index = tokenIndex[tokenAddress];
|
||||||
|
uint256 lastIndex = tokenList.length - 1;
|
||||||
|
|
||||||
|
if (index != lastIndex) {
|
||||||
|
address lastToken = tokenList[lastIndex];
|
||||||
|
tokenList[index] = lastToken;
|
||||||
|
tokenIndex[lastToken] = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenList.pop();
|
||||||
|
delete tokenIndex[tokenAddress];
|
||||||
|
delete supportedTokens[tokenAddress];
|
||||||
|
totalTokensSupported--;
|
||||||
|
|
||||||
|
emit TokenRemoved(tokenAddress, tokenInfo.symbol, block.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Изменить статус токена (активен/неактивен)
|
||||||
|
* @param tokenAddress Адрес токена
|
||||||
|
* @param isActive Новый статус
|
||||||
|
*/
|
||||||
|
function setTokenStatus(address tokenAddress, bool isActive) external onlyDLE {
|
||||||
|
require(supportedTokens[tokenAddress].tokenAddress == tokenAddress, "Token not found");
|
||||||
|
require(tokenAddress != address(0), "Cannot deactivate native token");
|
||||||
|
|
||||||
|
supportedTokens[tokenAddress].isActive = isActive;
|
||||||
|
emit TokenStatusUpdated(tokenAddress, isActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Перевести токены (только через DLE governance)
|
||||||
|
* @param tokenAddress Адрес токена (0x0 для нативной монеты)
|
||||||
|
* @param recipient Получатель
|
||||||
|
* @param amount Количество для перевода
|
||||||
|
* @param proposalId ID предложения DLE (для логирования)
|
||||||
|
*/
|
||||||
|
function transferFunds(
|
||||||
|
address tokenAddress,
|
||||||
|
address recipient,
|
||||||
|
uint256 amount,
|
||||||
|
bytes32 proposalId
|
||||||
|
) external onlyDLE whenNotPaused validToken(tokenAddress) nonReentrant {
|
||||||
|
require(recipient != address(0), "Recipient cannot be zero");
|
||||||
|
require(amount > 0, "Amount must be positive");
|
||||||
|
|
||||||
|
TokenInfo storage tokenInfo = supportedTokens[tokenAddress];
|
||||||
|
require(tokenInfo.balance >= amount, "Insufficient balance");
|
||||||
|
|
||||||
|
// Обновляем баланс
|
||||||
|
_updateTokenBalance(tokenAddress, tokenInfo.balance - amount);
|
||||||
|
|
||||||
|
// Выполняем перевод
|
||||||
|
if (tokenInfo.isNative) {
|
||||||
|
payable(recipient).sendValue(amount);
|
||||||
|
} else {
|
||||||
|
IERC20(tokenAddress).safeTransfer(recipient, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalTransactions++;
|
||||||
|
tokenTransactionCount[tokenAddress]++;
|
||||||
|
|
||||||
|
emit FundsTransferred(
|
||||||
|
tokenAddress,
|
||||||
|
recipient,
|
||||||
|
amount,
|
||||||
|
tokenInfo.balance,
|
||||||
|
proposalId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Выполнить batch перевод (только через DLE governance)
|
||||||
|
* @param transfers Массив переводов
|
||||||
|
* @param proposalId ID предложения DLE
|
||||||
|
*/
|
||||||
|
function batchTransfer(
|
||||||
|
BatchTransfer[] memory transfers,
|
||||||
|
bytes32 proposalId
|
||||||
|
) external onlyDLE whenNotPaused nonReentrant {
|
||||||
|
require(transfers.length > 0, "No transfers provided");
|
||||||
|
require(transfers.length <= 100, "Too many transfers"); // Защита от DoS
|
||||||
|
|
||||||
|
uint256 totalAmount = 0;
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < transfers.length; i++) {
|
||||||
|
BatchTransfer memory transfer = transfers[i];
|
||||||
|
|
||||||
|
require(transfer.recipient != address(0), "Recipient cannot be zero");
|
||||||
|
require(transfer.amount > 0, "Amount must be positive");
|
||||||
|
require(supportedTokens[transfer.tokenAddress].isActive, "Token not supported");
|
||||||
|
|
||||||
|
TokenInfo storage tokenInfo = supportedTokens[transfer.tokenAddress];
|
||||||
|
require(tokenInfo.balance >= transfer.amount, "Insufficient balance");
|
||||||
|
|
||||||
|
// Обновляем баланс
|
||||||
|
_updateTokenBalance(transfer.tokenAddress, tokenInfo.balance - transfer.amount);
|
||||||
|
|
||||||
|
// Выполняем перевод
|
||||||
|
if (tokenInfo.isNative) {
|
||||||
|
payable(transfer.recipient).sendValue(transfer.amount);
|
||||||
|
} else {
|
||||||
|
IERC20(transfer.tokenAddress).safeTransfer(transfer.recipient, transfer.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalAmount += transfer.amount;
|
||||||
|
tokenTransactionCount[transfer.tokenAddress]++;
|
||||||
|
|
||||||
|
emit FundsTransferred(
|
||||||
|
transfer.tokenAddress,
|
||||||
|
transfer.recipient,
|
||||||
|
transfer.amount,
|
||||||
|
tokenInfo.balance,
|
||||||
|
proposalId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
totalTransactions += transfers.length;
|
||||||
|
emit BatchTransferExecuted(transfers.length, totalAmount, proposalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Пополнить казну ERC20 токенами
|
||||||
|
* @param tokenAddress Адрес токена
|
||||||
|
* @param amount Количество для пополнения
|
||||||
|
*/
|
||||||
|
function depositToken(
|
||||||
|
address tokenAddress,
|
||||||
|
uint256 amount
|
||||||
|
) external whenNotPaused validToken(tokenAddress) nonReentrant {
|
||||||
|
require(amount > 0, "Amount must be positive");
|
||||||
|
require(tokenAddress != address(0), "Use receive() for native deposits");
|
||||||
|
|
||||||
|
IERC20(tokenAddress).safeTransferFrom(msg.sender, address(this), amount);
|
||||||
|
|
||||||
|
_updateTokenBalance(tokenAddress, supportedTokens[tokenAddress].balance + amount);
|
||||||
|
|
||||||
|
emit FundsDeposited(tokenAddress, msg.sender, amount, supportedTokens[tokenAddress].balance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Обновить баланс токена (синхронизация с реальным балансом)
|
||||||
|
* @param tokenAddress Адрес токена
|
||||||
|
*/
|
||||||
|
function refreshBalance(address tokenAddress) external validToken(tokenAddress) {
|
||||||
|
_refreshTokenBalance(tokenAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Обновить балансы всех токенов
|
||||||
|
*/
|
||||||
|
function refreshAllBalances() external {
|
||||||
|
for (uint256 i = 0; i < tokenList.length; i++) {
|
||||||
|
if (supportedTokens[tokenList[i]].isActive) {
|
||||||
|
_refreshTokenBalance(tokenList[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Экстренная пауза (только emergency admin)
|
||||||
|
*/
|
||||||
|
function emergencyPause() external onlyEmergencyAdmin {
|
||||||
|
emergencyPaused = !emergencyPaused;
|
||||||
|
emit EmergencyPauseToggled(emergencyPaused, msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== VIEW ФУНКЦИИ =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Получить информацию о токене
|
||||||
|
*/
|
||||||
|
function getTokenInfo(address tokenAddress) external view returns (TokenInfo memory) {
|
||||||
|
return supportedTokens[tokenAddress];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Получить список всех токенов
|
||||||
|
*/
|
||||||
|
function getAllTokens() external view returns (address[] memory) {
|
||||||
|
return tokenList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Получить активные токены
|
||||||
|
*/
|
||||||
|
function getActiveTokens() external view returns (address[] memory) {
|
||||||
|
uint256 activeCount = 0;
|
||||||
|
|
||||||
|
// Считаем активные токены
|
||||||
|
for (uint256 i = 0; i < tokenList.length; i++) {
|
||||||
|
if (supportedTokens[tokenList[i]].isActive) {
|
||||||
|
activeCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём массив активных токенов
|
||||||
|
address[] memory activeTokens = new address[](activeCount);
|
||||||
|
uint256 index = 0;
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < tokenList.length; i++) {
|
||||||
|
if (supportedTokens[tokenList[i]].isActive) {
|
||||||
|
activeTokens[index] = tokenList[i];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Получить баланс токена
|
||||||
|
*/
|
||||||
|
function getTokenBalance(address tokenAddress) external view returns (uint256) {
|
||||||
|
return supportedTokens[tokenAddress].balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Получить реальный баланс токена (обращение к блокчейну)
|
||||||
|
*/
|
||||||
|
function getRealTokenBalance(address tokenAddress) external view returns (uint256) {
|
||||||
|
if (tokenAddress == address(0)) {
|
||||||
|
return address(this).balance;
|
||||||
|
} else {
|
||||||
|
return IERC20(tokenAddress).balanceOf(address(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Проверить, поддерживается ли токен
|
||||||
|
*/
|
||||||
|
function isTokenSupported(address tokenAddress) external view returns (bool) {
|
||||||
|
return supportedTokens[tokenAddress].isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Получить статистику казны
|
||||||
|
*/
|
||||||
|
function getTreasuryStats() external view returns (
|
||||||
|
uint256 totalTokens,
|
||||||
|
uint256 totalTxs,
|
||||||
|
uint256 currentChainId,
|
||||||
|
bool isPaused
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
totalTokensSupported,
|
||||||
|
totalTransactions,
|
||||||
|
chainId,
|
||||||
|
emergencyPaused
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ВНУТРЕННИЕ ФУНКЦИИ =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Автоматически добавить нативную монету
|
||||||
|
*/
|
||||||
|
function _addNativeToken() internal {
|
||||||
|
string memory nativeSymbol;
|
||||||
|
|
||||||
|
// Определяем символ нативной монеты по chain ID
|
||||||
|
if (chainId == 1 || chainId == 11155111) { // Ethereum Mainnet / Sepolia
|
||||||
|
nativeSymbol = "ETH";
|
||||||
|
} else if (chainId == 56 || chainId == 97) { // BSC Mainnet / Testnet
|
||||||
|
nativeSymbol = "BNB";
|
||||||
|
} else if (chainId == 137 || chainId == 80001) { // Polygon Mainnet / Mumbai
|
||||||
|
nativeSymbol = "MATIC";
|
||||||
|
} else if (chainId == 42161) { // Arbitrum One
|
||||||
|
nativeSymbol = "ETH";
|
||||||
|
} else if (chainId == 10) { // Optimism
|
||||||
|
nativeSymbol = "ETH";
|
||||||
|
} else if (chainId == 43114) { // Avalanche
|
||||||
|
nativeSymbol = "AVAX";
|
||||||
|
} else {
|
||||||
|
nativeSymbol = "NATIVE"; // Для неизвестных сетей
|
||||||
|
}
|
||||||
|
|
||||||
|
supportedTokens[address(0)] = TokenInfo({
|
||||||
|
tokenAddress: address(0),
|
||||||
|
symbol: nativeSymbol,
|
||||||
|
decimals: 18,
|
||||||
|
isActive: true,
|
||||||
|
isNative: true,
|
||||||
|
addedTimestamp: block.timestamp,
|
||||||
|
balance: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
tokenList.push(address(0));
|
||||||
|
tokenIndex[address(0)] = 0;
|
||||||
|
totalTokensSupported = 1;
|
||||||
|
|
||||||
|
emit TokenAdded(address(0), nativeSymbol, 18, true, block.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Обновить кэшированный баланс токена
|
||||||
|
*/
|
||||||
|
function _updateTokenBalance(address tokenAddress, uint256 newBalance) internal {
|
||||||
|
uint256 oldBalance = supportedTokens[tokenAddress].balance;
|
||||||
|
supportedTokens[tokenAddress].balance = newBalance;
|
||||||
|
emit BalanceUpdated(tokenAddress, oldBalance, newBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Синхронизировать кэшированный баланс с реальным
|
||||||
|
*/
|
||||||
|
function _refreshTokenBalance(address tokenAddress) internal {
|
||||||
|
uint256 realBalance;
|
||||||
|
|
||||||
|
if (tokenAddress == address(0)) {
|
||||||
|
realBalance = address(this).balance;
|
||||||
|
} else {
|
||||||
|
realBalance = IERC20(tokenAddress).balanceOf(address(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateTokenBalance(tokenAddress, realBalance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();'
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
83
backend/routes/compile.js
Normal 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;
|
||||||
@@ -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
30
backend/routes/ens.js
Normal 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
51
backend/routes/uploads.js
Normal 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;
|
||||||
|
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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==
|
||||||
|
|||||||
@@ -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
291
docs/DLE_DEPLOY_GUIDE.md
Normal 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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user