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

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", "_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", "_format": "hh-sol-cache-2",
"files": { "files": {
"/home/alex/Digital_Legal_Entity(DLE)/backend/contracts/DLE.sol": { "/home/alex/Digital_Legal_Entity(DLE)/backend/contracts/DLE.sol": {
"lastModificationDate": 1754485037554, "lastModificationDate": 1754682456067,
"contentHash": "f121cb518877db715ab5cd2e3ee5ff3a", "contentHash": "6d46f24614a8d6c838144dcfad200e26",
"sourceName": "contracts/DLE.sol", "sourceName": "contracts/DLE.sol",
"solcConfig": { "solcConfig": {
"version": "0.8.20", "version": "0.8.20",
@@ -32,8 +32,10 @@
}, },
"imports": [ "imports": [
"@openzeppelin/contracts/token/ERC20/ERC20.sol", "@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/ReentrancyGuard.sol",
"@openzeppelin/contracts/utils/cryptography/MerkleProof.sol" "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"
], ],
"versionPragmas": [ "versionPragmas": [
"^0.8.20" "^0.8.20"
@@ -348,6 +350,864 @@
"artifacts": [ "artifacts": [
"Hashes" "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; pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 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/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
/** /**
* @title DLE (Digital Legal Entity) * @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 { struct DLEInfo {
string name; string name;
string symbol; string symbol;
@@ -53,11 +57,15 @@ contract DLE is ERC20, ReentrancyGuard {
uint256 forVotes; uint256 forVotes;
uint256 againstVotes; uint256 againstVotes;
bool executed; bool executed;
uint256 deadline; bool canceled;
uint256 deadline; // конец периода голосования (sec)
address initiator; 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(address => bool) hasVoted;
mapping(uint256 => bool) chainVoteSynced; // Синхронизация голосов между цепочками
} }
@@ -74,15 +82,11 @@ contract DLE is ERC20, ReentrancyGuard {
// Предложения // Предложения
mapping(uint256 => Proposal) public proposals; mapping(uint256 => Proposal) public proposals;
uint256[] public allProposalIds;
// Мульти-чейн // Мульти-чейн
mapping(uint256 => bool) public supportedChains; mapping(uint256 => bool) public supportedChains;
uint256[] public supportedChainIds; 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( event DLEInitialized(
@@ -101,21 +105,29 @@ contract DLE is ERC20, ReentrancyGuard {
event ProposalCreated(uint256 proposalId, address initiator, string description); event ProposalCreated(uint256 proposalId, address initiator, string description);
event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower); event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower);
event ProposalExecuted(uint256 proposalId, bytes operation); 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 ModuleAdded(bytes32 moduleId, address moduleAddress);
event ModuleRemoved(bytes32 moduleId); event ModuleRemoved(bytes32 moduleId);
event CrossChainExecutionSync(uint256 proposalId, uint256 fromChainId, uint256 toChainId); event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId);
event CrossChainVoteSync(uint256 proposalId, uint256 fromChainId, uint256 toChainId);
event ChainAdded(uint256 chainId); event ChainAdded(uint256 chainId);
event ChainRemoved(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 DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, uint256 oktmo, string[] okvedCodes, uint256 kpp);
event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage); event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage);
event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId); 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( constructor(
DLEConfig memory config, DLEConfig memory config,
uint256 _currentChainId uint256 _currentChainId
) ERC20(config.name, config.symbol) { ) ERC20(config.name, config.symbol) ERC20Permit(config.name) {
dleInfo = DLEInfo({ dleInfo = DLEInfo({
name: config.name, name: config.name,
symbol: config.symbol, symbol: config.symbol,
@@ -143,9 +155,13 @@ contract DLE is ERC20, ReentrancyGuard {
require(config.initialPartners.length > 0, "No initial partners"); require(config.initialPartners.length > 0, "No initial partners");
for (uint256 i = 0; i < config.initialPartners.length; i++) { for (uint256 i = 0; i < config.initialPartners.length; i++) {
require(config.initialPartners[i] != address(0), "Zero address"); address partner = config.initialPartners[i];
require(config.initialAmounts[i] > 0, "Zero amount"); uint256 amount = config.initialAmounts[i];
_mint(config.initialPartners[i], 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); emit InitialTokensDistributed(config.initialPartners, config.initialAmounts);
@@ -174,12 +190,14 @@ contract DLE is ERC20, ReentrancyGuard {
string memory _description, string memory _description,
uint256 _duration, uint256 _duration,
bytes memory _operation, bytes memory _operation,
uint256 _governanceChainId uint256 _governanceChainId,
uint256[] memory _targetChains,
uint256 _timelockDelay
) external returns (uint256) { ) external returns (uint256) {
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal"); require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
require(_duration > 0, "Duration must be positive"); require(_duration > 0, "Duration must be positive");
require(supportedChains[_governanceChainId], "Chain not supported"); require(supportedChains[_governanceChainId], "Chain not supported");
require(checkChainConnection(_governanceChainId), "Chain not available"); require(_timelockDelay <= 365 days, "Timelock too big");
uint256 proposalId = proposalCounter++; uint256 proposalId = proposalCounter++;
Proposal storage proposal = proposals[proposalId]; Proposal storage proposal = proposals[proposalId];
@@ -192,8 +210,22 @@ contract DLE is ERC20, ReentrancyGuard {
proposal.deadline = block.timestamp + _duration; proposal.deadline = block.timestamp + _duration;
proposal.initiator = msg.sender; proposal.initiator = msg.sender;
proposal.operation = _operation; 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 ProposalCreated(proposalId, msg.sender, _description);
emit ProposalGovernanceChainSet(proposalId, _governanceChainId);
emit ProposalTargetsSet(proposalId, _targetChains);
emit ProposalTimelockSet(proposalId, proposal.timelock);
return proposalId; return proposalId;
} }
@@ -208,9 +240,10 @@ contract DLE is ERC20, ReentrancyGuard {
require(block.timestamp < proposal.deadline, "Voting ended"); require(block.timestamp < proposal.deadline, "Voting ended");
require(!proposal.executed, "Proposal already executed"); require(!proposal.executed, "Proposal already executed");
require(!proposal.hasVoted[msg.sender], "Already voted"); 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; proposal.hasVoted[msg.sender] = true;
if (_support) { if (_support) {
@@ -222,53 +255,7 @@ contract DLE is ERC20, ReentrancyGuard {
emit ProposalVoted(_proposalId, msg.sender, _support, votingPower); emit ProposalVoted(_proposalId, msg.sender, _support, votingPower);
} }
/** // УДАЛЕНО: syncVoteFromChain с MerkleProof — небезопасно без доверенного моста
* @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);
}
/** /**
* @dev Проверить результат предложения * @dev Проверить результат предложения
@@ -281,7 +268,9 @@ contract DLE is ERC20, ReentrancyGuard {
require(proposal.id == _proposalId, "Proposal does not exist"); require(proposal.id == _proposalId, "Proposal does not exist");
uint256 totalVotes = proposal.forVotes + proposal.againstVotes; 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; quorumReached = totalVotes >= quorumRequired;
passed = quorumReached && proposal.forVotes > proposal.againstVotes; passed = quorumReached && proposal.forVotes > proposal.againstVotes;
@@ -297,6 +286,7 @@ contract DLE is ERC20, ReentrancyGuard {
Proposal storage proposal = proposals[_proposalId]; Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist"); require(proposal.id == _proposalId, "Proposal does not exist");
require(!proposal.executed, "Proposal already executed"); require(!proposal.executed, "Proposal already executed");
require(currentChainId == proposal.governanceChainId, "Execute only in governance chain");
(bool passed, bool quorumReached) = checkProposalResult(_proposalId); (bool passed, bool quorumReached) = checkProposalResult(_proposalId);
@@ -307,6 +297,7 @@ contract DLE is ERC20, ReentrancyGuard {
"Voting not ended and quorum not reached" "Voting not ended and quorum not reached"
); );
require(passed && quorumReached, "Proposal not passed"); require(passed && quorumReached, "Proposal not passed");
require(block.timestamp >= proposal.timelock, "Timelock not expired");
proposal.executed = true; proposal.executed = true;
@@ -317,49 +308,74 @@ contract DLE is ERC20, ReentrancyGuard {
} }
/** /**
* @dev Синхронизировать исполнение из другой цепочки * @dev Отмена предложения до истечения голосования инициатором при наличии достаточной голосующей силы.
* @param _proposalId ID предложения * Это soft-cancel для защиты от явных ошибок. Порог: >= 10% от снапшотного supply.
* @param _fromChainId ID цепочки откуда синхронизируем
*/ */
function syncExecutionFromChain( function cancelProposal(uint256 _proposalId, string calldata reason) external {
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;
// Проверяем, что предложение существует и не было исполнено
Proposal storage proposal = proposals[_proposalId]; Proposal storage proposal = proposals[_proposalId];
require(proposal.id == _proposalId, "Proposal does not exist"); 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; proposal.canceled = true;
emit ProposalCancelled(_proposalId, reason);
// Исполняем операцию из предложения
if (proposal.id == _proposalId) {
_executeOperation(proposal.operation);
} }
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 Доступна ли цепочка * @return isAvailable Доступна ли цепочка
*/ */
function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable) { function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable) {
// Проверяем, поддерживается ли цепочка // Упрощенная проверка: цепочка объявлена как поддерживаемая
if (!supportedChains[_chainId]) { return supportedChains[_chainId];
return false;
}
// Проверяем, что Merkle root установлен для цепочки
// Это означает, что цепочка активна и готова к синхронизации
bytes32 merkleRoot = chainMerkleRoots[_chainId];
if (merkleRoot == bytes32(0)) {
return false;
}
return true;
} }
/** /**
@@ -410,12 +415,8 @@ contract DLE is ERC20, ReentrancyGuard {
function syncToAllChains(uint256 _proposalId) external { function syncToAllChains(uint256 _proposalId) external {
require(checkSyncReadiness(_proposalId), "Not all chains ready"); require(checkSyncReadiness(_proposalId), "Not all chains ready");
// Выполняем синхронизацию во все цепочки // В этой версии без внешнего моста синхронизация выполняется
for (uint256 i = 0; i < getSupportedChainCount(); i++) { // через executeProposalBySignatures в целевых сетях.
uint256 chainId = getSupportedChainId(i);
syncToChain(_proposalId, chainId);
}
emit SyncCompleted(_proposalId); emit SyncCompleted(_proposalId);
} }
@@ -424,26 +425,7 @@ contract DLE is ERC20, ReentrancyGuard {
* @param _proposalId ID предложения * @param _proposalId ID предложения
* @param _chainId ID цепочки * @param _chainId ID цепочки
*/ */
function syncToChain(uint256 _proposalId, uint256 _chainId) internal { // УДАЛЕНО: syncToChain — не используется в подпись‑ориентированной схеме
// Проверяем, что цепочка поддерживается
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);
}
/** /**
* @dev Получить количество поддерживаемых цепочек * @dev Получить количество поддерживаемых цепочек
@@ -465,14 +447,12 @@ contract DLE is ERC20, ReentrancyGuard {
* @dev Добавить поддерживаемую цепочку (только для владельцев токенов) * @dev Добавить поддерживаемую цепочку (только для владельцев токенов)
* @param _chainId ID цепочки * @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(!supportedChains[_chainId], "Chain already supported");
require(_chainId != currentChainId, "Cannot add current chain"); require(_chainId != currentChainId, "Cannot add current chain");
supportedChains[_chainId] = true; supportedChains[_chainId] = true;
supportedChainIds.push(_chainId); supportedChainIds.push(_chainId);
emit ChainAdded(_chainId); emit ChainAdded(_chainId);
} }
@@ -480,13 +460,10 @@ contract DLE is ERC20, ReentrancyGuard {
* @dev Удалить поддерживаемую цепочку (только для владельцев токенов) * @dev Удалить поддерживаемую цепочку (только для владельцев токенов)
* @param _chainId ID цепочки * @param _chainId ID цепочки
*/ */
function removeSupportedChain(uint256 _chainId) external { function _removeSupportedChain(uint256 _chainId) internal {
require(balanceOf(msg.sender) > 0, "Must hold tokens to remove chain");
require(supportedChains[_chainId], "Chain not supported"); require(supportedChains[_chainId], "Chain not supported");
require(_chainId != currentChainId, "Cannot remove current chain"); require(_chainId != currentChainId, "Cannot remove current chain");
supportedChains[_chainId] = false; supportedChains[_chainId] = false;
// Удаляем из массива // Удаляем из массива
for (uint256 i = 0; i < supportedChainIds.length; i++) { for (uint256 i = 0; i < supportedChainIds.length; i++) {
if (supportedChainIds[i] == _chainId) { if (supportedChainIds[i] == _chainId) {
@@ -495,10 +472,6 @@ contract DLE is ERC20, ReentrancyGuard {
break; break;
} }
} }
// Очищаем Merkle root для цепочки
delete chainMerkleRoots[_chainId];
emit ChainRemoved(_chainId); emit ChainRemoved(_chainId);
} }
@@ -507,22 +480,13 @@ contract DLE is ERC20, ReentrancyGuard {
* @param _chainId ID цепочки * @param _chainId ID цепочки
* @param _merkleRoot Merkle root для цепочки * @param _merkleRoot Merkle root для цепочки
*/ */
function setChainMerkleRoot(uint256 _chainId, bytes32 _merkleRoot) external { // УДАЛЕНО: setChainMerkleRoot — небезопасно отдавать любому холдеру
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);
}
/** /**
* @dev Получить Merkle root для цепочки * @dev Получить Merkle root для цепочки
* @param _chainId ID цепочки * @param _chainId ID цепочки
*/ */
function getChainMerkleRoot(uint256 _chainId) external view returns (bytes32) { // УДАЛЕНО: getChainMerkleRoot — устарело
return chainMerkleRoots[_chainId];
}
/** /**
* @dev Исполнить операцию * @dev Исполнить операцию
@@ -532,19 +496,7 @@ contract DLE is ERC20, ReentrancyGuard {
// Декодируем операцию // Декодируем операцию
(bytes4 selector, bytes memory data) = abi.decode(_operation, (bytes4, bytes)); (bytes4 selector, bytes memory data) = abi.decode(_operation, (bytes4, bytes));
if (selector == bytes4(keccak256("transfer(address,uint256)"))) { if (selector == bytes4(keccak256("updateDLEInfo(string,string,string,string,uint256,uint256,string[],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)"))) {
// Операция обновления информации DLE // Операция обновления информации DLE
(string memory name, string memory symbol, string memory location, string memory coordinates, (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)); 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)); (bytes32 moduleId) = abi.decode(data, (bytes32));
_removeModule(moduleId); _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 { } else {
// Неизвестная операция // Неизвестная операция
revert("Unknown operation"); revert("Unknown operation");
@@ -654,7 +616,6 @@ contract DLE is ERC20, ReentrancyGuard {
uint256 _chainId uint256 _chainId
) external returns (uint256) { ) external returns (uint256) {
require(supportedChains[_chainId], "Chain not supported"); require(supportedChains[_chainId], "Chain not supported");
require(checkChainConnection(_chainId), "Chain not available");
require(_moduleAddress != address(0), "Zero address"); require(_moduleAddress != address(0), "Zero address");
require(!activeModules[_moduleId], "Module already exists"); require(!activeModules[_moduleId], "Module already exists");
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal"); require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
@@ -693,7 +654,6 @@ contract DLE is ERC20, ReentrancyGuard {
uint256 _chainId uint256 _chainId
) external returns (uint256) { ) external returns (uint256) {
require(supportedChains[_chainId], "Chain not supported"); require(supportedChains[_chainId], "Chain not supported");
require(checkChainConnection(_chainId), "Chain not available");
require(activeModules[_moduleId], "Module does not exist"); require(activeModules[_moduleId], "Module does not exist");
require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal"); require(balanceOf(msg.sender) > 0, "Must hold tokens to create proposal");
@@ -782,192 +742,146 @@ contract DLE is ERC20, ReentrancyGuard {
return currentChainId; return currentChainId;
} }
// События для новых функций // ===== Интерфейс аналитики для API =====
event SyncCompleted(uint256 proposalId); function getProposalSummary(uint256 _proposalId) external view returns (
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 (
uint256 id, uint256 id,
string memory description, string memory description,
uint256 forVotes, uint256 forVotes,
uint256 againstVotes, uint256 againstVotes,
bool executed, bool executed,
bool canceled,
uint256 deadline, uint256 deadline,
address initiator, address initiator,
uint256 chainId uint256 governanceChainId,
uint256 timelock,
uint256 snapshotTimepoint,
uint256[] memory targets
) { ) {
DeactivationProposal storage proposal = deactivationProposals[_proposalId]; Proposal storage p = proposals[_proposalId];
require(proposal.id == _proposalId, "Deactivation proposal does not exist"); require(p.id == _proposalId, "Proposal does not exist");
return ( return (
proposal.id, p.id,
proposal.description, p.description,
proposal.forVotes, p.forVotes,
proposal.againstVotes, p.againstVotes,
proposal.executed, p.executed,
proposal.deadline, p.canceled,
proposal.initiator, p.deadline,
proposal.chainId 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 path = require('path');
const fs = require('fs'); const fs = require('fs');
const ethers = require('ethers'); // Added ethers for private key validation const ethers = require('ethers'); // Added ethers for private key validation
const create2 = require('../utils/create2');
/** /**
* @route POST /api/dle-v2 * @route POST /api/dle-v2
@@ -299,3 +300,33 @@ router.post('/validate-private-key', async (req, res, next) => {
}); });
module.exports = router; 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 Тарабанов Александр Викторович * Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved. * 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); fs.copyFileSync(paramsFile, tempParamsFile);
logger.info(`Файл параметров скопирован успешно`); logger.info(`Файл параметров скопирован успешно`);
// Определяем сеть для деплоя (берем первую из выбранных сетей) // Готовим RPC для всех выбранных сетей
const chainId = deployParams.supportedChainIds && deployParams.supportedChainIds.length > 0 const rpcUrls = [];
? deployParams.supportedChainIds[0] for (const cid of deployParams.supportedChainIds) {
: 1; // По умолчанию Ethereum logger.info(`Поиск RPC URL для chain_id: ${cid}`);
const ru = await getRpcUrlByChainId(cid);
// Получаем rpc_url из базы по chain_id if (!ru) {
logger.info(`Поиск RPC URL для chain_id: ${chainId}`); throw new Error(`RPC URL для сети с chain_id ${cid} не найден в базе данных`);
const rpcUrl = await getRpcUrlByChainId(chainId); }
if (!rpcUrl) { rpcUrls.push(ru);
logger.error(`RPC URL для сети с chain_id ${chainId} не найден в базе данных`);
throw new Error(`RPC URL для сети с chain_id ${chainId} не найден в базе данных`);
} }
logger.info(`Найден RPC URL для chain_id ${chainId}: ${rpcUrl}`);
// Проверяем баланс кошелька // Лёгкая проверка баланса в первой сети
{
const { ethers } = require('ethers'); 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; const walletAddress = dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey, provider).address : null;
if (walletAddress) { if (walletAddress) {
const balance = await provider.getBalance(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`); logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`);
if (balance < minBalance) { if (balance < minBalance) {
logger.warn(`Недостаточно ETH для деплоя. Баланс: ${ethers.formatEther(balance)} ETH, требуется минимум: ${ethers.formatEther(minBalance)} ETH`); throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`);
throw new Error(`Недостаточно ETH для деплоя. Баланс: ${ethers.formatEther(balance)} ETH, требуется минимум: ${ethers.formatEther(minBalance)} ETH. Пополните кошелек через Sepolia faucet: https://sepoliafaucet.com/`); }
} }
} }
if (!dleParams.privateKey) { if (!dleParams.privateKey) {
throw new Error('Приватный ключ для деплоя не передан'); throw new Error('Приватный ключ для деплоя не передан');
} }
// Маппинг chain_id к именам сетей в Hardhat // Рассчитываем INIT_CODE_HASH автоматически из актуального initCode
const chainIdToNetworkName = { const initCodeHash = await this.computeInitCodeHash(deployParams);
1: 'ethereum',
137: 'polygon',
56: 'bsc',
42161: 'arbitrum',
11155111: 'sepolia'
};
const networkName = chainIdToNetworkName[chainId]; // Собираем адреса фабрик по сетям (если есть)
if (!networkName) { const factoryAddresses = deployParams.supportedChainIds.map(cid => process.env[`FACTORY_ADDRESS_${cid}`] || '').join(',');
throw new Error(`Сеть с chain_id ${chainId} не поддерживается для деплоя`);
}
// Запускаем скрипт деплоя с нужными переменными окружения // Мультисетевой деплой одним вызовом
const result = await this.runDeployScript(paramsFile, { const result = await this.runDeployMultichain(paramsFile, {
rpcUrl, rpcUrls: rpcUrls.join(','),
privateKey: dleParams.privateKey, privateKey: dleParams.privateKey,
networkId: networkName, salt: process.env.CREATE2_SALT,
envNetworkKey: chainId.toString().toUpperCase() initCodeHash,
factories: factoryAddresses
}); });
// Очищаем временные файлы // Очищаем временные файлы
@@ -237,14 +226,12 @@ class DLEV2Service {
return; return;
} }
// Формируем переменные окружения для скрипта деплоя
const envVars = { const envVars = {
...process.env, ...process.env,
RPC_URL: extraEnv.rpcUrl, RPC_URL: extraEnv.rpcUrl,
PRIVATE_KEY: extraEnv.privateKey PRIVATE_KEY: extraEnv.privateKey
}; };
// Запускаем скрипт без указания сети, передаем RPC URL и приватный ключ через переменные окружения
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath], { const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath], {
cwd: path.join(__dirname, '..'), cwd: path.join(__dirname, '..'),
env: envVars, env: envVars,
@@ -266,7 +253,6 @@ class DLEV2Service {
hardhatProcess.on('close', (code) => { hardhatProcess.on('close', (code) => {
try { try {
// Пытаемся извлечь результат из stdout независимо от кода завершения
const result = this.extractDeployResult(stdout); const result = this.extractDeployResult(stdout);
resolve(result); resolve(result);
} catch (error) { } 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 * Извлекает результат деплоя из stdout
* @param {string} stdout - Вывод скрипта * @param {string} stdout - Вывод скрипта
@@ -358,6 +378,30 @@ class DLEV2Service {
return []; 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(); module.exports = new DLEV2Service();

View File

@@ -416,8 +416,9 @@ async function getBot() {
if (recentMessages && recentMessages.length > 0) { if (recentMessages && recentMessages.length > 0) {
// Преобразуем сообщения в формат для AI // Преобразуем сообщения в формат для AI
history = recentMessages.reverse().map(msg => ({ history = recentMessages.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant', // Любые человеческие роли трактуем как 'user', только ответы ассистента — 'assistant'
content: msg.content || '' // content уже расшифрован encryptedDb role: msg.sender_type === 'assistant' ? 'assistant' : 'user',
content: msg.content || ''
})); }));
} }
} catch (historyError) { } catch (historyError) {
@@ -465,7 +466,7 @@ async function getBot() {
sender_type: 'assistant', sender_type: 'assistant',
content: aiResponse, content: aiResponse,
channel: 'telegram', channel: 'telegram',
role: role, role: 'assistant',
direction: 'out', direction: 'out',
created_at: new Date() 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 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 - Единый Смарт-Контракт с Модульной Архитектурой
## 🎯 ПОЛНОЕ ПОНИМАНИЕ ЗАДАЧИ DLE ## 🎯 ПОЛНОЕ ПОНИМАНИЕ ЗАДАЧИ DLE
@@ -51,12 +63,12 @@ DLE (Digital Legal Entity) = Универсальная цифровая юри
### **3. СИСТЕМА УПРАВЛЕНИЯ:** ### **3. СИСТЕМА УПРАВЛЕНИЯ:**
#### **Голосование и мультиподпись:** #### **Голосование токен‑холдеров:**
``` ```
- Только токен-холдеры участвуют в управлении - Только токен-холдеры участвуют в управлении
- Каждый токен = одна голосующая сила - Каждый токен = одна голосующая сила
- Кворум настраиваемый (например, 60% от общего количества токенов) - Кворум настраиваемый (например, 60% от общего количества токенов)
- Мультиподпись через токен-холдеров (проверка баланса при каждой операции) - Коллективное голосование токенхолдеров (ERC20Votes снапшоты)
``` ```
#### **Создание предложений:** #### **Создание предложений:**
@@ -101,7 +113,6 @@ DLE (Digital Legal Entity) = Универсальная цифровая юри
``` ```
- ERC-20 токены - ERC-20 токены
- Система голосования - Система голосования
- Мультиподпись
- Мультичейн синхронизация - Мультичейн синхронизация
- Управление модулями - Управление модулями
- DLEInfo (юридическая информация) - DLEInfo (юридическая информация)
@@ -141,7 +152,7 @@ DLE (Digital Legal Entity) = Универсальная цифровая юри
``` ```
- Только токен-холдеры управляют - Только токен-холдеры управляют
- Проверка баланса при каждой операции - Проверка баланса при каждой операции
- Кворум мультиподписей - Кворум голосов - все решения через коллективное голосование
- Синхронизация между цепочками - Синхронизация между цепочками
``` ```
@@ -174,7 +185,7 @@ DLE.sol (Основной контракт) + Модули (добавляемы
1. **Один основной контракт** - управление токенами, голосованием, мультиподписью 1. **Один основной контракт** - управление токенами, голосованием, мультиподписью
2. **Модули** - специализированные функции (казначейство, иерархическое голосование, коммуникации) 2. **Модули** - специализированные функции (казначейство, иерархическое голосование, коммуникации)
3. **Только токен-холдеры** - никаких админских ролей 3. **Только токен-холдеры** - никаких админских ролей
4. **Кворум мультиподписей** - все решения через коллективное голосование 4. **Кворум голосов** - все решения через коллективное голосование
5. **Проверка баланса** - при каждой операции 5. **Проверка баланса** - при каждой операции
--- ---
@@ -190,7 +201,7 @@ DLE.sol
├── Выбор цепочки для кворума (governanceChainId) ├── Выбор цепочки для кворума (governanceChainId)
├── Синхронизация голосов между цепочками ├── Синхронизация голосов между цепочками
├── Поддержка множественных цепочек ├── Поддержка множественных цепочек
├── Мультиподпись (через токен-холдеров) ├── Голосование токенхолдеров
├── Мультичейн синхронизация ├── Мультичейн синхронизация
└── Система модулей (добавление/управление) └── Система модулей (добавление/управление)
``` ```
@@ -216,7 +227,7 @@ DLE.sol
- **Выбор цепочки для кворума** - токен-холдер может выбрать любую поддерживаемую цепочку - **Выбор цепочки для кворума** - токен-холдер может выбрать любую поддерживаемую цепочку
- **Синхронизация голосов** - после голосования результаты синхронизируются между цепочками - **Синхронизация голосов** - после голосования результаты синхронизируются между цепочками
- **Поддержка множественных цепочек** - Ethereum, Polygon, BSC и др. - **Поддержка множественных цепочек** - Ethereum, Polygon, BSC и др.
- **Мультиподпись** - через токен-холдеров с проверкой баланса - **Голосование** - через токенхолдеров с проверкой баланса
- **Мультичейн синхронизация** - одинаковый адрес во всех цепочках - **Мультичейн синхронизация** - одинаковый адрес во всех цепочках
- **Управление модулями** - добавление/удаление через голосование - **Управление модулями** - добавление/удаление через голосование
@@ -241,14 +252,14 @@ DLE.sol
### 5. ExternalDLEModule.sol ✅ ### 5. ExternalDLEModule.sol ✅
- **Меж-DLE взаимодействие** - управление DLE B через приложение DLE A - **Меж-DLE взаимодействие** - управление DLE B через приложение DLE A
- **Встраивание интерфейсов** - безопасное управление - **Встраивание интерфейсов** - безопасное управление
- **Проверка прав** - через мультиподпись - **Проверка прав** - через голосование токен‑холдеров
- **Аудит действий** - отслеживание операций - **Аудит действий** - отслеживание операций
### 6. Мульти-чейн архитектура ✅ ### 6. Мульти-чейн архитектура ✅
- **CREATE2 деплой** - одинаковый адрес во всех цепочках - **CREATE2 деплой** - одинаковый адрес во всех цепочках
- **Синхронизация состояния** - токены, предложения, голосования - **Синхронизация состояния** - токены, предложения, голосования
- **Создание предложений** - в любой цепочке - **Создание предложений** - в любой цепочке
- **Голосование** - в любой цепочке с синхронизацией - **Голосование** - через токен‑холдеров с проверкой баланса
--- ---
@@ -257,7 +268,7 @@ DLE.sol
### Основные принципы безопасности: ### Основные принципы безопасности:
1. **Только токен-холдеры** - никаких админских ролей 1. **Только токен-холдеры** - никаких админских ролей
2. **Проверка баланса** - при каждой операции 2. **Проверка баланса** - при каждой операции
3. **Кворум мультиподписей** - все решения коллективные 3. **Кворум голосов** - все решения коллективные
4. **Простая логика** - минимум уязвимостей 4. **Простая логика** - минимум уязвимостей
### Защита от атак: ### Защита от атак:
@@ -503,13 +514,10 @@ function createProposal(
uint256 _governanceChainId uint256 _governanceChainId
) external returns (uint256); ) external returns (uint256);
// Синхронизация голосов между цепочками // Исполнение в целевых сетях по EIP-712 подписям (без мостов)
function syncVoteFromChain( function executeProposalBySignatures(
uint256 _proposalId, uint256 proposalId,
uint256 _fromChainId, bytes[] calldata signatures
uint256 _forVotes,
uint256 _againstVotes,
bytes memory _proof
) external; ) external;
// Проверка поддерживаемых цепочек // Проверка поддерживаемых цепочек
@@ -517,29 +525,8 @@ function isChainSupported(uint256 _chainId) external view returns (bool);
``` ```
### Синхронизация между цепочками ### Синхронизация между цепочками
```solidity - Результаты голосования фиксируются снапшотами ERC20Votes в governanceсети.
// Синхронизация токенов - Целевые сети принимают исполнение при верификации EIP712 подписей холдеров и кворума на зафиксированном timepoint.
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;
```
--- ---
@@ -940,7 +927,7 @@ contract DLE is ERC20, ReentrancyGuard {
### ✅ Безопасность ### ✅ Безопасность
- Никаких админских ролей - Никаких админских ролей
- Простая логика мультиподписи - Простая логика коллективного голосования
- Защита от основных атак - Защита от основных атак
- Прозрачность всех операций - Прозрачность всех операций
@@ -962,7 +949,7 @@ contract DLE is ERC20, ReentrancyGuard {
**DLE - это единый смарт-контракт с модульной архитектурой, который:** **DLE - это единый смарт-контракт с модульной архитектурой, который:**
1. **Управляется только токен-холдерами** через кворум мультиподписей 1. **Управляется только токенхолдерами** через кворум голосов
2. **Проверяет баланс токенов** при каждой операции 2. **Проверяет баланс токенов** при каждой операции
3. **Использует модули** для специализированных функций 3. **Использует модули** для специализированных функций
4. **Синхронизируется между цепочками** с одинаковым адресом 4. **Синхронизируется между цепочками** с одинаковым адресом

View File

@@ -16,7 +16,7 @@
### Архитектурные требования ### Архитектурные требования
- **Single-Chain Governance**: Голосование происходит только в одной выбранной сети - **Single-Chain Governance**: Голосование происходит только в одной выбранной сети
- **Мультиподпись токен-холдеров**: Все операции требуют кворума подписей - **Кворум голосов токенхолдеров**: Все операции требуют достижения кворума голосующей силы по снапшотам
- **Настраиваемые таймлоки**: Инициатор устанавливает задержку для каждого предложения - **Настраиваемые таймлоки**: Инициатор устанавливает задержку для каждого предложения
- **Cross-chain исполнение**: Решения выполняются во всех целевых сетях - **Cross-chain исполнение**: Решения выполняются во всех целевых сетях
- **Без админских ролей**: Только коллективное управление через токен-холдеров - **Без админских ролей**: Только коллективное управление через токен-холдеров
@@ -24,11 +24,23 @@
### Технический стек ### Технический стек
- **Frontend**: Vue.js 3 + Composition API - **Frontend**: Vue.js 3 + Composition API
- **Web3**: ethers.js или web3.js - **Web3**: ethers.js или web3.js
- **Контракты**: Solidity + OpenZeppelin + ERC-4337 - **Контракты**: Solidity + OpenZeppelin (ERC4337 опционально для кошельков/UX)
- **Стили**: Scoped CSS с переменными - **Стили**: 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. БЛОК "ПРЕДЛОЖЕНИЯ" (`/management/proposals`)
### Задача 1.1: Создание предложений ### Задача 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
### 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 токены (голосующая сила) ├── ERC-20 токены (голосующая сила)
├── Настраиваемый кворум (% от общего количества токенов) ├── Настраиваемый кворум (% от общего количества токенов)
├── Система голосования (проверка баланса токенов) ├── Система голосования (проверка баланса токенов)
├── Мультиподпись (через токен-холдеров) ├── Голосование токенхолдеров
├── Модули (добавляемые через голосование) ├── Модули (добавляемые через голосование)
├── Мультичейн синхронизация ├── Мультичейн синхронизация
└── Полное управление данными DLE через кворум └── Полное управление данными DLE через кворум
@@ -70,13 +126,12 @@ DLE.sol (Один контракт)
- **Изменение процента кворума** через кворум - **Изменение процента кворума** через кворум
- **Изменение текущей цепочки** через кворум - **Изменение текущей цепочки** через кворум
#### 5. Мультиподпись через токен-холдеров #### 5. Голосование токенхолдеров
- **Описание**: Система подписей для критических операций - **Описание**: Критические операции подтверждаются голосованием держателей токенов
- **Функции**: - **Функции**:
- Подписание операций токен-холдерами - Подача голосов за/против с учетом голосующей силы
- Проверка баланса токенов при подписи - Подсчет голосов по снапшотам `ERC20Votes`
- Сбор подписей до достижения кворума - Исполнение операций после достижения кворума
- Выполнение операций после сбора подписей
#### 6. Казначейские функции #### 6. Казначейские функции
- **Описание**: Управление финансами DLE через голосование - **Описание**: Управление финансами DLE через голосование
@@ -110,7 +165,7 @@ DLE может владеть токенами других DLE и участв
#### Механизм работы #### Механизм работы
1. **DLE A** владеет токенами **DLE B** 1. **DLE A** владеет токенами **DLE B**
2. **Голос DLE A** в **DLE B** прямо пропорционален количеству токенов **DLE B** на балансе **DLE A** 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** как единое целое 4. После достижения кворума подписей **DLE A** может голосовать в **DLE B** как единое целое
### Новые возможности изменения данных DLE ✅ ### Новые возможности изменения данных DLE ✅
@@ -165,7 +220,7 @@ event CurrentChainIdUpdated(uint256 oldChainId, uint256 newChainId);
#### 4. Синхронизация #### 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** 1. **DLE A** владеет токенами **DLE B**
2. **Голос DLE A** в **DLE B** прямо пропорционален количеству токенов **DLE B** на балансе **DLE A** 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** как единое целое 4. После достижения кворума подписей **DLE A** может голосовать в **DLE B** как единое целое
#### Пример #### Пример
@@ -228,8 +283,8 @@ DLE может владеть токенами других DLE и участв
- **DLE B** получает от **DLE A** подпись на **10% голосов** - **DLE B** получает от **DLE A** подпись на **10% голосов**
#### Технические требования #### Технические требования
- Система сбора мультиподписей внутри DLE для внешнего голосования - Система сбора голосов внутри DLE для внешнего голосования
- Проверка кворума подписей перед активацией голоса DLE - Проверка прав через голосование
- Прямо пропорциональный подсчет голосов по количеству токенов - Прямо пропорциональный подсчет голосов по количеству токенов
- Интерфейсы для взаимодействия между DLE - Интерфейсы для взаимодействия между DLE
@@ -272,11 +327,11 @@ function DLEBManagementInterface({ dleBAddress }) {
- URL: `http://localhost:5173/dle-management` - URL: `http://localhost:5173/dle-management`
- Встраивание компонентов управления DLE B - Встраивание компонентов управления DLE B
- Безопасное подписание транзакций для DLE B - Безопасное подписание транзакций для DLE B
- Проверка прав через мультиподпись - Проверка прав через голосование
### Технические требования ### Технические требования
- Один адрес = универсальная точка входа - Один адрес = универсальная точка входа
- Безопасность мультиподписи через токен-холдеров - Безопасность коллективного голосования токенхолдеров по снапшотам
- Масштабируемость через модули - Масштабируемость через модули
- Поддержка аудио/видео коммуникации - Поддержка аудио/видео коммуникации
- Совместимость с существующими стандартами (ERC-20, ERC-721) - Совместимость с существующими стандартами (ERC-20, ERC-721)
@@ -299,12 +354,12 @@ DLE должен функционировать в нескольких блок
#### 2. Синхронные токены управления #### 2. Синхронные токены управления
- Одинаковое количество токенов для каждого партнера во всех сетях - Одинаковое количество токенов для каждого партнера во всех сетях
- Синхронизация операций с токенами между всеми развернутыми сетями - Синхронизация операций с токенами между всеми развернутыми сетями
- Все операции с токенами только через мультиподпись и кворум - Все операции с токенами только через кворум голосов
- Защита от double-spending и рассинхронизации - Защита от double-spending и рассинхронизации
#### 3. Single-Chain Governance система #### 3. Single-Chain Governance система
- Инициатор предложения выбирает ОДНУ сеть для голосования - Инициатор предложения выбирает ОДНУ сеть для голосования
- Все токен-холдеры участвуют в мультиподписи только в выбранной сети - Все токен-холдеры участвуют в голосовании только в выбранной сети
- Инициатор устанавливает таймлок для предложения - Инициатор устанавливает таймлок для предложения
- Проверка балансов токен-холдеров при подписании - Проверка балансов токен-холдеров при подписании
- Исполнение решения происходит во всех целевых сетях - Исполнение решения происходит во всех целевых сетях
@@ -408,7 +463,7 @@ contract DLE_SingleChainGovernance {
ERC-4337 предоставляет стандартную инфраструктуру для смарт-контракт кошельков с универсальностью (один адрес во всех цепочках) и готовыми решениями для оптимизации газа. ERC-4337 предоставляет стандартную инфраструктуру для смарт-контракт кошельков с универсальностью (один адрес во всех цепочках) и готовыми решениями для оптимизации газа.
#### Компоненты ERC-4337 #### Компоненты ERC-4337
- **Smart Contract Wallets** - встроенная мультиподпись - **Smart Contract Wallets** — инфраструктура аккаунтов (опционально для UX)
- **Bundlers** - оптимизация газа через агрегацию транзакций - **Bundlers** - оптимизация газа через агрегацию транзакций
- **Paymasters** - гибкая оплата транзакций - **Paymasters** - гибкая оплата транзакций
- **Account Abstraction** - универсальность и стандартизация - **Account Abstraction** - универсальность и стандартизация
@@ -449,7 +504,7 @@ ERC-4337 распространяется под лицензией **CC0** (Pub
-**ERC-20** - токены управления -**ERC-20** - токены управления
-**Governance** - система голосования -**Governance** - система голосования
-**Access Control** - роли и разрешения -**Access Control** - роли и разрешения
-**Multisig** - мультиподпись (устарело) Multisig — используем голосование токен‑холдеров (ERC20Votes)
-**Timelock** - задержки выполнения -**Timelock** - задержки выполнения
### 2. **ERC-4337** (аудит: Trail of Bits) ### 2. **ERC-4337** (аудит: Trail of Bits)
@@ -475,7 +530,7 @@ contract DLE is ERC20, Governor, TimelockController {
### **Компоненты для интеграции:** ### **Компоненты для интеграции:**
- **ERC-20** - токен управления DLE - **ERC-20** - токен управления DLE
- **Governor** - система голосования с мультиподписью - **Governor** - система голосования
- **TimelockController** - настраиваемые таймлоки - **TimelockController** - настраиваемые таймлоки
- **Account Abstraction** - универсальность адреса - **Account Abstraction** - универсальность адреса
@@ -493,10 +548,13 @@ contract DLE is ERC20, Governor, TimelockController {
### ✅ **Надежность** ### ✅ **Надежность**
- Временем проверенные решения - Временем проверенные решения
- Простая логика мультиподписи токен-холдеров - Простая логика коллективного голосования токенхолдеров
- Понятные механизмы таймлоков - Понятные механизмы таймлоков
### ✅ **Совместимость** ### ✅ **Совместимость**
- Стандартные интерфейсы Ethereum - Стандартные интерфейсы Ethereum
- Совместимость с существующими кошельками - Совместимость с существующими кошельками
- Легкая интеграция с DeFi протоколами - Легкая интеграция с DeFi протоколами
### Примечание про ERC-4337 (опционально)
- Может использоваться в кошельках/окружении для UX (userOps), но не является частью ядра DLE v2.

View File

@@ -85,7 +85,7 @@ export async function createProposal(dleAddress, proposalData) {
// ABI для создания предложения // ABI для создания предложения
const dleAbi = [ 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); const dle = new ethers.Contract(dleAddress, dleAbi, signer);
@@ -95,7 +95,9 @@ export async function createProposal(dleAddress, proposalData) {
proposalData.description, proposalData.description,
proposalData.duration, proposalData.duration,
proposalData.operation, proposalData.operation,
proposalData.governanceChainId proposalData.governanceChainId,
proposalData.targetChains || [],
proposalData.timelockDelay || 0
); );
// Ждем подтверждения транзакции // Ждем подтверждения транзакции

View File

@@ -683,6 +683,61 @@
<div class="preview-item"> <div class="preview-item">
<strong>💰 Общая стоимость:</strong> ~${{ totalDeployCost.toFixed(2) }} <strong>💰 Общая стоимость:</strong> ~${{ totalDeployCost.toFixed(2) }}
</div> </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> </div>
<!-- Приватный ключ --> <!-- Приватный ключ -->
@@ -903,6 +958,13 @@ const availableNetworks = ref([]);
const isLoadingNetworks = ref(false); const isLoadingNetworks = ref(false);
const totalDeployCost = ref(0); const totalDeployCost = ref(0);
const predictedAddress = ref(''); 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); const useSameKeyForAllChains = ref(true);
@@ -958,6 +1020,61 @@ const hasSelectedNetworks = computed(() => {
return selectedNetworks.value.length > 0; 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(() => { const selectedTokenStandardInfo = computed(() => {
return tokenStandardsData[dleSettings.tokenStandard] || null; return tokenStandardsData[dleSettings.tokenStandard] || null;
@@ -2306,7 +2423,8 @@ const deploySmartContracts = async () => {
currentChainId: dleSettings.selectedNetworks[0] || 1, currentChainId: dleSettings.selectedNetworks[0] || 1,
// Приватный ключ для деплоя // Приватный ключ для деплоя
privateKey: unifiedPrivateKey.value privateKey: unifiedPrivateKey.value,
explorerApiKeys: explorerApiKeys
}; };
console.log('Данные для деплоя DLE:', deployData); console.log('Данные для деплоя DLE:', deployData);
@@ -2333,6 +2451,12 @@ const deploySmartContracts = async () => {
// Перенаправляем на главную страницу управления // Перенаправляем на главную страницу управления
router.push('/management'); router.push('/management');
}, 2000); }, 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 { } else {
showDeployProgress.value = false; showDeployProgress.value = false;
@@ -2375,6 +2499,15 @@ const validateCoordinates = (coordinates) => {
</script> </script>
<style scoped> <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 { .settings-panel {
padding: var(--block-padding); padding: var(--block-padding);
background-color: var(--color-light); background-color: var(--color-light);

View File

@@ -214,6 +214,15 @@
</div> </div>
</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"> <div class="form-section">
<h5>🔗 Выбор цепочки для кворума</h5> <h5>🔗 Выбор цепочки для кворума</h5>
@@ -239,7 +248,23 @@
</div> </div>
</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"> <div class="form-section">
<h5> Тип операции</h5> <h5> Тип операции</h5>
@@ -458,7 +483,22 @@
</div> </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 class="form-section"> <div class="form-section">
<h5>👁 Предварительный просмотр</h5> <h5>👁 Предварительный просмотр</h5>
<div class="preview-card"> <div class="preview-card">
@@ -480,21 +520,6 @@
</div> </div>
</div> </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 для авторизованных пользователей --> </div> <!-- Закрываем div для авторизованных пользователей -->
</div> </div>
@@ -507,6 +532,11 @@ import { useRouter, useRoute } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth'; import { useAuthContext } from '@/composables/useAuth';
import BaseLayout from '../../components/BaseLayout.vue'; import BaseLayout from '../../components/BaseLayout.vue';
import { getDLEInfo, loadProposals, createProposal as createProposalAPI, voteForProposal as voteForProposalAPI, executeProposal as executeProposalAPI, getSupportedChains } from '../../utils/dle-contract.js'; 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 wsClient from '../../utils/websocket.js';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
@@ -552,6 +582,8 @@ const newProposal = ref({
description: '', description: '',
duration: 7, duration: 7,
governanceChainId: null, governanceChainId: null,
timelockHours: 0,
targetChains: [],
operationType: '', operationType: '',
operationParams: { operationParams: {
to: '', to: '',
@@ -584,6 +616,7 @@ const isFormValid = computed(() => {
newProposal.value.duration > 0 && newProposal.value.duration > 0 &&
newProposal.value.governanceChainId && newProposal.value.governanceChainId &&
newProposal.value.operationType && newProposal.value.operationType &&
newProposal.value.timelockHours >= 0 &&
validateOperationParams() validateOperationParams()
); );
}); });
@@ -981,7 +1014,9 @@ async function createProposal() {
description: newProposal.value.description, description: newProposal.value.description,
duration: newProposal.value.duration * 24 * 60 * 60, // конвертируем в секунды duration: newProposal.value.duration * 24 * 60 * 60, // конвертируем в секунды
operation: operation, 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); console.log('Предложение создано:', result);