# Мультичейн-управление переводом токенов DLE ## Обзор системы Система мультичейн-управления DLE позволяет холдерам токенов создавать предложения по переводу токенов со своего кошелька на другой адрес (или в казну) через процесс голосования во всех сетях, где развернут контракт DLE. Каждая сеть имеет независимый кворум, но предложения координируются и отображаются как единое целое. ## Архитектура ### Мультичейн-контракты DLE - Один DLE может быть развернут в нескольких блокчейн-сетях (например, Sepolia, Arbitrum Sepolia, Base Sepolia) - Каждый контракт DLE в каждой сети работает независимо - Предложения создаются, голосуются и выполняются в каждой сети отдельно - ID предложений уникальны для каждой сети (предложение с ID=1 в Sepolia и предложение с ID=1 в Arbitrum Sepolia - это разные предложения) ### Группировка предложений - Предложения с одинаковым описанием и инициатором группируются в одну карточку - Карточка отображает статус предложения во всех сетях DLE - Каждая сеть в карточке имеет свой собственный ID предложения, состояние и результаты голосования ## Процесс перевода токенов ### Этап 1: Создание предложения #### Описание процесса 1. Пользователь заполняет форму перевода токенов: - **Описание предложения** - текстовое описание цели перевода - **Адрес получателя** - адрес кошелька или казны, на который будут переведены токены - **Количество токенов** - количество DLE токенов для перевода - **Длительность голосования** - период времени, в течение которого можно голосовать - **Ваш подключенный кошелек** - автоматически заполняется адресом подключенного кошелька (токены будут отправлены с этого адреса) 2. Система определяет все сети, где развернут контракт DLE 3. **Последовательное создание предложений в каждой сети:** - Для каждой сети DLE: - Переключение MetaMask на соответствующую сеть - Задержка 1 секунда после переключения - Создание предложения в контракте DLE этой сети - Получение уникального ID предложения для этой сети - Задержка 3 секунды после подтверждения транзакции (5 секунд для Base Sepolia) - При ошибках RPC выполняется автоматический retry с экспоненциальной задержкой (до 3 попыток) 4. **Подписи в MetaMask:** - Пользователь должен подписать транзакцию создания предложения в каждой сети DLE - Количество подписей = количество сетей DLE - Каждая подпись создает отдельное предложение в соответствующей сети #### Технические детали - **Функция контракта:** `createProposal(description, duration, operation, targetChains, timelockDelay)` - **Порядок параметров:** `description`, `duration`, `operation`, `targetChains` (массив), `timelockDelay` - **targetChains:** Массив ID сетей, где будет выполнена операция (обычно `[chainId]` для текущей сети) - **Операция:** `_transferTokens(sender, recipient, amount)` - где `sender` = адрес инициатора предложения - **Сигнатура:** `_transferTokens(address,address,uint256)` - **все три параметра обязательны!** - `sender` получается автоматически из `signer.getAddress()` при создании предложения - **ID предложения:** Генерируется автоматически контрактом в каждой сети (начинается с 0, инкрементируется) - **Группировка:** Предложения с одинаковым `description` и `initiator` группируются в одну карточку #### Кодирование операции Операция для выполнения должна быть закодирована в формате ABI (Application Binary Interface) перед передачей в `createProposal`. **Для операции перевода токенов `_transferTokens(address,address,uint256)`:** 1. **Сигнатура функции:** `_transferTokens(address,address,uint256)` 2. **Селектор функции:** Первые 4 байта от `keccak256(signature)` 3. **Параметры:** - `sender` - адрес отправителя (инициатора предложения) - `recipient` - адрес получателя токенов - `amount` - количество токенов (в wei, т.е. количество * 10^18) **Пример кодирования (JavaScript/ethers.js):** ```javascript // Способ 1: Использование Interface (рекомендуется) const functionSignature = '_transferTokens(address,address,uint256)'; const iface = new ethers.Interface([`function ${functionSignature}`]); const encodedOperation = iface.encodeFunctionData('_transferTokens', [ senderAddress, // адрес инициатора recipientAddress, // адрес получателя ethers.parseUnits(amount.toString(), 18) // количество в wei ]); // Способ 2: Ручное кодирование const functionSignature = '_transferTokens(address,address,uint256)'; const selectorBytes = ethers.keccak256(ethers.toUtf8Bytes(functionSignature)); const selector = '0x' + selectorBytes.slice(2, 10); // первые 4 байта const abiCoder = ethers.AbiCoder.defaultAbiCoder(); const encodedParams = abiCoder.encode( ['address', 'address', 'uint256'], [senderAddress, recipientAddress, ethers.parseUnits(amount.toString(), 18)] ); const encodedOperation = ethers.concat([selector, encodedParams]); ``` **Важные моменты:** - `sender` должен совпадать с адресом инициатора предложения (проверяется в контракте при выполнении) - `amount` **ОБЯЗАТЕЛЬНО** передается в wei (1 токен = 10^18 wei) - используйте `ethers.parseUnits(amount.toString(), 18)` - **КРИТИЧЕСКИ ВАЖНО:** `sender` должен определяться из `signer.getAddress()` при создании предложения в каждой сети отдельно, а не один раз до цикла - Операция кодируется для каждой сети отдельно с актуальным адресом signer для этой сети - Контракт декодирует операцию при выполнении и проверяет соответствие `sender` и `initiator` **КРИТИЧЕСКИ ВАЖНО - Правильная реализация:** ```javascript // ✅ ПРАВИЛЬНО: Кодирование внутри цикла с актуальным адресом signer для каждой сети async function createProposalsInAllChains(allChains, formData) { const results = []; for (let index = 0; index < allChains.length; index++) { const chainId = allChains[index]; // 1. Переключаемся на нужную сеть await switchToVotingNetwork(chainId.toString()); await new Promise(resolve => setTimeout(resolve, 1000)); // Задержка после переключения // 2. КРИТИЧЕСКИ ВАЖНО: Получаем адрес signer для текущей сети const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const senderAddress = await signer.getAddress(); // Адрес инициатора из signer! // 3. Кодируем операцию с актуальным адресом signer для этой сети const transferCallData = encodeTransferTokensCall( senderAddress, // адрес инициатора из signer (обязательно!) formData.recipient, // адрес получателя formData.amount // количество (будет сконвертировано в wei) ); // 4. Создаем предложение const proposalData = { description: formData.description, duration: formData.duration, operation: transferCallData, targetChains: [chainId], timelockDelay: 0 }; await createProposal(contractAddress, proposalData); } } function encodeTransferTokensCall(sender, recipient, amount) { const functionSignature = '_transferTokens(address,address,uint256)'; const iface = new ethers.Interface([`function ${functionSignature}`]); // КРИТИЧЕСКИ ВАЖНО: конвертируем amount в wei const amountInWei = ethers.parseUnits(amount.toString(), 18); const encodedCall = iface.encodeFunctionData('_transferTokens', [ sender, // адрес инициатора (обязательно! должен быть из signer.getAddress()) recipient, // адрес получателя amountInWei // количество в wei (обязательно!) ]); return encodedCall; } // ❌ НЕПРАВИЛЬНО: Кодирование один раз до цикла // const transferCallData = encodeTransferTokensCall(formData.sender, ...); // НЕПРАВИЛЬНО! // for (const chainId of allChains) { // await createProposal(contractAddress, { operation: transferCallData, ... }); // sender может не совпасть! // } // ❌ НЕПРАВИЛЬНО: Отсутствует sender или amount не в wei // const transferFunctionSelector = ethers.id("_transferTokens(address,uint256)"); // НЕПРАВИЛЬНО! // const amount = transferData.amount; // НЕПРАВИЛЬНО! Нужна конвертация в wei ``` **Другие поддерживаемые операции:** - `_addModule(bytes32,address)` - добавление модуля - `_removeModule(bytes32)` - удаление модуля - `_addSupportedChain(uint256)` - добавление поддерживаемой сети - `_removeSupportedChain(uint256)` - удаление поддерживаемой сети - `_updateVotingDurations(uint256,uint256)` - обновление времени голосования - `_updateQuorumPercentage(uint256)` - обновление процента кворума - `_updateDLEInfo(...)` - обновление информации DLE - `_setLogoURI(string)` - обновление URI логотипа #### Результат - Создано N предложений (по одному в каждой сети DLE) - Каждое предложение имеет уникальный ID в своей сети - Все предложения отображаются как одна карточка в интерфейсе - Карточка показывает статус предложения в каждой сети --- ### Этап 2: Голосование #### Описание процесса 1. Пользователь видит карточку предложения с информацией о всех сетях DLE 2. Пользователь выбирает голос "За" или "Против" 3. **Последовательное голосование во всех активных сетях:** - Система определяет все активные цепочки (state === 0 или 'active', не выполнены, не отменены) - Для каждой активной сети: - Переключение MetaMask на соответствующую сеть - Задержка 1 секунда после переключения - **Проверка баланса токенов в этой сети** (балансы могут отличаться в разных сетях) - Если баланс отсутствует, сеть пропускается с предупреждением - Голосование продолжается в других сетях - Голосование с использованием **уникального ID предложения для этой сети** - Задержка 3 секунды после подтверждения транзакции (5 секунд для Base Sepolia) 4. **Подписи в MetaMask:** - Пользователь должен подписать транзакцию голосования в каждой активной сети DLE - Количество подписей = количество активных сетей DLE - Каждая подпись регистрирует голос в соответствующей сети #### Технические детали - **Функция контракта:** `vote(proposalId, support)` - где `proposalId` уникален для каждой сети - **Проверка ID:** Система использует `chain.id` (ID предложения из конкретной сети), а не общий ID группы - **Проверка баланса:** Баланс токенов проверяется **в каждой сети отдельно** перед голосованием - В мультичейн-системе балансы могут отличаться в разных сетях - Если в сети нет токенов, голосование в этой сети пропускается - Контракт также проверяет баланс через `getPastVotes()` и вернет ошибку, если токенов нет - **Вес голоса:** Зависит от баланса токенов голосующего в соответствующей сети - **Независимые кворумы:** Каждая сеть имеет свой собственный кворум #### Результат - Голос зарегистрирован во всех активных сетях DLE - Каждая сеть обновляет свои счетчики голосов (forVotes, againstVotes) - Карточка предложения обновляется с новыми данными голосования --- ### Этап 3: Выполнение предложения #### Условия выполнения **КРИТИЧЕСКИ ВАЖНО:** Предложение может быть выполнено только при условии, что кворум достигнут **во всех сетях DLE**, где предложение активно. Условия для каждой сети: - Состояние предложения: `ReadyForExecution` (state === 5) - Кворум достигнут: `forVotes >= quorumRequired` - Большинство голосов "За": `forVotes > againstVotes` - Предложение не выполнено: `executed === false` - Предложение не отменено: `canceled === false` - Истек период голосования (если применимо) - Истек период timelock (если применимо) #### Описание процесса 1. Система проверяет, что предложение готово к выполнению во всех активных сетях DLE 2. **Последовательное выполнение во всех готовых сетях:** - Для каждой сети, где предложение готово к выполнению: - Переключение MetaMask на соответствующую сеть - Задержка 1 секунда после переключения - Выполнение предложения с использованием **уникального ID предложения для этой сети** - Задержка 3 секунды после подтверждения транзакции (5 секунд для Base Sepolia) 3. **Подписи в MetaMask:** - Пользователь должен подписать транзакцию выполнения в каждой готовой сети DLE - Количество подписей = количество готовых сетей DLE - Каждая подпись выполняет перевод токенов в соответствующей сети #### Технические детали - **Функция контракта:** `executeProposal(proposalId)` - где `proposalId` уникален для каждой сети - **Операция перевода:** `_transferTokens(sender, recipient, amount)` - `sender` = адрес инициатора предложения (проверяется в контракте) - `recipient` = адрес получателя из предложения - `amount` = количество токенов из предложения - **Проверка безопасности:** Контракт проверяет, что `sender` совпадает с `initiator` предложения - **Перевод токенов:** Токены переводятся с кошелька инициатора, а не с баланса контракта #### Результат - Перевод токенов выполнен во всех готовых сетях DLE - Каждая сеть независимо выполняет перевод с кошелька инициатора - Карточка предложения обновляется, показывая статус "Выполнено" во всех сетях --- ## Отмена предложения ### Условия отмены - Только инициатор предложения может отменить его - Предложение должно быть активным (не выполнено, не отменено) - Отмена возможна в любой момент до выполнения ### Процесс отмены 1. **Последовательная отмена во всех активных сетях:** - Для каждой активной сети: - Переключение MetaMask на соответствующую сеть - Задержка 1 секунда после переключения - Отмена предложения с использованием **уникального ID предложения для этой сети** - Задержка 3 секунды после подтверждения транзакции 2. **Подписи в MetaMask:** - Пользователь должен подписать транзакцию отмены в каждой активной сети DLE - Количество подписей = количество активных сетей DLE ### Технические детали - **Функция контракта:** `cancelProposal(proposalId, reason)` - **Проверка прав:** Контракт проверяет, что вызывающий является инициатором предложения --- ## Важные особенности системы ### 1. Уникальность ID предложений - **Каждая сеть имеет свой собственный счетчик предложений** - Предложение с ID=1 в Sepolia и предложение с ID=1 в Arbitrum Sepolia - это **разные предложения** - При группировке система сохраняет ID из каждой сети отдельно - При голосовании/выполнении/отмене используется правильный ID для каждой сети ### 2. Группировка предложений - Предложения группируются по ключу: `${description}_${initiator}` - Одна карточка = одно логическое предложение во всех сетях - Карточка отображает: - Общее описание - Инициатора - Список сетей с их статусами - Результаты голосования по каждой сети - Общий статус (активно/выполнено/отменено) ### 3. Независимые кворумы - **Каждая сеть имеет свой собственный кворум** - Кворум рассчитывается на основе общего предложения токенов в соответствующей сети - Голосование в одной сети не влияет на кворум в другой сети - Для выполнения предложения кворум должен быть достигнут **во всех сетях** ### 4. Последовательное выполнение операций - Все операции (создание, голосование, выполнение, отмена) выполняются **последовательно**, а не параллельно - Это необходимо, так как MetaMask может работать только с одной сетью одновременно - Между операциями есть задержки для стабилизации MetaMask - **КРИТИЧЕСКИ ВАЖНО:** Использование `Promise.all` для параллельного выполнения недопустимо и приведет к ошибкам ### 5. Обработка ошибок - **Retry для временных ошибок RPC:** - Автоматический retry до 3 попыток - Экспоненциальная задержка (2s, 4s, 8s) - Только для retryable ошибок (Internal JSON-RPC error, rate limiting) - **Ошибки в отдельных сетях:** - Если операция не удалась в одной сети, процесс продолжается для других сетей - Пользователь получает сводку успешных и неудачных операций - **Обработка отсутствия баланса:** - Перед голосованием в каждой сети проверяется баланс токенов - Если в сети нет токенов, голосование в этой сети пропускается с предупреждением - Голосование продолжается в других сетях, где баланс есть - Контракт также проверяет баланс и вернет ошибку `ErrNoPower`, если токенов нет ### 6. Безопасность - **Проверка инициатора:** При выполнении контракт проверяет, что `sender` совпадает с `initiator` - **Проверка баланса:** Перед голосованием проверяется наличие токенов **в каждой сети отдельно** - Балансы могут отличаться в разных сетях - Если в сети нет токенов, голосование в этой сети пропускается - Контракт также проверяет баланс через `getPastVotes()` и вернет ошибку `ErrNoPower`, если токенов нет - **Проверка состояния:** Перед каждой операцией проверяется актуальное состояние предложения - **Валидация данных:** Все данные предложения валидируются перед отправкой в контракт ### 7. Перевод токенов - **Источник токенов:** Токены переводятся с кошелька инициатора предложения, а не с баланса контракта - **Получатель:** Может быть любой адрес (кошелек или казна) - **Количество:** Указывается инициатором при создании предложения - **Проверка баланса:** Контракт проверяет достаточность баланса инициатора перед выполнением --- ## Пользовательский интерфейс ### Карточка предложения - **Заголовок:** Описание предложения - **Инициатор:** Адрес создателя предложения - **Список сетей:** - Название сети - Статус (Активно/Выполнено/Отменено/Истекло) - ID предложения в этой сети - Результаты голосования (За/Против) - Кворум (достигнут/не достигнут) - **Действия:** - Голосовать "За" / "Против" (если активно) - Выполнить (если готово к выполнению) - Отменить (если инициатор) ### Индикаторы статуса - **Активно:** Предложение открыто для голосования - **Готово к выполнению:** Кворум достигнут во всех сетях - **Выполнено:** Перевод токенов выполнен во всех сетях - **Отменено:** Предложение отменено инициатором - **Истекло:** Истек период голосования --- ## Технические детали реализации ### Кодирование операций Подробное описание процесса кодирования операций для создания предложений см. в разделе [Кодирование операции](#кодирование-операции) (Этап 1: Создание предложения). ### Структура данных предложения ```javascript { id: number, // ID группы (из первой сети) description: string, // Описание предложения initiator: address, // Адрес инициатора deadline: number, // Дедлайн голосования chains: [ // Массив данных по каждой сети { id: number, // УНИКАЛЬНЫЙ ID для этой сети chainId: number, // ID сети (11155111, 421614, 84532) networkName: string, // Название сети contractAddress: address, // Адрес контракта DLE в этой сети state: number, // Состояние (0=Active, 3=Executed, 4=Canceled, 5=ReadyForExecution) forVotes: bigint, // Голоса "За" againstVotes: bigint, // Голоса "Против" quorumRequired: bigint, // Требуемый кворум executed: boolean, // Выполнено canceled: boolean, // Отменено transactionHash: string // Хеш транзакции создания } ], createdAt: number, // Время создания uniqueId: string // Уникальный ключ группировки } ``` ### Функции контракта DLE #### createProposal ```solidity function createProposal( string memory _description, uint256 _duration, bytes memory _operation, uint256[] memory _targetChains, uint256 _timelockDelay ) public returns (uint256 proposalId) ``` #### vote ```solidity function vote(uint256 _proposalId, bool _support) public ``` #### executeProposal ```solidity function executeProposal(uint256 _proposalId) public ``` #### cancelProposal ```solidity function cancelProposal(uint256 _proposalId, string memory _reason) public ``` #### _transferTokens (internal) ```solidity function _transferTokens( address _sender, address _recipient, uint256 _amount ) internal ``` --- ## Примеры использования ### Пример 0: Кодирование операции перевода токенов Перед созданием предложения необходимо закодировать операцию. **КРИТИЧЕСКИ ВАЖНО:** Используйте правильную сигнатуру и конвертируйте amount в wei. ```javascript import { ethers } from 'ethers'; // Получаем адрес инициатора из signer const signer = await provider.getSigner(); const sender = await signer.getAddress(); // Адрес инициатора (обязательно!) // Параметры перевода const recipient = '0x1234567890123456789012345678901234567890'; // Получатель const amount = 100; // 100 токенов (в обычных единицах, не в wei) // Кодирование операции const functionSignature = '_transferTokens(address,address,uint256)'; const iface = new ethers.Interface([`function ${functionSignature}`]); // КРИТИЧЕСКИ ВАЖНО: конвертируем amount в wei const amountInWei = ethers.parseUnits(amount.toString(), 18); // 100 * 10^18 wei const encodedOperation = iface.encodeFunctionData('_transferTokens', [ sender, // адрес инициатора (обязательно!) recipient, // адрес получателя amountInWei // количество в wei (обязательно!) ]); // encodedOperation теперь можно использовать в createProposal // Результат: 0x... (селектор функции + закодированные параметры) // Создание предложения с правильным порядком параметров const tx = await dle.createProposal( "Перевод 100 токенов в казну", // description 86400, // duration (1 день в секундах) encodedOperation, // operation [chainId], // targetChains (массив!) 0 // timelockDelay ); ``` **Результат:** Закодированная операция в формате bytes, готовая для передачи в `createProposal`. **Частые ошибки:** - ❌ Использование `_transferTokens(address,uint256)` - неправильная сигнатура - ❌ Отсутствие параметра `sender` - контракт не сможет проверить инициатора - ❌ Передача `amount` без конвертации в wei - неправильное количество токенов - ❌ Неправильный порядок параметров в `createProposal` - ошибка вызова контракта ### Пример 1: Создание предложения в 3 сетях 1. Пользователь создает предложение "Перевод 100 токенов в казну" 2. Система определяет 3 сети: Sepolia, Arbitrum Sepolia, Base Sepolia 3. Создаются 3 предложения: - Sepolia: ID=5 - Arbitrum Sepolia: ID=3 - Base Sepolia: ID=7 4. Все 3 предложения отображаются как одна карточка ### Пример 2: Голосование 1. Пользователь голосует "За" за предложение 2. Система голосует в 3 сетях: - Sepolia: vote(5, true) - Arbitrum Sepolia: vote(3, true) - Base Sepolia: vote(7, true) 3. Каждое голосование требует отдельной подписи в MetaMask ### Пример 3: Выполнение 1. Кворум достигнут во всех 3 сетях 2. Система выполняет предложение в 3 сетях: - Sepolia: executeProposal(5) - Arbitrum Sepolia: executeProposal(3) - Base Sepolia: executeProposal(7) 3. В каждой сети токены переводятся с кошелька инициатора на адрес получателя --- ## Ограничения и особенности ### Ограничения - MetaMask может работать только с одной сетью одновременно - Операции выполняются последовательно, что может занять время при большом количестве сетей - Ошибки RPC могут потребовать ручного retry ### Особенности - Если предложение не создано в одной из сетей (из-за ошибки), оно все равно может быть создано в других сетях - Голосование возможно только в тех сетях, где предложение активно - Выполнение возможно только если кворум достигнут во всех активных сетях --- ## Безопасность ### Защита от атак - Проверка инициатора при выполнении - Проверка баланса перед переводом - Независимые кворумы в каждой сети - Валидация всех входных данных ### Рекомендации - Всегда проверяйте адрес получателя перед созданием предложения - Убедитесь, что у вас достаточно токенов для перевода - Проверяйте статус предложения перед голосованием/выполнением - Следите за транзакциями в каждой сети --- ## Известные проблемы и исправления ### Исправленные критические ошибки #### 1. Отсутствие конвертации amount в wei **Проблема:** Параметр `amount` передавался без конвертации в wei, что приводило к неправильному количеству токенов при выполнении. **Исправление:** Добавлена обязательная конвертация через `ethers.parseUnits(amount.toString(), 18)` перед кодированием операции. **Файл:** `frontend/src/views/smartcontracts/TransferTokensFormView.vue` **Статус:** ✅ Исправлено #### 2. Неправильная сигнатура функции _transferTokens **Проблема:** Использовалась неправильная сигнатура `_transferTokens(address,uint256)` вместо `_transferTokens(address,address,uint256)`, что приводило к ошибкам декодирования в контракте. **Исправление:** Исправлена сигнатура функции и добавлен обязательный параметр `sender` (адрес инициатора). **Файл:** `frontend/src/utils/dle-contract.js` (функция `createTransferTokensProposal`) **Статус:** ✅ Исправлено #### 3. Неправильный порядок параметров в createProposal **Проблема:** В функцию `createProposal` передавался `governanceChainId` вместо `targetChains` в 4-м параметре, что нарушало сигнатуру контракта. **Исправление:** Исправлен порядок параметров согласно сигнатуре контракта: `description`, `duration`, `operation`, `targetChains`, `timelockDelay`. **Файл:** `frontend/src/utils/dle-contract.js` (функция `createTransferTokensProposal`) **Статус:** ✅ Исправлено #### 4. Неправильное кодирование операции при создании предложений (КРИТИЧЕСКАЯ) **Проблема:** Операция перевода токенов кодировалась один раз до цикла по сетям, используя адрес из `formData.value.sender`. По документации, `sender` должен определяться из `signer.getAddress()` при создании предложения в каждой сети, что гарантирует совпадение с инициатором предложения. **Исправление:** - Кодирование операции перемещено внутрь цикла по сетям - Добавлено получение адреса signer для каждой сети через `await signer.getAddress()` - Добавлена проверка соответствия адреса signer адресу из формы - Операция теперь кодируется с актуальным адресом signer для каждой сети отдельно **Файл:** `frontend/src/views/smartcontracts/TransferTokensFormView.vue` (функция `submitForm`) **Статус:** ✅ Исправлено (2025-01-XX) #### 5. Параллельное выполнение в executeMultichainProposal (КРИТИЧЕСКАЯ) **Проблема:** Функция `executeMultichainProposal` использовала `Promise.all` для параллельного выполнения операций во всех сетях одновременно. Это не работает с MetaMask, который может работать только с одной сетью одновременно. **Исправление:** - Заменен `Promise.all` на последовательный цикл `for` - Добавлено переключение сетей через `switchToVotingNetwork` для каждой сети - Добавлены задержки после переключения сетей (1 секунда) и после подтверждения транзакций (3 секунды, 5 секунд для Base Sepolia) - Добавлена фильтрация только готовых к выполнению цепочек **Файл:** `frontend/src/composables/useProposals.js` (функция `executeMultichainProposal`) **Статус:** ✅ Исправлено (2025-01-XX) #### 6. Отсутствие переключения сетей в voteOnMultichainProposal **Проблема:** Функция `voteOnMultichainProposal` не использовала переключение сетей и проверку баланса токенов, что требовалось по документации для корректной работы в мультичейн-среде. **Исправление:** - Добавлено переключение сетей через `switchToVotingNetwork` для каждой сети - Добавлена проверка баланса токенов перед голосованием в каждой сети через `checkTokenBalance` - Добавлены задержки после переключения сетей (1 секунда) и после подтверждения транзакций (3 секунды, 5 секунд для Base Sepolia) - Добавлена фильтрация только активных цепочек - Добавлена обработка ошибок с пропуском сетей при отсутствии баланса **Файл:** `frontend/src/composables/useProposals.js` (функция `voteOnMultichainProposal`) **Статус:** ✅ Исправлено (2025-01-XX) ### Текущая корректная реализация Все функции кодирования операций и мультичейн-операции теперь используют: - ✅ Правильную сигнатуру: `_transferTokens(address,address,uint256)` - ✅ Все три параметра: `sender`, `recipient`, `amount` - ✅ Конвертацию amount в wei: `ethers.parseUnits(amount.toString(), 18)` - ✅ Правильный порядок параметров в `createProposal` - ✅ Определение `sender` из `signer.getAddress()` при создании предложения в каждой сети - ✅ Последовательное выполнение операций во всех мультичейн-функциях (не параллельное) - ✅ Переключение сетей перед каждой операцией в мультичейн-функциях - ✅ Проверку баланса токенов перед голосованием в каждой сети ### Проверка корректности Перед развертыванием убедитесь, что: 1. Функция `_transferTokens` кодируется с **тремя** параметрами (sender, recipient, amount) 2. `amount` всегда конвертируется в wei перед кодированием 3. `sender` получается из `signer.getAddress()` при создании предложения в каждой сети и совпадает с инициатором предложения 4. Порядок параметров в `createProposal` соответствует сигнатуре контракта 5. Все мультичейн-операции (создание, голосование, выполнение) используют последовательное выполнение с переключением сетей 6. При голосовании проверяется баланс токенов в каждой сети отдельно --- ## Заключение Система мультичейн-управления DLE обеспечивает децентрализованное управление переводом токенов через независимые кворумы в каждой сети, при этом предоставляя единый интерфейс для управления предложениями во всех сетях одновременно. **Важно:** Все критические ошибки в кодировании операций исправлены. Код соответствует документации и контракту.