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

This commit is contained in:
2025-08-08 23:40:13 +03:00
parent badb8b9557
commit a10810df55
19 changed files with 2599 additions and 898 deletions

5
-b
View File

@@ -1,5 +0,0 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 1757248505 sessionId s%3ARIwffwSv4wqUqAqxpOk_ya9pUWlGlu4W.yc4Bi0rNrhM9%2BUzO0IUZezYTJJfB2ybrng1a3P0imjw

View File

@@ -1,4 +1,4 @@
{
"_format": "hh-sol-dbg-1",
"buildInfo": "../../build-info/362ff3981c938c72363f6427a454b84b.json"
"buildInfo": "../../build-info/998bc5520c3173891dd242df2365077b.json"
}

File diff suppressed because one or more lines are too long

View File

@@ -2,8 +2,8 @@
"_format": "hh-sol-cache-2",
"files": {
"/home/alex/Digital_Legal_Entity(DLE)/backend/contracts/DLE.sol": {
"lastModificationDate": 1754485037554,
"contentHash": "f121cb518877db715ab5cd2e3ee5ff3a",
"lastModificationDate": 1754682456067,
"contentHash": "6d46f24614a8d6c838144dcfad200e26",
"sourceName": "contracts/DLE.sol",
"solcConfig": {
"version": "0.8.20",
@@ -32,8 +32,10 @@
},
"imports": [
"@openzeppelin/contracts/token/ERC20/ERC20.sol",
"@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol",
"@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol",
"@openzeppelin/contracts/utils/ReentrancyGuard.sol",
"@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"
"@openzeppelin/contracts/utils/cryptography/ECDSA.sol"
],
"versionPragmas": [
"^0.8.20"
@@ -348,6 +350,864 @@
"artifacts": [
"Hashes"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol": {
"lastModificationDate": 1754306768254,
"contentHash": "b1a8fc63b83ce00408e0c9ed1230b717",
"sourceName": "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"./IERC20Permit.sol",
"../ERC20.sol",
"../../../utils/cryptography/ECDSA.sol",
"../../../utils/cryptography/EIP712.sol",
"../../../utils/Nonces.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"ERC20Permit"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/cryptography/ECDSA.sol": {
"lastModificationDate": 1754306764456,
"contentHash": "81de029d56aa803972be03c5d277cb6c",
"sourceName": "@openzeppelin/contracts/utils/cryptography/ECDSA.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"ECDSA"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol": {
"lastModificationDate": 1754306768254,
"contentHash": "51c2083b160453420aaa0a046c16d5ca",
"sourceName": "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"../ERC20.sol",
"../../../governance/utils/Votes.sol",
"../../../utils/structs/Checkpoints.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"ERC20Votes"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/Nonces.sol": {
"lastModificationDate": 1754306760451,
"contentHash": "c32d108058718efb9061b88e83a83f79",
"sourceName": "@openzeppelin/contracts/utils/Nonces.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"Nonces"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/cryptography/EIP712.sol": {
"lastModificationDate": 1754306764456,
"contentHash": "8dbb261c55f358342798c4d1803d4f8e",
"sourceName": "@openzeppelin/contracts/utils/cryptography/EIP712.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"./MessageHashUtils.sol",
"../ShortStrings.sol",
"../../interfaces/IERC5267.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"EIP712"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol": {
"lastModificationDate": 1754306768254,
"contentHash": "94ec15baf0d5df863f45b8f351937ec7",
"sourceName": "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"IERC20Permit"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/interfaces/IERC5267.sol": {
"lastModificationDate": 1754306760460,
"contentHash": "94364524cb1a39dcbc3d3afff6d8e53e",
"sourceName": "@openzeppelin/contracts/interfaces/IERC5267.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"IERC5267"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/ShortStrings.sol": {
"lastModificationDate": 1754306760451,
"contentHash": "94e7feaf138d08fb736e43ca0be9bf26",
"sourceName": "@openzeppelin/contracts/utils/ShortStrings.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"./StorageSlot.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"ShortStrings"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol": {
"lastModificationDate": 1754306764465,
"contentHash": "86fd93657e4e27ff76c38699e9b9fcef",
"sourceName": "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"../Strings.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"MessageHashUtils"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/StorageSlot.sol": {
"lastModificationDate": 1754306760451,
"contentHash": "e656d64c4ce918f3d13030b91c935134",
"sourceName": "@openzeppelin/contracts/utils/StorageSlot.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"StorageSlot"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/Strings.sol": {
"lastModificationDate": 1754306760451,
"contentHash": "a55fef2557b35bac18a1880d3c2e6740",
"sourceName": "@openzeppelin/contracts/utils/Strings.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"./math/Math.sol",
"./math/SafeCast.sol",
"./math/SignedMath.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"Strings"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/math/Math.sol": {
"lastModificationDate": 1754306764456,
"contentHash": "2b2665ae9bdb1af440658741a77fe213",
"sourceName": "@openzeppelin/contracts/utils/math/Math.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"../Panic.sol",
"./SafeCast.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"Math"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/math/SafeCast.sol": {
"lastModificationDate": 1754306764456,
"contentHash": "2adca1150f58fc6f3d1f0a0f22ee7cca",
"sourceName": "@openzeppelin/contracts/utils/math/SafeCast.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"SafeCast"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/math/SignedMath.sol": {
"lastModificationDate": 1754306764456,
"contentHash": "ae3528afb8bdb0a7dcfba5b115ee8074",
"sourceName": "@openzeppelin/contracts/utils/math/SignedMath.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"./SafeCast.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"SignedMath"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/Panic.sol": {
"lastModificationDate": 1754306760451,
"contentHash": "2133dc13536b4a6a98131e431fac59e1",
"sourceName": "@openzeppelin/contracts/utils/Panic.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"Panic"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/governance/utils/Votes.sol": {
"lastModificationDate": 1754306764456,
"contentHash": "95aceafdc639babdd22576e5e3774d64",
"sourceName": "@openzeppelin/contracts/governance/utils/Votes.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"../../interfaces/IERC5805.sol",
"../../utils/Context.sol",
"../../utils/Nonces.sol",
"../../utils/cryptography/EIP712.sol",
"../../utils/structs/Checkpoints.sol",
"../../utils/math/SafeCast.sol",
"../../utils/cryptography/ECDSA.sol",
"../../utils/types/Time.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"Votes"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/structs/Checkpoints.sol": {
"lastModificationDate": 1754306764456,
"contentHash": "657c6dfea3bae1af948de6113ba01cea",
"sourceName": "@openzeppelin/contracts/utils/structs/Checkpoints.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"../math/Math.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"Checkpoints"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/interfaces/IERC5805.sol": {
"lastModificationDate": 1754306760460,
"contentHash": "65ba9f89b1057e2192e341b286d4e261",
"sourceName": "@openzeppelin/contracts/interfaces/IERC5805.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"../governance/utils/IVotes.sol",
"./IERC6372.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"IERC5805"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/utils/types/Time.sol": {
"lastModificationDate": 1754306764456,
"contentHash": "d83e7814a059fc1287fd765f424ce004",
"sourceName": "@openzeppelin/contracts/utils/types/Time.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [
"../math/Math.sol",
"../math/SafeCast.sol"
],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"Time"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/governance/utils/IVotes.sol": {
"lastModificationDate": 1754306764456,
"contentHash": "905ffceb29869fee4b5a649abe7e2927",
"sourceName": "@openzeppelin/contracts/governance/utils/IVotes.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"IVotes"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/node_modules/@openzeppelin/contracts/interfaces/IERC6372.sol": {
"lastModificationDate": 1754306760460,
"contentHash": "414cd6acf090e4009cf016ff62ecbd88",
"sourceName": "@openzeppelin/contracts/interfaces/IERC6372.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"IERC6372"
]
},
"/home/alex/Digital_Legal_Entity(DLE)/backend/contracts/FactoryDeployer.sol": {
"lastModificationDate": 1754683537642,
"contentHash": "6161bbb21af830bc05d6acd8682d9678",
"sourceName": "contracts/FactoryDeployer.sol",
"solcConfig": {
"version": "0.8.20",
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata"
],
"": [
"ast"
]
}
}
}
},
"imports": [],
"versionPragmas": [
"^0.8.20"
],
"artifacts": [
"FactoryDeployer"
]
}
}
}

View File

@@ -11,14 +11,18 @@
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
/**
* @title DLE (Digital Legal Entity)
* @dev Основной контракт DLE с модульной архитектурой и мульти-чейн поддержкой
* @dev Основной контракт DLE с модульной архитектурой, Single-Chain Governance
* и безопасной мульти-чейн синхронизацией без сторонних мостов (через подписи холдеров).
*/
contract DLE is ERC20, ReentrancyGuard {
contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard {
using ECDSA for bytes32;
struct DLEInfo {
string name;
string symbol;
@@ -53,11 +57,15 @@ contract DLE is ERC20, ReentrancyGuard {
uint256 forVotes;
uint256 againstVotes;
bool executed;
uint256 deadline;
bool canceled;
uint256 deadline; // конец периода голосования (sec)
address initiator;
bytes operation; // Операция для исполнения
bytes operation; // операция для исполнения
uint256 governanceChainId; // сеть голосования (Single-Chain Governance)
uint256[] targetChains; // целевые сети для исполнения
uint256 timelock; // earliest execution timestamp (sec)
uint256 snapshotTimepoint; // блок/временная точка для getPastVotes
mapping(address => bool) hasVoted;
mapping(uint256 => bool) chainVoteSynced; // Синхронизация голосов между цепочками
}
@@ -74,15 +82,11 @@ contract DLE is ERC20, ReentrancyGuard {
// Предложения
mapping(uint256 => Proposal) public proposals;
uint256[] public allProposalIds;
// Мульти-чейн
mapping(uint256 => bool) public supportedChains;
uint256[] public supportedChainIds;
mapping(uint256 => bool) public executedProposals; // Синхронизация исполненных предложений
// Merkle proofs для cross-chain синхронизации
mapping(uint256 => bytes32) public chainMerkleRoots; // chainId => merkleRoot
mapping(uint256 => mapping(uint256 => bool)) public processedProofs; // proposalId => proofHash => processed
// События
event DLEInitialized(
@@ -101,21 +105,29 @@ contract DLE is ERC20, ReentrancyGuard {
event ProposalCreated(uint256 proposalId, address initiator, string description);
event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower);
event ProposalExecuted(uint256 proposalId, bytes operation);
event ProposalCancelled(uint256 proposalId, string reason);
event ProposalTimelockSet(uint256 proposalId, uint256 timelock);
event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains);
event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId);
event ModuleAdded(bytes32 moduleId, address moduleAddress);
event ModuleRemoved(bytes32 moduleId);
event CrossChainExecutionSync(uint256 proposalId, uint256 fromChainId, uint256 toChainId);
event CrossChainVoteSync(uint256 proposalId, uint256 fromChainId, uint256 toChainId);
event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId);
event ChainAdded(uint256 chainId);
event ChainRemoved(uint256 chainId);
event ChainMerkleRootSet(uint256 chainId, bytes32 merkleRoot);
event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, uint256 oktmo, string[] okvedCodes, uint256 kpp);
event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage);
event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId);
// EIP712 typehash для подписи одобрения исполнения предложения в целевой сети
// ExecutionApproval(uint256 proposalId, bytes32 operationHash, uint256 chainId, uint256 snapshotTimepoint)
bytes32 private constant EXECUTION_APPROVAL_TYPEHASH = keccak256(
"ExecutionApproval(uint256 proposalId,bytes32 operationHash,uint256 chainId,uint256 snapshotTimepoint)"
);
constructor(
DLEConfig memory config,
uint256 _currentChainId
) ERC20(config.name, config.symbol) {
) ERC20(config.name, config.symbol) ERC20Permit(config.name) {
dleInfo = DLEInfo({
name: config.name,
symbol: config.symbol,
@@ -143,9 +155,13 @@ contract DLE is ERC20, ReentrancyGuard {
require(config.initialPartners.length > 0, "No initial partners");
for (uint256 i = 0; i < config.initialPartners.length; i++) {
require(config.initialPartners[i] != address(0), "Zero address");
require(config.initialAmounts[i] > 0, "Zero amount");
_mint(config.initialPartners[i], config.initialAmounts[i]);
address partner = config.initialPartners[i];
uint256 amount = config.initialAmounts[i];
require(partner != address(0), "Zero address");
require(amount > 0, "Zero amount");
_mint(partner, amount);
// Авто-делегирование голосов себе, чтобы getPastVotes работал без действия пользователя
_delegate(partner, partner);
}
emit InitialTokensDistributed(config.initialPartners, config.initialAmounts);
@@ -174,12 +190,14 @@ contract DLE is ERC20, ReentrancyGuard {
string memory _description,
uint256 _duration,
bytes memory _operation,
uint256 _governanceChainId
uint256 _governanceChainId,
uint256[] memory _targetChains,
uint256 _timelockDelay
) external returns (uint256) {
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
require(_duration > 0, "Duration must be positive");
require(supportedChains[_governanceChainId], "Chain not supported");
require(checkChainConnection(_governanceChainId), "Chain not available");
require(_timelockDelay <= 365 days, "Timelock too big");
uint256 proposalId = proposalCounter++;
Proposal storage proposal = proposals[proposalId];
@@ -192,8 +210,22 @@ contract DLE is ERC20, ReentrancyGuard {
proposal.deadline = block.timestamp + _duration;
proposal.initiator = msg.sender;
proposal.operation = _operation;
proposal.governanceChainId = _governanceChainId;
proposal.timelock = block.timestamp + _timelockDelay;
// Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке
uint256 nowClock = clock();
proposal.snapshotTimepoint = nowClock == 0 ? 0 : nowClock - 1;
// запись целевых сетей
for (uint256 i = 0; i < _targetChains.length; i++) {
require(supportedChains[_targetChains[i]], "Target chain not supported");
proposal.targetChains.push(_targetChains[i]);
}
allProposalIds.push(proposalId);
emit ProposalCreated(proposalId, msg.sender, _description);
emit ProposalGovernanceChainSet(proposalId, _governanceChainId);
emit ProposalTargetsSet(proposalId, _targetChains);
emit ProposalTimelockSet(proposalId, proposal.timelock);
return proposalId;
}
@@ -208,9 +240,10 @@ contract DLE is ERC20, ReentrancyGuard {
require(block.timestamp < proposal.deadline, "Voting ended");
require(!proposal.executed, "Proposal already executed");
require(!proposal.hasVoted[msg.sender], "Already voted");
require(balanceOf(msg.sender) > 0, "No tokens to vote");
require(currentChainId == proposal.governanceChainId, "Wrong chain for voting");
uint256 votingPower = balanceOf(msg.sender);
// используем снапшот голосов для защиты от перелива
uint256 votingPower = getPastVotes(msg.sender, proposal.snapshotTimepoint);
proposal.hasVoted[msg.sender] = true;
if (_support) {
@@ -222,53 +255,7 @@ contract DLE is ERC20, ReentrancyGuard {
emit ProposalVoted(_proposalId, msg.sender, _support, votingPower);
}
/**
* @dev Синхронизировать голос из другой цепочки
* @param _proposalId ID предложения
* @param _fromChainId ID цепочки откуда синхронизируем
* @param _forVotes Голоса за
* @param _againstVotes Голоса против
*/
function syncVoteFromChain(
uint256 _proposalId,
uint256 _fromChainId,
uint256 _forVotes,
uint256 _againstVotes,
bytes memory _proof
) external {
Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist");
require(supportedChains[_fromChainId], "Chain not supported");
require(!proposal.chainVoteSynced[_fromChainId], "Already synced");
// Проверяем доказательство cross-chain синхронизации
require(_proof.length > 0, "Proof required for cross-chain sync");
// Проверяем Merkle proof для cross-chain синхронизации
bytes32 proofHash = keccak256(abi.encodePacked(_proposalId, _fromChainId, _forVotes, _againstVotes));
require(!processedProofs[_proposalId][uint256(proofHash)], "Proof already processed");
// Проверяем, что Merkle root для цепочки установлен
bytes32 merkleRoot = chainMerkleRoots[_fromChainId];
require(merkleRoot != bytes32(0), "Merkle root not set for chain");
// Проверяем Merkle proof
bytes32[] memory proof = abi.decode(_proof, (bytes32[]));
require(MerkleProof.verify(proof, merkleRoot, proofHash), "Invalid Merkle proof");
// Отмечаем proof как обработанный
processedProofs[_proposalId][uint256(proofHash)] = true;
// Проверяем, что голоса не превышают общее количество токенов
uint256 totalVotes = _forVotes + _againstVotes;
require(totalVotes <= totalSupply(), "Votes exceed total supply");
proposal.forVotes += _forVotes;
proposal.againstVotes += _againstVotes;
proposal.chainVoteSynced[_fromChainId] = true;
emit CrossChainVoteSync(_proposalId, _fromChainId, currentChainId);
}
// УДАЛЕНО: syncVoteFromChain с MerkleProof — небезопасно без доверенного моста
/**
* @dev Проверить результат предложения
@@ -281,7 +268,9 @@ contract DLE is ERC20, ReentrancyGuard {
require(proposal.id == _proposalId, "Proposal does not exist");
uint256 totalVotes = proposal.forVotes + proposal.againstVotes;
uint256 quorumRequired = (totalSupply() * quorumPercentage) / 100;
// Используем снапшот totalSupply на момент начала голосования
uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint);
uint256 quorumRequired = (pastSupply * quorumPercentage) / 100;
quorumReached = totalVotes >= quorumRequired;
passed = quorumReached && proposal.forVotes > proposal.againstVotes;
@@ -297,6 +286,7 @@ contract DLE is ERC20, ReentrancyGuard {
Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist");
require(!proposal.executed, "Proposal already executed");
require(currentChainId == proposal.governanceChainId, "Execute only in governance chain");
(bool passed, bool quorumReached) = checkProposalResult(_proposalId);
@@ -307,6 +297,7 @@ contract DLE is ERC20, ReentrancyGuard {
"Voting not ended and quorum not reached"
);
require(passed && quorumReached, "Proposal not passed");
require(block.timestamp >= proposal.timelock, "Timelock not expired");
proposal.executed = true;
@@ -317,49 +308,74 @@ contract DLE is ERC20, ReentrancyGuard {
}
/**
* @dev Синхронизировать исполнение из другой цепочки
* @param _proposalId ID предложения
* @param _fromChainId ID цепочки откуда синхронизируем
* @dev Отмена предложения до истечения голосования инициатором при наличии достаточной голосующей силы.
* Это soft-cancel для защиты от явных ошибок. Порог: >= 10% от снапшотного supply.
*/
function syncExecutionFromChain(
uint256 _proposalId,
uint256 _fromChainId,
bytes memory _proof
) external {
require(supportedChains[_fromChainId], "Chain not supported");
require(!executedProposals[_proposalId], "Already executed");
// Проверяем доказательство исполнения из другой цепочки
require(_proof.length > 0, "Proof required for cross-chain execution");
// Проверяем Merkle proof для cross-chain исполнения
bytes32 proofHash = keccak256(abi.encodePacked(_proposalId, _fromChainId, "EXECUTION"));
require(!processedProofs[_proposalId][uint256(proofHash)], "Proof already processed");
// Проверяем, что Merkle root для цепочки установлен
bytes32 merkleRoot = chainMerkleRoots[_fromChainId];
require(merkleRoot != bytes32(0), "Merkle root not set for chain");
// Проверяем Merkle proof
bytes32[] memory proof = abi.decode(_proof, (bytes32[]));
require(MerkleProof.verify(proof, merkleRoot, proofHash), "Invalid Merkle proof");
// Отмечаем proof как обработанный
processedProofs[_proposalId][uint256(proofHash)] = true;
// Проверяем, что предложение существует и не было исполнено
function cancelProposal(uint256 _proposalId, string calldata reason) external {
Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist");
require(!proposal.executed, "Proposal already executed");
require(!proposal.executed, "Already executed");
require(block.timestamp < proposal.deadline, "Voting ended");
require(msg.sender == proposal.initiator, "Only initiator");
uint256 vp = getPastVotes(msg.sender, proposal.snapshotTimepoint);
uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint);
require(vp * 10 >= pastSupply, "Insufficient voting power to cancel");
executedProposals[_proposalId] = true;
// Исполняем операцию из предложения
if (proposal.id == _proposalId) {
_executeOperation(proposal.operation);
proposal.canceled = true;
emit ProposalCancelled(_proposalId, reason);
}
emit CrossChainExecutionSync(_proposalId, _fromChainId, currentChainId);
// УДАЛЕНО: syncExecutionFromChain с MerkleProof — небезопасно без доверенного моста
/**
* @dev Исполнение предложения в НЕ governance-сети по подписям холдеров на снапшоте.
* Подходит для target chains. Не требует внешнего моста.
*/
function executeProposalBySignatures(
uint256 _proposalId,
address[] calldata signers,
bytes[] calldata signatures
) external nonReentrant {
Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist");
require(!proposal.executed, "Proposal already executed in this chain");
require(currentChainId != proposal.governanceChainId, "Use executeProposal in governance chain");
require(_isTargetChain(proposal, currentChainId), "Chain not in targets");
require(block.timestamp >= proposal.timelock, "Timelock not expired");
require(signers.length == signatures.length, "Bad signatures");
bytes32 opHash = keccak256(proposal.operation);
bytes32 structHash = keccak256(abi.encode(
EXECUTION_APPROVAL_TYPEHASH,
_proposalId,
opHash,
currentChainId,
proposal.snapshotTimepoint
));
bytes32 digest = _hashTypedDataV4(structHash);
uint256 votesFor = 0;
// простая защита от дублей адресов (O(n^2) по малому n)
for (uint256 i = 0; i < signers.length; i++) {
address recovered = ECDSA.recover(digest, signatures[i]);
require(recovered == signers[i], "Bad signature");
// проверка на дубли
for (uint256 j = 0; j < i; j++) {
require(signers[j] != recovered, "Duplicate signer");
}
uint256 vp = getPastVotes(recovered, proposal.snapshotTimepoint);
require(vp > 0, "No voting power at snapshot");
votesFor += vp;
}
uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint);
uint256 quorumRequired = (pastSupply * quorumPercentage) / 100;
require(votesFor >= quorumRequired, "Quorum not reached by sigs");
proposal.executed = true;
_executeOperation(proposal.operation);
emit ProposalExecuted(_proposalId, proposal.operation);
emit ProposalExecutionApprovedInChain(_proposalId, currentChainId);
}
/**
@@ -368,19 +384,8 @@ contract DLE is ERC20, ReentrancyGuard {
* @return isAvailable Доступна ли цепочка
*/
function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable) {
// Проверяем, поддерживается ли цепочка
if (!supportedChains[_chainId]) {
return false;
}
// Проверяем, что Merkle root установлен для цепочки
// Это означает, что цепочка активна и готова к синхронизации
bytes32 merkleRoot = chainMerkleRoots[_chainId];
if (merkleRoot == bytes32(0)) {
return false;
}
return true;
// Упрощенная проверка: цепочка объявлена как поддерживаемая
return supportedChains[_chainId];
}
/**
@@ -410,12 +415,8 @@ contract DLE is ERC20, ReentrancyGuard {
function syncToAllChains(uint256 _proposalId) external {
require(checkSyncReadiness(_proposalId), "Not all chains ready");
// Выполняем синхронизацию во все цепочки
for (uint256 i = 0; i < getSupportedChainCount(); i++) {
uint256 chainId = getSupportedChainId(i);
syncToChain(_proposalId, chainId);
}
// В этой версии без внешнего моста синхронизация выполняется
// через executeProposalBySignatures в целевых сетях.
emit SyncCompleted(_proposalId);
}
@@ -424,26 +425,7 @@ contract DLE is ERC20, ReentrancyGuard {
* @param _proposalId ID предложения
* @param _chainId ID цепочки
*/
function syncToChain(uint256 _proposalId, uint256 _chainId) internal {
// Проверяем, что цепочка поддерживается
require(supportedChains[_chainId], "Chain not supported");
// Получаем информацию о предложении
Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist");
// Проверяем, что цепочка готова к синхронизации
require(checkChainConnection(_chainId), "Chain not ready for sync");
// Создаем Merkle root для синхронизации
bytes32 syncData = keccak256(abi.encodePacked(_proposalId, currentChainId, proposal.operation));
// Обновляем Merkle root для целевой цепочки
chainMerkleRoots[_chainId] = syncData;
// Эмитим событие для cross-chain bridge
emit CrossChainExecutionSync(_proposalId, currentChainId, _chainId);
}
// УДАЛЕНО: syncToChain — не используется в подпись‑ориентированной схеме
/**
* @dev Получить количество поддерживаемых цепочек
@@ -465,14 +447,12 @@ contract DLE is ERC20, ReentrancyGuard {
* @dev Добавить поддерживаемую цепочку (только для владельцев токенов)
* @param _chainId ID цепочки
*/
function addSupportedChain(uint256 _chainId) external {
require(balanceOf(msg.sender) > 0, "Must hold tokens to add chain");
// Управление списком сетей теперь выполняется только через предложения
function _addSupportedChain(uint256 _chainId) internal {
require(!supportedChains[_chainId], "Chain already supported");
require(_chainId != currentChainId, "Cannot add current chain");
supportedChains[_chainId] = true;
supportedChainIds.push(_chainId);
emit ChainAdded(_chainId);
}
@@ -480,13 +460,10 @@ contract DLE is ERC20, ReentrancyGuard {
* @dev Удалить поддерживаемую цепочку (только для владельцев токенов)
* @param _chainId ID цепочки
*/
function removeSupportedChain(uint256 _chainId) external {
require(balanceOf(msg.sender) > 0, "Must hold tokens to remove chain");
function _removeSupportedChain(uint256 _chainId) internal {
require(supportedChains[_chainId], "Chain not supported");
require(_chainId != currentChainId, "Cannot remove current chain");
supportedChains[_chainId] = false;
// Удаляем из массива
for (uint256 i = 0; i < supportedChainIds.length; i++) {
if (supportedChainIds[i] == _chainId) {
@@ -495,10 +472,6 @@ contract DLE is ERC20, ReentrancyGuard {
break;
}
}
// Очищаем Merkle root для цепочки
delete chainMerkleRoots[_chainId];
emit ChainRemoved(_chainId);
}
@@ -507,22 +480,13 @@ contract DLE is ERC20, ReentrancyGuard {
* @param _chainId ID цепочки
* @param _merkleRoot Merkle root для цепочки
*/
function setChainMerkleRoot(uint256 _chainId, bytes32 _merkleRoot) external {
require(balanceOf(msg.sender) > 0, "Must hold tokens to set merkle root");
require(supportedChains[_chainId], "Chain not supported");
chainMerkleRoots[_chainId] = _merkleRoot;
emit ChainMerkleRootSet(_chainId, _merkleRoot);
}
// УДАЛЕНО: setChainMerkleRoot — небезопасно отдавать любому холдеру
/**
* @dev Получить Merkle root для цепочки
* @param _chainId ID цепочки
*/
function getChainMerkleRoot(uint256 _chainId) external view returns (bytes32) {
return chainMerkleRoots[_chainId];
}
// УДАЛЕНО: getChainMerkleRoot — устарело
/**
* @dev Исполнить операцию
@@ -532,19 +496,7 @@ contract DLE is ERC20, ReentrancyGuard {
// Декодируем операцию
(bytes4 selector, bytes memory data) = abi.decode(_operation, (bytes4, bytes));
if (selector == bytes4(keccak256("transfer(address,uint256)"))) {
// Операция передачи токенов
(address to, uint256 amount) = abi.decode(data, (address, uint256));
_transfer(msg.sender, to, amount);
} else if (selector == bytes4(keccak256("mint(address,uint256)"))) {
// Операция минтинга токенов
(address to, uint256 amount) = abi.decode(data, (address, uint256));
_mint(to, amount);
} else if (selector == bytes4(keccak256("burn(address,uint256)"))) {
// Операция сжигания токенов
(address from, uint256 amount) = abi.decode(data, (address, uint256));
_burn(from, amount);
} else if (selector == bytes4(keccak256("updateDLEInfo(string,string,string,string,uint256,uint256,string[],uint256)"))) {
if (selector == bytes4(keccak256("updateDLEInfo(string,string,string,string,uint256,uint256,string[],uint256)"))) {
// Операция обновления информации DLE
(string memory name, string memory symbol, string memory location, string memory coordinates,
uint256 jurisdiction, uint256 oktmo, string[] memory okvedCodes, uint256 kpp) = abi.decode(data, (string, string, string, string, uint256, uint256, string[], uint256));
@@ -565,6 +517,16 @@ contract DLE is ERC20, ReentrancyGuard {
// Операция удаления модуля
(bytes32 moduleId) = abi.decode(data, (bytes32));
_removeModule(moduleId);
} else if (selector == bytes4(keccak256("_addSupportedChain(uint256)"))) {
(uint256 chainIdToAdd) = abi.decode(data, (uint256));
_addSupportedChain(chainIdToAdd);
} else if (selector == bytes4(keccak256("_removeSupportedChain(uint256)"))) {
(uint256 chainIdToRemove) = abi.decode(data, (uint256));
_removeSupportedChain(chainIdToRemove);
} else if (selector == bytes4(keccak256("offchainAction(bytes32,string,bytes32)"))) {
// Оффчейн операция для приложения: идентификатор, тип, хеш полезной нагрузки
// (bytes32 actionId, string memory kind, bytes32 payloadHash) = abi.decode(data, (bytes32, string, bytes32));
// Ончейн-побочных эффектов нет. Факт решения фиксируется событием ProposalExecuted.
} else {
// Неизвестная операция
revert("Unknown operation");
@@ -654,7 +616,6 @@ contract DLE is ERC20, ReentrancyGuard {
uint256 _chainId
) external returns (uint256) {
require(supportedChains[_chainId], "Chain not supported");
require(checkChainConnection(_chainId), "Chain not available");
require(_moduleAddress != address(0), "Zero address");
require(!activeModules[_moduleId], "Module already exists");
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
@@ -693,7 +654,6 @@ contract DLE is ERC20, ReentrancyGuard {
uint256 _chainId
) external returns (uint256) {
require(supportedChains[_chainId], "Chain not supported");
require(checkChainConnection(_chainId), "Chain not available");
require(activeModules[_moduleId], "Module does not exist");
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
@@ -782,192 +742,146 @@ contract DLE is ERC20, ReentrancyGuard {
return currentChainId;
}
// События для новых функций
event SyncCompleted(uint256 proposalId);
event DLEDeactivated(address indexed deactivatedBy, uint256 timestamp);
event DeactivationProposalCreated(uint256 proposalId, address indexed initiator, string description);
event DeactivationProposalVoted(uint256 proposalId, address indexed voter, bool support, uint256 votingPower);
event DeactivationProposalExecuted(uint256 proposalId, address indexed executedBy);
// Структура для предложения деактивации
struct DeactivationProposal {
uint256 id;
string description;
uint256 forVotes;
uint256 againstVotes;
bool executed;
uint256 deadline;
address initiator;
uint256 chainId;
mapping(address => bool) hasVoted;
}
// Предложения деактивации
mapping(uint256 => DeactivationProposal) public deactivationProposals;
uint256 public deactivationProposalCounter;
bool public isDeactivated;
/**
* @dev Создать предложение о деактивации DLE
* @param _description Описание предложения
* @param _duration Длительность голосования в секундах
* @param _chainId ID цепочки для деактивации
*/
function createDeactivationProposal(
string memory _description,
uint256 _duration,
uint256 _chainId
) external returns (uint256) {
require(!isDeactivated, "DLE already deactivated");
require(balanceOf(msg.sender) > 0, "Must hold tokens to create deactivation proposal");
require(_duration > 0, "Duration must be positive");
require(supportedChains[_chainId], "Chain not supported");
uint256 proposalId = deactivationProposalCounter++;
DeactivationProposal storage proposal = deactivationProposals[proposalId];
proposal.id = proposalId;
proposal.description = _description;
proposal.forVotes = 0;
proposal.againstVotes = 0;
proposal.executed = false;
proposal.deadline = block.timestamp + _duration;
proposal.initiator = msg.sender;
proposal.chainId = _chainId;
emit DeactivationProposalCreated(proposalId, msg.sender, _description);
return proposalId;
}
/**
* @dev Голосовать за предложение деактивации
* @param _proposalId ID предложения
* @param _support Поддержка предложения
*/
function voteDeactivation(uint256 _proposalId, bool _support) external nonReentrant {
DeactivationProposal storage proposal = deactivationProposals[_proposalId];
require(proposal.id == _proposalId, "Deactivation proposal does not exist");
require(block.timestamp < proposal.deadline, "Voting ended");
require(!proposal.executed, "Proposal already executed");
require(!proposal.hasVoted[msg.sender], "Already voted");
require(balanceOf(msg.sender) > 0, "No tokens to vote");
uint256 votingPower = balanceOf(msg.sender);
if (_support) {
proposal.forVotes += votingPower;
} else {
proposal.againstVotes += votingPower;
}
proposal.hasVoted[msg.sender] = true;
emit DeactivationProposalVoted(_proposalId, msg.sender, _support, votingPower);
}
/**
* @dev Проверить результат предложения деактивации
* @param _proposalId ID предложения
*/
function checkDeactivationProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) {
DeactivationProposal storage proposal = deactivationProposals[_proposalId];
require(proposal.id == _proposalId, "Deactivation proposal does not exist");
uint256 totalVotes = proposal.forVotes + proposal.againstVotes;
uint256 totalSupply = totalSupply();
quorumReached = totalVotes >= (totalSupply * quorumPercentage) / 100;
passed = quorumReached && proposal.forVotes > proposal.againstVotes;
return (passed, quorumReached);
}
/**
* @dev Исполнить предложение деактивации
* @param _proposalId ID предложения
*/
function executeDeactivationProposal(uint256 _proposalId) external {
DeactivationProposal storage proposal = deactivationProposals[_proposalId];
require(proposal.id == _proposalId, "Deactivation proposal does not exist");
require(!proposal.executed, "Proposal already executed");
require(block.timestamp >= proposal.deadline, "Voting not ended");
(bool passed, bool quorumReached) = checkDeactivationProposalResult(_proposalId);
require(quorumReached, "Quorum not reached");
require(passed, "Proposal not passed");
proposal.executed = true;
isDeactivated = true;
dleInfo.isActive = false;
emit DeactivationProposalExecuted(_proposalId, msg.sender);
emit DLEDeactivated(msg.sender, block.timestamp);
}
/**
* @dev Деактивировать DLE напрямую (только при достижении кворума)
* Может быть вызвана только если есть активное предложение деактивации с достигнутым кворумом
*/
function deactivate() external {
require(!isDeactivated, "DLE already deactivated");
require(balanceOf(msg.sender) > 0, "Must hold tokens to deactivate DLE");
// Проверяем, есть ли активное предложение деактивации с достигнутым кворумом
bool hasValidDeactivationProposal = false;
for (uint256 i = 0; i < deactivationProposalCounter; i++) {
DeactivationProposal storage proposal = deactivationProposals[i];
if (!proposal.executed && block.timestamp >= proposal.deadline) {
(bool passed, bool quorumReached) = checkDeactivationProposalResult(i);
if (quorumReached && passed) {
hasValidDeactivationProposal = true;
proposal.executed = true;
break;
}
}
}
require(hasValidDeactivationProposal, "No valid deactivation proposal with quorum");
isDeactivated = true;
dleInfo.isActive = false;
emit DLEDeactivated(msg.sender, block.timestamp);
}
/**
* @dev Проверить, деактивирован ли DLE
*/
function isActive() external view returns (bool) {
return !isDeactivated && dleInfo.isActive;
}
/**
* @dev Получить информацию о предложении деактивации
* @param _proposalId ID предложения
*/
function getDeactivationProposal(uint256 _proposalId) external view returns (
// ===== Интерфейс аналитики для API =====
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 chainId
uint256 governanceChainId,
uint256 timelock,
uint256 snapshotTimepoint,
uint256[] memory targets
) {
DeactivationProposal storage proposal = deactivationProposals[_proposalId];
require(proposal.id == _proposalId, "Deactivation proposal does not exist");
Proposal storage p = proposals[_proposalId];
require(p.id == _proposalId, "Proposal does not exist");
return (
proposal.id,
proposal.description,
proposal.forVotes,
proposal.againstVotes,
proposal.executed,
proposal.deadline,
proposal.initiator,
proposal.chainId
p.id,
p.description,
p.forVotes,
p.againstVotes,
p.executed,
p.canceled,
p.deadline,
p.initiator,
p.governanceChainId,
p.timelock,
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
function getProposalState(uint256 _proposalId) public view returns (uint8 state) {
Proposal storage p = proposals[_proposalId];
require(p.id == _proposalId, "Proposal does not exist");
if (p.canceled) return 4;
if (p.executed) return 3;
(bool passed, bool quorumReached) = checkProposalResult(_proposalId);
bool votingOver = block.timestamp >= p.deadline;
bool ready = passed && quorumReached && block.timestamp >= p.timelock;
if (ready) return 5; // ReadyForExecution
if (passed && (votingOver || quorumReached)) return 1; // Succeeded
if (votingOver && !passed) return 2; // Defeated
return 0; // Pending
}
function getQuorumAt(uint256 timepoint) external view returns (uint256) {
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.
function isActive() external view returns (bool) {
return !isDeactivated && dleInfo.isActive;
}
// ===== Вспомогательные функции =====
function _isTargetChain(Proposal storage p, uint256 chainId) internal view returns (bool) {
for (uint256 i = 0; i < p.targetChains.length; i++) {
if (p.targetChains[i] == chainId) return true;
}
return false;
}
// ===== Overrides для ERC20Votes =====
function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Votes)
{
super._update(from, to, value);
}
// Разрешаем неоднозначность nonces из базовых классов
function nonces(address owner)
public
view
override(ERC20Permit, Nonces)
returns (uint256)
{
return super.nonces(owner);
}
// Запрет делегирования на третьих лиц: разрешено только делегировать самому себе
function _delegate(address delegator, address delegatee) internal override {
require(delegator == delegatee, "Delegation disabled");
super._delegate(delegator, delegatee);
}
}

View File

@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract FactoryDeployer {
event Deployed(address addr, bytes32 salt);
function deploy(bytes32 salt, bytes memory creationCode) external payable returns (address addr) {
require(creationCode.length != 0, "init code empty");
// solhint-disable-next-line no-inline-assembly
assembly {
addr := create2(callvalue(), add(creationCode, 0x20), mload(creationCode), salt)
}
require(addr != address(0), "CREATE2 failed");
emit Deployed(addr, salt);
}
function computeAddress(bytes32 salt, bytes32 initCodeHash) external view returns (address) {
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash));
return address(uint160(uint256(hash)));
}
}

View File

@@ -18,6 +18,7 @@ const auth = require('../middleware/auth');
const path = require('path');
const fs = require('fs');
const ethers = require('ethers'); // Added ethers for private key validation
const create2 = require('../utils/create2');
/**
* @route POST /api/dle-v2
@@ -299,3 +300,33 @@ router.post('/validate-private-key', async (req, res, next) => {
});
module.exports = router;
/**
* Дополнительные маршруты (подключаются из app.js)
*/
// Предсказание адресов по выбранным сетям с использованием CREATE2
router.post('/predict-addresses', auth.requireAuth, auth.requireAdmin, async (req, res) => {
try {
const { name, symbol, selectedNetworks } = req.body || {};
if (!selectedNetworks || !Array.isArray(selectedNetworks) || selectedNetworks.length === 0) {
return res.status(400).json({ success: false, message: 'Не переданы сети' });
}
// Используем служебные секреты для фабрики и SALT
// Ожидаем, что на сервере настроены переменные окружения или конфиги на сеть
const result = {};
for (const chainId of selectedNetworks) {
const factory = process.env[`FACTORY_ADDRESS_${chainId}`] || process.env.FACTORY_ADDRESS;
const saltHex = process.env[`CREATE2_SALT_${chainId}`] || process.env.CREATE2_SALT;
const initCodeHash = process.env[`INIT_CODE_HASH_${chainId}`] || process.env.INIT_CODE_HASH;
if (!factory || !saltHex || !initCodeHash) continue;
result[chainId] = create2.computeCreate2Address(factory, saltHex, initCodeHash);
}
return res.json({ success: true, data: result });
} catch (e) {
logger.error('predict-addresses error', e);
return res.status(500).json({ success: false, message: 'Ошибка расчета адресов' });
}
});

View File

@@ -1,3 +1,71 @@
/* 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,
oktmo: params.oktmo,
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.

View File

@@ -0,0 +1,69 @@
/* eslint-disable no-console */
const hre = require('hardhat');
const path = require('path');
async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, factoryAddress, dleInit) {
const { ethers } = hre;
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(pk, provider);
// Ensure factory
let faddr = factoryAddress;
const code = faddr ? await provider.getCode(faddr) : '0x';
if (!faddr || code === '0x') {
const Factory = await hre.ethers.getContractFactory('FactoryDeployer', wallet);
const factory = await Factory.deploy();
await factory.waitForDeployment();
faddr = await factory.getAddress();
}
const Factory = await hre.ethers.getContractAt('FactoryDeployer', faddr, 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));
return { factory: faddr, address: addr };
}
async function main() {
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 || !salt || !initCodeHash || networks.length === 0) throw new Error('Env: PRIVATE_KEY, CREATE2_SALT, INIT_CODE_HASH, MULTICHAIN_RPC_URLS');
// Prepare init code once
const paramsPath = path.join(__dirname, './current-params.json');
const params = require(paramsPath);
const DLE = await hre.ethers.getContractFactory('DLE');
const dleConfig = {
name: params.name,
symbol: params.symbol,
location: params.location,
coordinates: params.coordinates,
jurisdiction: params.jurisdiction,
oktmo: params.oktmo,
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;
const results = [];
for (let i = 0; i < networks.length; i++) {
const rpcUrl = networks[i];
const factory = factories[i] || process.env.FACTORY_ADDRESS || null;
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, factory, dleInit);
results.push({ rpcUrl, ...r });
}
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(results));
}
main().catch((e) => { console.error(e); process.exit(1); });

View File

@@ -49,59 +49,48 @@ class DLEV2Service {
fs.copyFileSync(paramsFile, tempParamsFile);
logger.info(`Файл параметров скопирован успешно`);
// Определяем сеть для деплоя (берем первую из выбранных сетей)
const chainId = deployParams.supportedChainIds && deployParams.supportedChainIds.length > 0
? deployParams.supportedChainIds[0]
: 1; // По умолчанию Ethereum
// Получаем rpc_url из базы по chain_id
logger.info(`Поиск RPC URL для chain_id: ${chainId}`);
const rpcUrl = await getRpcUrlByChainId(chainId);
if (!rpcUrl) {
logger.error(`RPC URL для сети с chain_id ${chainId} не найден в базе данных`);
throw new Error(`RPC URL для сети с chain_id ${chainId} не найден в базе данных`);
// Готовим RPC для всех выбранных сетей
const rpcUrls = [];
for (const cid of deployParams.supportedChainIds) {
logger.info(`Поиск RPC URL для chain_id: ${cid}`);
const ru = await getRpcUrlByChainId(cid);
if (!ru) {
throw new Error(`RPC URL для сети с chain_id ${cid} не найден в базе данных`);
}
rpcUrls.push(ru);
}
logger.info(`Найден RPC URL для chain_id ${chainId}: ${rpcUrl}`);
// Проверяем баланс кошелька
// Лёгкая проверка баланса в первой сети
{
const { ethers } = require('ethers');
const provider = new ethers.JsonRpcProvider(rpcUrl);
const provider = new ethers.JsonRpcProvider(rpcUrls[0]);
const walletAddress = dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey, provider).address : null;
if (walletAddress) {
const balance = await provider.getBalance(walletAddress);
const minBalance = ethers.parseEther("0.00001"); // Временно уменьшено для тестирования
const minBalance = ethers.parseEther("0.00001");
logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`);
if (balance < minBalance) {
logger.warn(`Недостаточно ETH для деплоя. Баланс: ${ethers.formatEther(balance)} ETH, требуется минимум: ${ethers.formatEther(minBalance)} ETH`);
throw new Error(`Недостаточно ETH для деплоя. Баланс: ${ethers.formatEther(balance)} ETH, требуется минимум: ${ethers.formatEther(minBalance)} ETH. Пополните кошелек через Sepolia faucet: https://sepoliafaucet.com/`);
throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`);
}
}
}
if (!dleParams.privateKey) {
throw new Error('Приватный ключ для деплоя не передан');
}
// Маппинг chain_id к именам сетей в Hardhat
const chainIdToNetworkName = {
1: 'ethereum',
137: 'polygon',
56: 'bsc',
42161: 'arbitrum',
11155111: 'sepolia'
};
// Рассчитываем INIT_CODE_HASH автоматически из актуального initCode
const initCodeHash = await this.computeInitCodeHash(deployParams);
const networkName = chainIdToNetworkName[chainId];
if (!networkName) {
throw new Error(`Сеть с chain_id ${chainId} не поддерживается для деплоя`);
}
// Собираем адреса фабрик по сетям (если есть)
const factoryAddresses = deployParams.supportedChainIds.map(cid => process.env[`FACTORY_ADDRESS_${cid}`] || '').join(',');
// Запускаем скрипт деплоя с нужными переменными окружения
const result = await this.runDeployScript(paramsFile, {
rpcUrl,
// Мультисетевой деплой одним вызовом
const result = await this.runDeployMultichain(paramsFile, {
rpcUrls: rpcUrls.join(','),
privateKey: dleParams.privateKey,
networkId: networkName,
envNetworkKey: chainId.toString().toUpperCase()
salt: process.env.CREATE2_SALT,
initCodeHash,
factories: factoryAddresses
});
// Очищаем временные файлы
@@ -237,14 +226,12 @@ class DLEV2Service {
return;
}
// Формируем переменные окружения для скрипта деплоя
const envVars = {
...process.env,
RPC_URL: extraEnv.rpcUrl,
PRIVATE_KEY: extraEnv.privateKey
};
// Запускаем скрипт без указания сети, передаем RPC URL и приватный ключ через переменные окружения
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath], {
cwd: path.join(__dirname, '..'),
env: envVars,
@@ -266,7 +253,6 @@ class DLEV2Service {
hardhatProcess.on('close', (code) => {
try {
// Пытаемся извлечь результат из stdout независимо от кода завершения
const result = this.extractDeployResult(stdout);
resolve(result);
} catch (error) {
@@ -286,6 +272,40 @@ class DLEV2Service {
});
}
// Мультисетевой деплой
runDeployMultichain(paramsFile, opts = {}) {
return new Promise((resolve, reject) => {
const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js');
if (!fs.existsSync(scriptPath)) return reject(new Error('Скрипт мультисетевого деплоя не найден'));
const envVars = {
...process.env,
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' });
let stdout = '', stderr = '';
p.stdout.on('data', (d) => { stdout += d.toString(); logger.info(`[MULTI] ${d.toString().trim()}`); });
p.stderr.on('data', (d) => { stderr += d.toString(); logger.error(`[MULTI_ERR] ${d.toString().trim()}`); });
p.on('close', () => {
try {
const m = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s*(\[.*\])/s);
if (!m) throw new Error('Результат не найден');
const arr = JSON.parse(m[1]);
const addr = arr[0].address;
const allSame = arr.every(x => x.address.toLowerCase() === addr.toLowerCase());
if (!allSame) throw new Error('Адреса отличаются между сетями');
resolve({ success: true, data: { dleAddress: addr, networks: arr } });
} catch (e) {
reject(new Error(`Ошибка мультисетевого деплоя: ${e.message}\nSTDOUT:${stdout}\nSTDERR:${stderr}`));
}
});
p.on('error', (e) => reject(e));
});
}
/**
* Извлекает результат деплоя из stdout
* @param {string} stdout - Вывод скрипта
@@ -358,6 +378,30 @@ class DLEV2Service {
return [];
}
}
// Авто-расчёт INIT_CODE_HASH
async computeInitCodeHash(params) {
const hre = require('hardhat');
const { ethers } = hre;
const DLE = await hre.ethers.getContractFactory('DLE');
const dleConfig = {
name: params.name,
symbol: params.symbol,
location: params.location,
coordinates: params.coordinates,
jurisdiction: params.jurisdiction,
oktmo: params.oktmo,
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 initCode = deployTx.data;
return ethers.keccak256(initCode);
}
}
module.exports = new DLEV2Service();

View File

@@ -416,8 +416,9 @@ async function getBot() {
if (recentMessages && recentMessages.length > 0) {
// Преобразуем сообщения в формат для AI
history = recentMessages.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',
content: msg.content || '' // content уже расшифрован encryptedDb
// Любые человеческие роли трактуем как 'user', только ответы ассистента — 'assistant'
role: msg.sender_type === 'assistant' ? 'assistant' : 'user',
content: msg.content || ''
}));
}
} catch (historyError) {
@@ -465,7 +466,7 @@ async function getBot() {
sender_type: 'assistant',
content: aiResponse,
channel: 'telegram',
role: role,
role: 'assistant',
direction: 'out',
created_at: new Date()
});

21
backend/utils/create2.js Normal file
View File

@@ -0,0 +1,21 @@
const { keccak256, getAddress } = require('ethers').utils || require('ethers');
function toBytes(hex) {
if (hex.startsWith('0x')) return Buffer.from(hex.slice(2), 'hex');
return Buffer.from(hex, 'hex');
}
function computeCreate2Address(factory, saltHex, initCodeHash) {
const parts = [
'0xff',
factory.toLowerCase(),
saltHex.toLowerCase(),
initCodeHash.toLowerCase()
].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');
return '0x' + hash.slice(-40);
}
module.exports = { computeCreate2Address };

View File

@@ -10,6 +10,18 @@
GitHub: https://github.com/HB3-ACCELERATOR
-->
# DLE v2 — краткие обновления
- SingleChain Governance: голосование фиксируется в одной сети, исполнение в целевых сетях по EIP712 подписям без внешних мостов.
- Снапшоты голосующей силы: `ERC20Votes` (`getPastVotes`, `getPastTotalSupply`) исключают перелив голосов.
- Делегирование «только на себя»: 1 токен = 1 голос, запрет делегирования третьим лицам.
- Модульность: казна, таймлок, деактивация, коммуникации выделены в отдельные модули, операции выполняются через ядро DLE.
- «100% или ничего»: много-сетевые операции исполняются только при готовности всех целевых сетей.
- Детерминированный деплой: `FactoryDeployer` + CREATE2 для одинаковых адресов во всех выбранных сетях; INIT_CODE_HASH рассчитывается автоматически из актуального initCode.
- Аналитика: добавлены viewфункции для сводок, пагинации и агрегирования по предложениям.
---
# DLE - Единый Смарт-Контракт с Модульной Архитектурой
## 🎯 ПОЛНОЕ ПОНИМАНИЕ ЗАДАЧИ DLE
@@ -51,12 +63,12 @@ DLE (Digital Legal Entity) = Универсальная цифровая юри
### **3. СИСТЕМА УПРАВЛЕНИЯ:**
#### **Голосование и мультиподпись:**
#### **Голосование токен‑холдеров:**
```
- Только токен-холдеры участвуют в управлении
- Каждый токен = одна голосующая сила
- Кворум настраиваемый (например, 60% от общего количества токенов)
- Мультиподпись через токен-холдеров (проверка баланса при каждой операции)
- Коллективное голосование токенхолдеров (ERC20Votes снапшоты)
```
#### **Создание предложений:**
@@ -101,7 +113,6 @@ DLE (Digital Legal Entity) = Универсальная цифровая юри
```
- ERC-20 токены
- Система голосования
- Мультиподпись
- Мультичейн синхронизация
- Управление модулями
- DLEInfo (юридическая информация)
@@ -141,7 +152,7 @@ DLE (Digital Legal Entity) = Универсальная цифровая юри
```
- Только токен-холдеры управляют
- Проверка баланса при каждой операции
- Кворум мультиподписей
- Кворум голосов - все решения через коллективное голосование
- Синхронизация между цепочками
```
@@ -174,7 +185,7 @@ DLE.sol (Основной контракт) + Модули (добавляемы
1. **Один основной контракт** - управление токенами, голосованием, мультиподписью
2. **Модули** - специализированные функции (казначейство, иерархическое голосование, коммуникации)
3. **Только токен-холдеры** - никаких админских ролей
4. **Кворум мультиподписей** - все решения через коллективное голосование
4. **Кворум голосов** - все решения через коллективное голосование
5. **Проверка баланса** - при каждой операции
---
@@ -190,7 +201,7 @@ DLE.sol
├── Выбор цепочки для кворума (governanceChainId)
├── Синхронизация голосов между цепочками
├── Поддержка множественных цепочек
├── Мультиподпись (через токен-холдеров)
├── Голосование токенхолдеров
├── Мультичейн синхронизация
└── Система модулей (добавление/управление)
```
@@ -216,7 +227,7 @@ DLE.sol
- **Выбор цепочки для кворума** - токен-холдер может выбрать любую поддерживаемую цепочку
- **Синхронизация голосов** - после голосования результаты синхронизируются между цепочками
- **Поддержка множественных цепочек** - Ethereum, Polygon, BSC и др.
- **Мультиподпись** - через токен-холдеров с проверкой баланса
- **Голосование** - через токенхолдеров с проверкой баланса
- **Мультичейн синхронизация** - одинаковый адрес во всех цепочках
- **Управление модулями** - добавление/удаление через голосование
@@ -241,14 +252,14 @@ DLE.sol
### 5. ExternalDLEModule.sol ✅
- **Меж-DLE взаимодействие** - управление DLE B через приложение DLE A
- **Встраивание интерфейсов** - безопасное управление
- **Проверка прав** - через мультиподпись
- **Проверка прав** - через голосование токен‑холдеров
- **Аудит действий** - отслеживание операций
### 6. Мульти-чейн архитектура ✅
- **CREATE2 деплой** - одинаковый адрес во всех цепочках
- **Синхронизация состояния** - токены, предложения, голосования
- **Создание предложений** - в любой цепочке
- **Голосование** - в любой цепочке с синхронизацией
- **Голосование** - через токен‑холдеров с проверкой баланса
---
@@ -257,7 +268,7 @@ DLE.sol
### Основные принципы безопасности:
1. **Только токен-холдеры** - никаких админских ролей
2. **Проверка баланса** - при каждой операции
3. **Кворум мультиподписей** - все решения коллективные
3. **Кворум голосов** - все решения коллективные
4. **Простая логика** - минимум уязвимостей
### Защита от атак:
@@ -503,13 +514,10 @@ function createProposal(
uint256 _governanceChainId
) external returns (uint256);
// Синхронизация голосов между цепочками
function syncVoteFromChain(
uint256 _proposalId,
uint256 _fromChainId,
uint256 _forVotes,
uint256 _againstVotes,
bytes memory _proof
// Исполнение в целевых сетях по EIP-712 подписям (без мостов)
function executeProposalBySignatures(
uint256 proposalId,
bytes[] calldata signatures
) external;
// Проверка поддерживаемых цепочек
@@ -517,29 +525,8 @@ function isChainSupported(uint256 _chainId) external view returns (bool);
```
### Синхронизация между цепочками
```solidity
// Синхронизация токенов
function syncTokenBalance(
address holder,
uint256 balance,
uint256 fromChainId
) external;
// Синхронизация предложений
function syncProposal(
uint256 proposalId,
Proposal memory proposal,
uint256 fromChainId
) external;
// Синхронизация голосов
function syncVote(
uint256 proposalId,
address voter,
bool support,
uint256 fromChainId
) external;
```
- Результаты голосования фиксируются снапшотами ERC20Votes в governanceсети.
- Целевые сети принимают исполнение при верификации EIP712 подписей холдеров и кворума на зафиксированном timepoint.
---
@@ -940,7 +927,7 @@ contract DLE is ERC20, ReentrancyGuard {
### ✅ Безопасность
- Никаких админских ролей
- Простая логика мультиподписи
- Простая логика коллективного голосования
- Защита от основных атак
- Прозрачность всех операций
@@ -962,7 +949,7 @@ contract DLE is ERC20, ReentrancyGuard {
**DLE - это единый смарт-контракт с модульной архитектурой, который:**
1. **Управляется только токен-холдерами** через кворум мультиподписей
1. **Управляется только токенхолдерами** через кворум голосов
2. **Проверяет баланс токенов** при каждой операции
3. **Использует модули** для специализированных функций
4. **Синхронизируется между цепочками** с одинаковым адресом

View File

@@ -16,7 +16,7 @@
### Архитектурные требования
- **Single-Chain Governance**: Голосование происходит только в одной выбранной сети
- **Мультиподпись токен-холдеров**: Все операции требуют кворума подписей
- **Кворум голосов токенхолдеров**: Все операции требуют достижения кворума голосующей силы по снапшотам
- **Настраиваемые таймлоки**: Инициатор устанавливает задержку для каждого предложения
- **Cross-chain исполнение**: Решения выполняются во всех целевых сетях
- **Без админских ролей**: Только коллективное управление через токен-холдеров
@@ -24,11 +24,23 @@
### Технический стек
- **Frontend**: Vue.js 3 + Composition API
- **Web3**: ethers.js или web3.js
- **Контракты**: Solidity + OpenZeppelin + ERC-4337
- **Контракты**: Solidity + OpenZeppelin (ERC4337 опционально для кошельков/UX)
- **Стили**: Scoped CSS с переменными
---
## Обновления (DLE v2)
- Деплой:
- Мультисетевой деплой одной кнопкой: backend вызывает `deploy-multichain.js`.
- Предсказанные адреса DLE отображаются автоматически (endpoint `/api/dle-v2/predict-addresses`).
- INIT_CODE_HASH вычисляется автоматически на backend, не вводится вручную.
- Предложения (UI):
- Порядок секций: Базовая информация → Timelock → Governanceсеть → Целевые сети → Тип операции и параметры → Предпросмотр.
- Поля: `timelockHours`, `targetChains`, `governanceChainId`.
- Аналитика:
- Использовать новые viewфункции: `getProposalSummary`, `getProposalState`, `getProposalVotes`, `getQuorumAt`, `getVotingPowerAt`, `listSupportedChains`, `getGovernanceParams`.
## 1. БЛОК "ПРЕДЛОЖЕНИЯ" (`/management/proposals`)
### Задача 1.1: Создание предложений

View File

@@ -106,3 +106,18 @@ contract GovernanceModule {
- Тестируйте граничные случаи
- Валидируйте входные параметры
- Проверяйте обработку ошибок
# Модульная архитектура (обновление для DLE v2)
- Модули выносятся в отдельные контракты: `TreasuryModule`, `TimelockModule`, `DeactivationModule`, `CommunicationModule`.
- Подключение/отключение модулей — строго через предложения DLE (`ModuleAdded`/`ModuleRemoved`).
- Исполнение модульных операций инициируется основным DLE через `_executeOperation` по безопасному `operationCalldata`.
- Денежные переводы из ядра исключены: все токено‑операции внутри `TreasuryModule`.
- Таймлок применяется на уровне предложения: `timelockHours` хранится в `Proposal` и проверяется при исполнении.
- Для оффчейн действий ядро эмитит событие `OffchainAction`, которое подписывает и обрабатывает бекенд/клиент.
Последовательность для казначейской операции:
1) Создание предложения с типом операции и параметрами, указание `governanceChainId`, `targetChains`, `timelockHours`.
2) Сбор голосов в выбранной сети (снапшоты ERC20Votes).
3) По наступлению timelock — `executeProposalBySignatures` в целевых сетях с проверкой EIP712 подписей и «100% или ничего».
4) Ядро вызывает `TreasuryModule` по `abi.encodeWithSelector(...)`.

View File

@@ -14,8 +14,64 @@
## Основной смарт контракт DLE
### DLE v2: ключевые изменения и API (актуально)
- Безопасность: удалены уязвимые Merkleмеханизмы crosschain; нет внешних мостов/оракулов.
- Голосующая сила: OpenZeppelin `ERC20Votes` (снимки `getPastVotes`, `getPastTotalSupply`).
- Делегирование: жестко ограничено «только на себя»; третьим лицам делегировать нельзя (1 токен = 1 голос).
- SingleChain Governance: голосование происходит в одной выбранной сети (`governanceChainId`), время снапшота фиксируется на создании предложения и используется во всех сетях.
- MultiChain исполнение: выполнение в целевых сетях по EIP712 подписям холдеров, проверяется суммарная голосующая сила на зафиксированном `timepoint` (без доверия к мостам).
- «100% или ничего»: операции считаются успешными только при готовности/успешности всех целевых сетей.
- Модули вынесены отдельно: `Treasury`, `Timelock`, `Deactivation`, `Communication` и др. Управление только через предложения.
- Детерминированные адреса: фабрика `FactoryDeployer` + CREATE2. Единый адрес DLE и модулей во всех выбранных сетях. INIT_CODE_HASH автоподставляется из актуального initCode.
- Аналитика: добавлены viewфункции для агрегирования и пагинации.
Пример основных функций DLE v2 (интерфейс):
```solidity
// Создание предложения с фиксацией сети голосования, целевых сетей и таймлока
function createProposal(
string calldata description,
uint256 governanceChainId,
uint256[] calldata targetChains,
uint64 timelockHours,
bytes calldata operationCalldata
) external returns (uint256 proposalId);
// Голосование с использованием снапшотов ERC20Votes (учет силы на момент создания)
function vote(uint256 proposalId, bool support) external;
// Отмена инициатором при наличии достаточной голосующей силы (мягкая отмена)
function cancelProposal(uint256 proposalId) external;
// Исполнение в целевой сети по EIP-712 подписям холдеров (без мостов)
function executeProposalBySignatures(
uint256 proposalId,
bytes[] calldata signatures
) external;
// Просмотровые функции (аналитика)
function getProposalState(uint256 proposalId) external view returns (uint8);
function getProposalVotes(uint256 proposalId) external view returns (uint256 forVotes, uint256 againstVotes);
function getQuorumAt(uint256 timepoint) external view returns (uint256);
function getVotingPowerAt(address account, uint256 timepoint) external view returns (uint256);
function getProposalSummary(uint256 proposalId) external view returns (/* агрегированные поля */);
function getGovernanceParams() external view returns (/* кворум, снапшоты, chainIds */);
function listSupportedChains() external view returns (uint256[] memory);
```
События (ключевые):
- `ProposalCreated`, `ProposalCancelled`, `ProposalExecuted`
- `OffchainAction` (триггер оффчейн‑действий через события)
- `ModuleAdded`, `ModuleRemoved`
Замечания по безопасности:
- Снапшоты голосующей силы через `ERC20Votes` исключают перелив голосов.
- Верификация EIP712 подписей исключает зависимость от внешних мостов.
- Отсутствуют админ‑роли: все изменения только предложением и кворумом.
- Защита от повторов: `nonces` и EIP712 схемы подписи используются по стандарту OZ.
```
### Концепция
**Один смарт-контракт** с ERC-20 токенами, настраиваемым кворумом, мультиподписью и модулями. Адрес контракта одновременно выполняет функции банковского счета и контактных данных.
**Один смарт-контракт** с ERC-20 токенами, настраиваемым кворумом и модулями. Адрес контракта одновременно выполняет функции банковского счета и контактных данных.
### Архитектура
```
@@ -23,7 +79,7 @@ DLE.sol (Один контракт)
├── ERC-20 токены (голосующая сила)
├── Настраиваемый кворум (% от общего количества токенов)
├── Система голосования (проверка баланса токенов)
├── Мультиподпись (через токен-холдеров)
├── Голосование токенхолдеров
├── Модули (добавляемые через голосование)
├── Мультичейн синхронизация
└── Полное управление данными DLE через кворум
@@ -70,13 +126,12 @@ DLE.sol (Один контракт)
- **Изменение процента кворума** через кворум
- **Изменение текущей цепочки** через кворум
#### 5. Мультиподпись через токен-холдеров
- **Описание**: Система подписей для критических операций
#### 5. Голосование токенхолдеров
- **Описание**: Критические операции подтверждаются голосованием держателей токенов
- **Функции**:
- Подписание операций токен-холдерами
- Проверка баланса токенов при подписи
- Сбор подписей до достижения кворума
- Выполнение операций после сбора подписей
- Подача голосов за/против с учетом голосующей силы
- Подсчет голосов по снапшотам `ERC20Votes`
- Исполнение операций после достижения кворума
#### 6. Казначейские функции
- **Описание**: Управление финансами DLE через голосование
@@ -110,7 +165,7 @@ DLE может владеть токенами других DLE и участв
#### Механизм работы
1. **DLE A** владеет токенами **DLE B**
2. **Голос DLE A** в **DLE B** прямо пропорционален количеству токенов **DLE B** на балансе **DLE A**
3. Для участия в голосовании **DLE B** холдеры **DLE A** должны собрать **кворум мультиподписей** внутри **DLE A**
3. Для участия в голосовании **DLE B** холдеры **DLE A** должны собрать **кворум голосов** внутри **DLE A**
4. После достижения кворума подписей **DLE A** может голосовать в **DLE B** как единое целое
### Новые возможности изменения данных DLE ✅
@@ -165,7 +220,7 @@ event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId);
#### 4. Синхронизация
- Изменения синхронизируются во все поддерживаемые цепочки
- Merkle proofs обеспечивают безопасность cross-chain операций
- EIP712 подписи холдеров обеспечивают безопасность cross-chain исполнения (без мостов)
### Примеры использования
@@ -203,7 +258,7 @@ event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId);
#### Защита от злоупотреблений
- Все изменения только через кворум
- Проверка баланса токенов при голосовании
- Merkle proofs для cross-chain безопасности
- EIP712 подписи и проверка снапшотов для cross-chain безопасности
#### Аудит изменений
- Все изменения логируются в событиях
@@ -218,7 +273,7 @@ DLE может владеть токенами других DLE и участв
#### Механизм работы
1. **DLE A** владеет токенами **DLE B**
2. **Голос DLE A** в **DLE B** прямо пропорционален количеству токенов **DLE B** на балансе **DLE A**
3. Для участия в голосовании **DLE B** холдеры **DLE A** должны собрать **кворум мультиподписей** внутри **DLE A**
3. Для участия в голосовании **DLE B** холдеры **DLE A** должны собрать **кворум голосов** внутри **DLE A**
4. После достижения кворума подписей **DLE A** может голосовать в **DLE B** как единое целое
#### Пример
@@ -228,8 +283,8 @@ DLE может владеть токенами других DLE и участв
- **DLE B** получает от **DLE A** подпись на **10% голосов**
#### Технические требования
- Система сбора мультиподписей внутри DLE для внешнего голосования
- Проверка кворума подписей перед активацией голоса DLE
- Система сбора голосов внутри DLE для внешнего голосования
- Проверка прав через голосование
- Прямо пропорциональный подсчет голосов по количеству токенов
- Интерфейсы для взаимодействия между DLE
@@ -272,11 +327,11 @@ function DLEBManagementInterface({ dleBAddress }) {
- URL: `http://localhost:5173/dle-management`
- Встраивание компонентов управления DLE B
- Безопасное подписание транзакций для DLE B
- Проверка прав через мультиподпись
- Проверка прав через голосование
### Технические требования
- Один адрес = универсальная точка входа
- Безопасность мультиподписи через токен-холдеров
- Безопасность коллективного голосования токенхолдеров по снапшотам
- Масштабируемость через модули
- Поддержка аудио/видео коммуникации
- Совместимость с существующими стандартами (ERC-20, ERC-721)
@@ -299,12 +354,12 @@ DLE должен функционировать в нескольких блок
#### 2. Синхронные токены управления
- Одинаковое количество токенов для каждого партнера во всех сетях
- Синхронизация операций с токенами между всеми развернутыми сетями
- Все операции с токенами только через мультиподпись и кворум
- Все операции с токенами только через кворум голосов
- Защита от double-spending и рассинхронизации
#### 3. Single-Chain Governance система
- Инициатор предложения выбирает ОДНУ сеть для голосования
- Все токен-холдеры участвуют в мультиподписи только в выбранной сети
- Все токен-холдеры участвуют в голосовании только в выбранной сети
- Инициатор устанавливает таймлок для предложения
- Проверка балансов токен-холдеров при подписании
- Исполнение решения происходит во всех целевых сетях
@@ -408,7 +463,7 @@ contract DLE_SingleChainGovernance {
ERC-4337 предоставляет стандартную инфраструктуру для смарт-контракт кошельков с универсальностью (один адрес во всех цепочках) и готовыми решениями для оптимизации газа.
#### Компоненты ERC-4337
- **Smart Contract Wallets** - встроенная мультиподпись
- **Smart Contract Wallets** — инфраструктура аккаунтов (опционально для UX)
- **Bundlers** - оптимизация газа через агрегацию транзакций
- **Paymasters** - гибкая оплата транзакций
- **Account Abstraction** - универсальность и стандартизация
@@ -449,7 +504,7 @@ ERC-4337 распространяется под лицензией **CC0** (Pub
-**ERC-20** - токены управления
-**Governance** - система голосования
-**Access Control** - роли и разрешения
-**Multisig** - мультиподпись
(устарело) Multisig — используем голосование токен‑холдеров (ERC20Votes)
-**Timelock** - задержки выполнения
### 2. **ERC-4337** (аудит: Trail of Bits)
@@ -475,7 +530,7 @@ contract DLE is ERC20, Governor, TimelockController {
### **Компоненты для интеграции:**
- **ERC-20** - токен управления DLE
- **Governor** - система голосования с мультиподписью
- **Governor** - система голосования
- **TimelockController** - настраиваемые таймлоки
- **Account Abstraction** - универсальность адреса
@@ -493,10 +548,13 @@ contract DLE is ERC20, Governor, TimelockController {
### ✅ **Надежность**
- Временем проверенные решения
- Простая логика мультиподписи токен-холдеров
- Простая логика коллективного голосования токенхолдеров
- Понятные механизмы таймлоков
### ✅ **Совместимость**
- Стандартные интерфейсы Ethereum
- Совместимость с существующими кошельками
- Легкая интеграция с DeFi протоколами
### Примечание про ERC-4337 (опционально)
- Может использоваться в кошельках/окружении для UX (userOps), но не является частью ядра DLE v2.

View File

@@ -85,7 +85,7 @@ export async function createProposal(dleAddress, proposalData) {
// ABI для создания предложения
const dleAbi = [
"function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId) external returns (uint256)"
"function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId, uint256[] memory _targetChains, uint256 _timelockDelay) external returns (uint256)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
@@ -95,7 +95,9 @@ export async function createProposal(dleAddress, proposalData) {
proposalData.description,
proposalData.duration,
proposalData.operation,
proposalData.governanceChainId
proposalData.governanceChainId,
proposalData.targetChains || [],
proposalData.timelockDelay || 0
);
// Ждем подтверждения транзакции

View File

@@ -683,6 +683,61 @@
<div class="preview-item">
<strong>💰 Общая стоимость:</strong> ~${{ totalDeployCost.toFixed(2) }}
</div>
<!-- Предсказанные адреса (CREATE2) -->
<div class="preview-item predicted-addresses">
<div class="predicted-header">
<strong>📍 Предсказанные адреса DLE:</strong>
</div>
<ul class="networks-list" v-if="Object.keys(predictedAddresses).length">
<li v-for="net in selectedNetworkDetails" :key="net.chainId">
{{ net.name }} ({{ net.chainId }}):
<code class="addr">{{ predictedAddresses[net.chainId] || '—' }}</code>
<button
v-if="predictedAddresses[net.chainId]"
type="button"
class="btn btn-xs btn-outline-secondary"
@click="copyToClipboard(predictedAddresses[net.chainId])"
>Копировать</button>
</li>
</ul>
<small class="text-muted" v-else>Адреса вычисляются автоматически при выборе сетей.</small>
</div>
</div>
<!-- Ключи блокчейн-сканов (опционально) -->
<div v-if="selectedNetworks.length > 0" class="preview-section explorer-keys-section">
<h4>🧩 Ключи блокчейн-сканов (опционально для авто-верификации)</h4>
<div class="explorer-keys-grid">
<div
v-for="network in selectedNetworkDetails"
:key="network.chainId"
class="explorer-key-item"
>
<label class="explorer-key-label">
{{ network.name }} (Chain ID: {{ network.chainId }})
</label>
<div class="explorer-key-input">
<input
:type="explorerKeyVisibility[network.chainId] ? 'text' : 'password'"
class="form-control"
:placeholder="`API ключ скана для ${network.name}`"
v-model="explorerApiKeys[network.chainId]"
autocomplete="off"
/>
<button type="button" class="btn btn-secondary btn-sm"
@click="explorerKeyVisibility[network.chainId] = !explorerKeyVisibility[network.chainId]">
{{ explorerKeyVisibility[network.chainId] ? 'Скрыть' : 'Показать' }}
</button>
<button type="button" class="btn btn-outline-danger btn-sm" @click="explorerApiKeys[network.chainId] = ''">
Очистить
</button>
</div>
</div>
</div>
<div class="explorer-keys-actions">
<label><input type="checkbox" v-model="persistExplorerKeys" /> Сохранить локально до конца деплоя</label>
</div>
</div>
<!-- Приватный ключ -->
@@ -903,6 +958,13 @@ const availableNetworks = ref([]);
const isLoadingNetworks = ref(false);
const totalDeployCost = ref(0);
const predictedAddress = ref('');
const predictedAddresses = reactive({}); // { chainId: address }
const isPredicting = ref(false);
// Ключи блокчейн-сканов (локально)
const explorerApiKeys = reactive({}); // { [chainId]: apiKey }
const explorerKeyVisibility = reactive({});
const persistExplorerKeys = ref(false);
// Состояние для приватных ключей
const useSameKeyForAllChains = ref(true);
@@ -958,6 +1020,61 @@ const hasSelectedNetworks = computed(() => {
return selectedNetworks.value.length > 0;
});
// Инициализация полей ключей при смене выбранных сетей
watch(selectedNetworkDetails, (nets) => {
nets.forEach(n => {
if (!(n.chainId in explorerKeyVisibility)) explorerKeyVisibility[n.chainId] = false;
if (persistExplorerKeys.value) {
const saved = localStorage.getItem(`scan_key_${n.chainId}`);
if (saved && !explorerApiKeys[n.chainId]) explorerApiKeys[n.chainId] = saved;
}
});
if (nets && nets.length > 0) predictAddresses();
}, { immediate: true });
watch(persistExplorerKeys, (val) => {
if (!val) return;
Object.entries(explorerApiKeys).forEach(([chainId, key]) => {
if (key) localStorage.setItem(`scan_key_${chainId}`, key);
});
});
function clearExplorerKeys() {
Object.keys(explorerApiKeys).forEach((k) => explorerApiKeys[k] = '');
Object.keys(localStorage)
.filter(k => k.startsWith('scan_key_'))
.forEach(k => localStorage.removeItem(k));
}
// Предсказание адресов (упрощенно через бэкенд)
async function predictAddresses() {
try {
isPredicting.value = true;
const payload = {
name: dleSettings.name,
symbol: dleSettings.tokenSymbol,
selectedNetworks: selectedNetworkDetails.value.map(n => n.chainId)
};
const resp = await axios.post('/dle-v2/predict-addresses', payload);
if (resp.data && resp.data.success && resp.data.data) {
// ожидаем вид { [chainId]: address }
Object.keys(predictedAddresses).forEach(k => delete predictedAddresses[k]);
Object.assign(predictedAddresses, resp.data.data);
}
} catch (e) {
console.error('Ошибка расчета предсказанных адресов:', e);
alert('Не удалось рассчитать предсказанные адреса');
} finally {
isPredicting.value = false;
}
}
function copyToClipboard(text) {
navigator.clipboard?.writeText(text).then(() => {
// no-op
}).catch(() => {});
}
// Информация о выбранном стандарте токена
const selectedTokenStandardInfo = computed(() => {
return tokenStandardsData[dleSettings.tokenStandard] || null;
@@ -2306,7 +2423,8 @@ const deploySmartContracts = async () => {
currentChainId: dleSettings.selectedNetworks[0] || 1,
// Приватный ключ для деплоя
privateKey: unifiedPrivateKey.value
privateKey: unifiedPrivateKey.value,
explorerApiKeys: explorerApiKeys
};
console.log('Данные для деплоя DLE:', deployData);
@@ -2333,6 +2451,12 @@ const deploySmartContracts = async () => {
// Перенаправляем на главную страницу управления
router.push('/management');
}, 2000);
if (!persistExplorerKeys.value) {
Object.keys(explorerApiKeys).forEach((k) => explorerApiKeys[k] = '');
Object.keys(localStorage)
.filter(k => k.startsWith('scan_key_'))
.forEach(k => localStorage.removeItem(k));
}
} else {
showDeployProgress.value = false;
@@ -2375,6 +2499,15 @@ const validateCoordinates = (coordinates) => {
</script>
<style scoped>
.explorer-keys-section { margin-top: 16px; }
.explorer-keys-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
.explorer-key-item { display: flex; flex-direction: column; gap: 8px; }
.explorer-key-input { display: flex; gap: 8px; align-items: center; flex-wrap: nowrap; }
.explorer-key-input input { flex: 1 1 auto; width: auto; min-width: 0; }
.explorer-keys-actions { margin-top: 8px; display: flex; gap: 12px; align-items: center; }
@media (min-width: 768px) {
.explorer-keys-grid { grid-template-columns: 1fr 1fr; }
}
.settings-panel {
padding: var(--block-padding);
background-color: var(--color-light);

View File

@@ -214,6 +214,15 @@
</div>
</div>
<!-- Timelock -->
<div class="form-section">
<h5> Timelock</h5>
<div class="form-group-inline">
<label for="timelockHours">Задержка исполнения (часы):</label>
<input id="timelockHours" type="number" min="0" step="1" v-model.number="newProposal.timelockHours" class="form-control small" />
</div>
</div>
<!-- Выбор цепочки для кворума -->
<div class="form-section">
<h5>🔗 Выбор цепочки для кворума</h5>
@@ -239,7 +248,23 @@
</div>
</div>
<!-- Тип операции -->
<!-- Целевые сети для исполнения (мультиселект) -->
<div class="form-section" v-if="showTargetChains">
<h5>🎯 Целевые сети для исполнения</h5>
<div class="targets-grid">
<label v-for="chain in availableChains" :key="chain.chainId" class="target-item">
<input type="checkbox" :value="chain.chainId" v-model="newProposal.targetChains" />
<span>{{ chain.name }} ({{ chain.chainId }})</span>
</label>
</div>
<small class="text-muted">Для offchainдействий целевые сети не требуются.</small>
</div>
<!-- Тип операции (последним блоком) -->
<div class="form-section">
<h5> Тип операции</h5>
@@ -458,7 +483,22 @@
</div>
</div>
<!-- Предварительный просмотр -->
<!-- Действия -->
<div class="form-actions">
<button
class="btn btn-success"
@click="createProposal"
:disabled="!isFormValid || isCreating"
>
<i class="fas fa-paper-plane"></i>
{{ isCreating ? 'Создание...' : 'Создать предложение' }}
</button>
<button class="btn btn-secondary" @click="resetForm">
<i class="fas fa-undo"></i> Сбросить
</button>
</div>
<!-- Предварительный просмотр (в конце формы) -->
<div class="form-section">
<h5>👁 Предварительный просмотр</h5>
<div class="preview-card">
@@ -480,21 +520,6 @@
</div>
</div>
</div>
<!-- Действия -->
<div class="form-actions">
<button
class="btn btn-success"
@click="createProposal"
:disabled="!isFormValid || isCreating"
>
<i class="fas fa-paper-plane"></i>
{{ isCreating ? 'Создание...' : 'Создать предложение' }}
</button>
<button class="btn btn-secondary" @click="resetForm">
<i class="fas fa-undo"></i> Сбросить
</button>
</div>
</div>
</div> <!-- Закрываем div для авторизованных пользователей -->
</div>
@@ -507,6 +532,11 @@ import { useRouter, useRoute } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth';
import BaseLayout from '../../components/BaseLayout.vue';
import { getDLEInfo, loadProposals, createProposal as createProposalAPI, voteForProposal as voteForProposalAPI, executeProposal as executeProposalAPI, getSupportedChains } from '../../utils/dle-contract.js';
const showTargetChains = computed(() => {
// Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн)
// Можно расширить логику при появлении offchain типа
return true;
});
import wsClient from '../../utils/websocket.js';
import { ethers } from 'ethers';
@@ -552,6 +582,8 @@ const newProposal = ref({
description: '',
duration: 7,
governanceChainId: null,
timelockHours: 0,
targetChains: [],
operationType: '',
operationParams: {
to: '',
@@ -584,6 +616,7 @@ const isFormValid = computed(() => {
newProposal.value.duration > 0 &&
newProposal.value.governanceChainId &&
newProposal.value.operationType &&
newProposal.value.timelockHours >= 0 &&
validateOperationParams()
);
});
@@ -981,7 +1014,9 @@ async function createProposal() {
description: newProposal.value.description,
duration: newProposal.value.duration * 24 * 60 * 60, // конвертируем в секунды
operation: operation,
governanceChainId: newProposal.value.governanceChainId
governanceChainId: newProposal.value.governanceChainId,
targetChains: showTargetChains.value ? newProposal.value.targetChains : [],
timelockDelay: (newProposal.value.timelockHours || 0) * 3600
});
console.log('Предложение создано:', result);