Архитектура relay-based моста EVM-TVM
Общее описание
Relay-based мост — это мультичейн система моста, обеспечивающая перевод токенов между блокчейнами разных типов: EVM сетями и TVM сетями.
Пользователь отправляет токены на одной стороне, а получает их эквивалент на другой. Подтверждение корректности каждого перевода обеспечивается сетью relay-нод — независимых серверов, которые отслеживают события в обоих блокчейнах.
Ключевые архитектурные особенности
1. Консенсус через Event-контракты в TVM сети
Для каждого трансфера создаётся специальный Event-контракт в TVM сети. Механизм достижения консенсуса различается в зависимости от направления трансфера:
EVM→TVM: Консенсус через подтверждения (confirm)
Relay-ноды вызывают метод confirm() без подписи — только голосование:
- Консенсус достигается путём подсчёта голосов
confirms >= requiredVotes - После подтверждения Event-контракт сразу вызывает callback в Proxy-контракт
- Proxy выполняет финальное действие (mint/unlock токенов) в TVM сети
- Подписи не нужны, т.к. действие происходит внутри TVM
TVM→EVM: Консенсус через подписи (sign)
Relay-ноды вызывают метод confirm() с подписью данных события:
- Консенсус достигается путём накопления криптографических подписей relay-нод
- Event-контракт хранит подписи в маппинге
signatures - Подписи передаются в EVM-контракт
MultiVault.saveWithdraw*() - EVM-контракт верифицирует подписи через публичные ключи relay-нод
- Подписи необходимы, т.к. EVM-контракты не могут читать состояние TVM
Сравнение механизмов
| Направление | Метод confirm | Подпись | Консенсус | Финальное действие |
|---|---|---|---|---|
| EVM→TVM | confirm(voteReceiver) | ❌ Нет | Подсчёт голосов | Callback в Proxy (TVM) |
| TVM→EVM | confirm(signature, voteReceiver) | ✅ Да | Накопление подписей | saveWithdraw*() с верификацией подписей (EVM) |
3. Diamond Pattern (EIP-2535)
MultiVault в EVM использует Diamond pattern — один адрес контракта с множеством модулей (facets):
- MultiVaultFacetDeposit — депозит токенов
- MultiVaultFacetWithdraw — вывод токенов с проверкой подписей
- MultiVaultFacetTokens — реестр токенов (native/alien)
- MultiVaultFacetFees — управление комиссиями
- MultiVaultFacetLiquidity — пулы ликвидности
- MultiVaultFacetSettings — конфигурация
- MultiVaultFacetPendingWithdrawals — очередь ожидающих выводов: rate-limiting по токенам (скользящее окно), одобрение/отклонение governance или withdrawGuardian, bounty-система для стимулирования исполнения, отмена с возвратом в TVM, принудительный вывод (emergency)
4. Двухуровневая API-архитектура
- multivault-graph (
multivault-graph-v2/) — индексирует события EVM-контрактов (Deposit, Withdraw, PendingWithdrawal). Создаётся по одному инстансу на каждую связку EVM-TVM сетей. Две реализации: The Graph (subgraph) и Envio - BridgeAPI (
ton-api/) — индексирует состояние TVM-контрактов (Event-контракты, конфигурации, статусы подтверждений) и забирает EVM-события из multivault-graph. Один инстанс на одну TVM сеть - Aggregator API (
chainconnect-history-api/) — агрегатор над инстансами BridgeAPI, предоставляет единый интерфейс/payload/build,/transfers/search,/transfers/status
Глоссарий
| Термин | Значение |
|---|---|
| Native | Токены изначально выпущенные в TVM сети. Термин используется относительно TVM независимо от направления трансфера |
| Alien | Токены изначально выпущенные в EVM сети. При бриджинге в TVM минтятся как alien-представления |
| Event (Event-контракт) | Смарт-контракт в TVM сети, создаваемый для каждого трансфера. Играет роль «журнала голосования»: relay-ноды вызывают метод confirm(). Когда достигнуто достаточно подтверждений — контракт считается подтверждённым и запускает следующий шаг (mint/unlock токенов) |
| Configuration | Контракт конфигурации события в TVM, определяет параметры создания Event-контрактов и целевые адреса |
| Proxy | Контракт-прокси в TVM (ProxyMultiVaultNative/Alien), управляет минтингом/сжиганием/трансфером токенов |
| MultiVault | Основной контракт моста в EVM (Diamond pattern), хранит залоченные токены и управляет mint/burn |
| relay-нода | Независимый сервер, который отслеживает события в обоих блокчейнах и подтверждает трансферы: для EVM→TVM вызывает confirm(), для TVM→EVM вызывает confirm(signature) |
| confirm | Метод Event-контракта для подтверждения события. Для EVM→TVM: confirm(voteReceiver) — только голос. Для TVM→EVM: confirm(signature, voteReceiver) — голос + подпись |
| signature | Криптографическая подпись relay-ноды, передаваемая в параметре confirm() для TVM→EVM; используется для верификации в EVM-контракте |
Типы трансферов
- EVM → TVM (Alien): lock alien в EVM (MultiVault), mint alien в TVM — перевод EVM-токена в TVM
- TVM → EVM (Alien): burn alien в TVM, unlock alien в EVM — возврат EVM-токена обратно
- TVM → EVM (Native): lock native в TVM (Proxy), mint/create2 в EVM — перевод TVM-токена в EVM
- EVM → TVM (Native): burn в EVM, unlock native в TVM — возврат TVM-токена обратно
Архитектурные слои
Система разделена на 3 слоя:
1. OFF-CHAIN (зелёный слой на диаграммах)
Индексеры, API-сервисы, Frontend, relay-ноды. Это «клей» между блокчейнами: отслеживает события, строит транзакции, подписывает подтверждения.
2. EVM (жёлтый слой)
Смарт-контракты EVM сетей: MultiVault (Diamond), ERC20 токены.
3. TVM (синий слой)
Смарт-контракты TVM сетей, включая Event-контракты (в которых достигается ончейн-консенсус relay-нод), Proxy-контракты, JettonMinter/TokenRoot, JettonWallet/TokenWallet.
Компоненты системы
OFF-CHAIN слой
relay
Назначение: Основная relay-нода — полный TVM узел, который отслеживает события в обоих блокчейнах.
Ключевые функции:
- Для EVM→TVM: деплоит Event-контракт и вызывает
confirm(voteReceiver)— записывает голос в TVM - Для TVM→EVM: вызывает
confirm(signature, voteReceiver)— записывает голос + криптографическую подпись для EVM - Подписка на события Bridge-контракта для получения конфигураций
- Мониторинг событий обоих блокчейнов
Взаимодействует с:
- Event-контрактами в TVM (вызов confirm)
- Bridge-контрактом в TVM (получение конфигураций)
- EVM RPC endpoints (мониторинг событий MultiVault)
Gas Credit Backend
Назначение: Сервис автоматического предфинансирования газа для кросс-чейн трансферов между EVM и TVM сетями.
Как работает: Пользователь оплачивает газ в сети отправления (включая оплату за деплой Event-контракта), а сервис автоматически деплоит Event-контракт в TVM сети назначения, конвертируя стоимость газа по текущему USD-курсу между нативными токенами сетей.
Особенность: Не требует полного TVM узла, работает через RPC. В отличие от основной relay-ноды (octusbridge-relay), не участвует в подписании событий — только в деплое Event-контрактов.
multivault-graph
Назначение: Индексер событий MultiVault в EVM сети. Отслеживает события Deposit, Withdraw, PendingWithdrawal и др.
Две взаимозаменяемые реализации:
- The Graph (subgraph) — первая реализация
- Envio (
multivault-graph-v2/) — вторая реализация, представлена в этом репозитории
Обе реализации решают одну задачу: индексируют события MultiVault и предоставляют данные через GraphQL API. Для работы достаточно одной реализации. Две поддерживаются для диверсификации рисков, т.к. это внешние сервисы.
Взаимодействует с:
- EVM RPC endpoints (индексация событий)
- BridgeAPI (предоставление данных)
Bridge API
Назначение: Backend API моста для индексации TVM-стороны и связанных с ней EVM сетей. Индексирует состояние TVM-контрактов (Event-контракты, конфигурации, статусы подтверждений) и забирает EVM-события из инстансов multivault-graph.
Особенность: Один инстанс BridgeAPI обслуживает одну TVM сеть и все связанные с ней EVM сети (через соответствующие multivault-graph).
Взаимодействует с:
- TVM сетью (индексация Event-контрактов)
- multivault-graph (получение EVM-событий связанных EVM сетей)
- Aggregator API (предоставление данных)
Bridge Aggregator API
Назначение: Агрегатор над инстансами BridgeAPI. Объединяет данные из нескольких TVM-бриджей (каждый BridgeAPI обслуживает свою TVM сеть с привязанными EVM сетями) и предоставляет единый endpoint для работы с бриджом.
Ключевые endpoints:
/payload/build(POST) — построить payload для транзакции трансфера/transfers/search(POST) — поиск трансферов с фильтрацией/transfers/status(POST) — получить статус конкретного трансфера
Взаимодействует с:
- Инстансами BridgeAPI (агрегация данных из нескольких TVM-бриджей)
- Frontend (предоставление единого интерфейса)
Frontend
Назначение: Веб-интерфейс пользователя для выполнения трансферов.
Взаимодействует с:
- Aggregator API (получение данных и построение транзакций)
- Wallet connections (Metamask, EverWallet и др.)
gas-price-api
Назначение: Сервис получения текущих цен на газ для EVM сетей.
EVM слой
MultiVault
Назначение: Главный контракт моста в EVM сети. Реализован через Diamond pattern (EIP-2535) с множеством facets.
Ключевые методы:
deposit(DepositParams)— депозит токенов для перевода в TVMdepositByNativeToken(DepositNativeTokenParams)— депозит нативной валюты (ETH, BNB и др.)saveWithdrawNative(bytes payload, bytes[] signatures)— вывод Native токенов с проверкой подписей relay-нодsaveWithdrawAlien(bytes payload, bytes[] signatures, uint bounty)— вывод Alien токенов с проверкой подписей relay-нод
Facets:
| Facet | Назначение |
|---|---|
| MultiVaultFacetDeposit | Методы deposit |
| MultiVaultFacetWithdraw | Методы withdraw с верификацией подписей |
| MultiVaultFacetTokens | Реестр токенов (native/alien, blacklist) |
| MultiVaultFacetFees | Управление комиссиями |
| MultiVaultFacetLiquidity | Пулы ликвидности |
| MultiVaultFacetSettings | Конфигурация (governance, emergency shutdown) |
| MultiVaultFacetPendingWithdrawals | Очередь ожидающих выводов: rate-limiting по токенам (скользящее окно), одобрение/отклонение governance/withdrawGuardian, bounty-система, отмена с возвратом в TVM, принудительный вывод (emergency) |
События:
Deposit— депозит токеновAlienTransfer— трансфер Alien токена (EVM → TVM)NativeTransfer— трансфер Native токена (EVM → TVM)Withdraw— вывод токеновPendingWithdrawalCreated— создан pending withdrawal
Взаимодействует с:
- ERC20 токенами (lock/unlock alien токенов)
- MultiVaultToken (burn/mint native представлений)
- relay-нодами (верификация подписей через публичные ключи)
MultiVaultToken
Назначение: ERC20 токен, созданный MultiVault через create2 для представления Native TVM токенов.
Особенность: Создаётся детерминированно при первом переводе Native токена из TVM в EVM.
Ключевые методы:
burn(address, uint256)— сжигание при возврате Native токена в TVMmint(address, uint256)— минтинг при получении Native токена из TVM
TVM слой
Ключевой принцип
Консенсус relay-нод достигается ончейн в TVM сети. Для каждого трансфера создаётся отдельный Event-контракт, который служит «голосовательным бюллетенем» — relay-ноды вызывают метод confirm(). Для EVM→TVM это просто голосование, для TVM→EVM — голосование с криптографической подписью. Когда достигнуто достаточное количество подтверждений, Event-контракт считается подтверждённым.
Event-контракты
Event-контракт — это одноразовый смарт-контракт в TVM, создаваемый для конкретного трансфера. Он нужен потому, что два разных блокчейна (EVM и TVM) не могут напрямую «видеть» друг друга. Event-контракт решает эту проблему: он хранит данные о трансфере и собирает подтверждения relay-нод.
Каждый тип трансфера использует свой тип Event-контракта (Alien/Native x EVM→TVM/TVM→EVM). После подтверждения Event-контракт вызывает callback в Proxy-контракт для выполнения финального действия (mint/transfer/unlock токенов).
MultiVaultEvmTvmEventAlien
Назначение: Event-контракт для EVM→TVM трансфера Alien токена.
Жизненный цикл:
- Relay деплоит контракт через EvmTvmEventConfiguration
- Контракт запрашивает у Proxy адрес alien токена (
deriveEvmAlienTokenRoot) - Контракт получает публичные ключи relay-нод из RoundDeployer
- Relay-ноды вызывают
confirm(записывают подтверждение) - При достаточном количестве подписей → событие
Confirmed - Callback в ProxyMultiVaultAlien →
onEventConfirmedExtended→ mint alien токена
Ключевые методы:
confirm()— relay-нода подтверждает событиеreceiveAlienTokenRoot(address _token)— получение адреса alien токенаreceiveConfigurationDetails()— получение конфигурации
Взаимодействует с:
- EvmTvmEventConfiguration (получение конфигурации)
- ProxyMultiVaultAlien (callback после подтверждения)
- RoundDeployer (получение публичных ключей relay-нод)
MultiVaultEvmTvmEventNative
Назначение: Event-контракт для EVM→TVM трансфера Native токена (возврат Native токена).
Жизненный цикл:
- Relay деплоит контракт через EvmTvmEventConfiguration
- Контракт запрашивает адрес token wallet Proxy (
walletOf) - Контракт получает публичные ключи relay-нод из RoundDeployer
- Relay-ноды вызывают
confirm - При достаточном количестве подписей → событие
Confirmed - Callback в ProxyMultiVaultNative →
onEventConfirmedExtended→ transfer (unlock) native токена пользователю
MultiVaultTvmEvmEventAlien
Назначение: Event-контракт для TVM→EVM трансфера Alien токена (возврат Alien токена).
Жизненный цикл:
- ProxyMultiVaultAlien деплоит контракт через TvmEvmEventConfiguration (при burn alien токена)
- Контракт верифицирует alien токен (
deriveEvmAlienTokenRoot) - Контракт получает публичные ключи relay-нод из RoundDeployer
- Relay-ноды вызывают
confirm(signature, voteReceiver)— записывают голос и криптографическую подпись для EVM - При достаточном количестве подписей → событие
Confirmed - Подписи собираются Aggregator API и передаются в EVM для
MultiVault.saveWithdrawAlien()
Ключевые методы:
confirm(signature, voteReceiver)— relay-нода записывает голос + подпись для EVMreceiveAlienTokenRoot()— верификация alien токенаgetDecodedData()— получение данных события для подписи
MultiVaultTvmEvmEventNative
Назначение: Event-контракт для TVM→EVM трансфера Native токена.
Жизненный цикл:
- ProxyMultiVaultNative деплоит контракт через TvmEvmEventConfiguration (при transfer native токена)
- Контракт верифицирует token wallet Proxy (
walletOf) - Контракт получает публичные ключи relay-нод из RoundDeployer
- Relay-ноды вызывают
confirm(signature, voteReceiver)— записывают голос и криптографическую подпись для EVM - При достаточном количестве подписей → событие
Confirmed - Подписи собираются Aggregator API и передаются в EVM для
MultiVault.saveWithdrawNative()
EvmTvmEventConfiguration
Назначение: Конфигурация для событий EVM→TVM направления. Определяет параметры создания Event-контрактов.
Ключевые параметры:
proxy— адрес Proxy-контракта (ProxyMultiVaultAlien или ProxyMultiVaultNative)startBlockNumber/endBlockNumber— диапазон блоков EVM для отслеживанияeventInitialBalance— начальный баланс Event-контракта
Ключевые методы:
deployEvent(EvmTvmEventVoteData)— деплой нового Event-контрактаgetDetails()— получение параметров конфигурации
TvmEvmEventConfiguration
Назначение: Конфигурация для событий TVM→EVM направления.
ProxyMultiVaultAlien
Назначение: Прокси для управления Alien токенами в TVM (представления EVM-токенов).
Для EVM→TVM (mint):
onEventConfirmedExtended()— callback от MultiVaultEvmTvmEventAlien после подтвержденияderiveEvmAlienTokenRoot()— вычисление детерминированного адреса alien токенаdeployEvmAlienToken()— деплой alien токена (если ещё не существует)_mintTokens()— минтинг alien токенов пользователю
Для TVM→EVM (burn):
onAcceptTokensBurn()— callback от TokenWallet при burn_deployEvmEvent()— деплой MultiVaultTvmEvmEventAlien для инициации трансфера
ProxyMultiVaultNative
Назначение: Прокси для управления Native токенами в TVM (lock/unlock TVM-токенов).
Для TVM→EVM (lock):
transferNotification()— callback от TokenWallet при transfer- Proxy залокивает (принимает на свой wallet) native токены
_deployEvmEvent()— деплой MultiVaultTvmEvmEventNative для инициации трансфера
Для EVM→TVM (unlock):
onEventConfirmedExtended()— callback от MultiVaultEvmTvmEventNative после подтверждения- Transfer native токенов с wallet Proxy на wallet пользователя
TokenRootAlienEvm
Назначение: Root контракт Alien токена в TVM (представляет EVM-токен).
Ключевые методы:
mint()— минтинг alien токенов (вызывается ProxyMultiVaultAlien)deployWallet()— деплой TokenWallet для пользователя
JettonMinter/TokenRoot
Назначение: Root контракт Native TVM токена. В TVM-экосистеме существуют две версии стандарта токенов: TIP-3 (TokenRoot/TokenWallet) и Jetton (JettonMinter/JettonWallet). Контракты функционально эквивалентны.
Ключевые методы (TIP-3 — TokenRoot):
deployWallet()— деплой TokenWallet для пользователяwalletOf()— вычисление адреса walletmint()— минтинг токенов (вызывается owner)burn()— сжигание токенов
Ключевые методы (Jetton — JettonMinter):
mint()— минтинг джеттоновget_jetton_data()— получение данных о джеттоне (total_supply, admin, content, wallet_code)get_wallet_address(owner)— вычисление адреса JettonWallet по адресу владельца
JettonWallet/TokenWallet
Назначение: Кошелёк пользователя для конкретного токена.
Ключевые методы (TIP-3 — TokenWallet):
transfer()— трансфер токенов (для Native токенов → инициирует TVM→EVM трансфер через ProxyMultiVaultNative)burn()— сжигание токенов (для Alien токенов → инициирует TVM→EVM трансфер через ProxyMultiVaultAlien)balance()— получение баланса
Ключевые методы (Jetton — JettonWallet):
send_tokens()/transfer()— трансфер джеттонов (аналогtransfer()в TIP-3)burn_tokens()/burn()— сжигание джеттонов (аналогburn()в TIP-3)get_wallet_data()— получение данных кошелька (balance, owner, jetton_master, wallet_code)
RoundDeployer
Назначение: Управляет раундами relay-нод. Предоставляет публичные ключи активных relay-нод для проверки подписей.
Особенность: Раунды relay-нод меняются периодически, обеспечивая ротацию набора relay-нод для безопасности.
CellEncoderStandalone
Назначение: Кодирует данные события для подписи relay-нодами.
MergeRouter / MergePool
Назначение: Роутер и пул для merge pool операций (опциональный путь для Alien токенов).
Особенность: Позволяет объединять токены разных сетей через пулы ликвидности.
Потоки трансферов
Поток 1: EVM → TVM (Alien токен)

Сценарий: Пользователь переводит Alien токен (изначально выпущенный в EVM) из EVM сети в TVM сеть.
Шаг 1: Депозит в EVM
Действие: Пользователь вызывает MultiVault.deposit()
Детали:
- User вызывает
deposit({token, amount, recipient, expected_gas, payload}) - MultiVault проверяет, что токен не в blacklist
- Определяется, что токен Alien (
isNative = false) - MultiVault вызывает
IERC20(token).safeTransferFrom(user, MultiVault, amount)— токены залокированы в MultiVault - Вычисляется комиссия (
_calculateMovementFee) - Эмитируется событие
DepositиAlienTransfer
Шаг 2: Индексация события
Действие: multivault-graph индексирует событие Deposit + AlienTransfer
Детали:
- Envio индексер отслеживает события MultiVault
- Данные сохраняются в GraphQL базу
- BridgeAPI и Aggregator API получают информацию о новом депозите
- Frontend показывает пользователю статус "Pending"
Шаг 3: Relay деплоит Event-контракт
Действие: relay-нода получает событие и деплоит MultiVaultEvmTvmEventAlien
Детали:
- relay мониторит события EVM через RPC
- relay вызывает
EvmTvmEventConfiguration.deployEvent(eventVoteData) EvmTvmEventConfigurationсоздаёт новый контрактMultiVaultEvmTvmEventAlien- Event-контракт инициализируется с данными:
{base_chainId, base_token, amount, recipient, name, symbol, decimals}
Шаг 4: Event-контракт запрашивает данные
Действие: MultiVaultEvmTvmEventAlien запрашивает конфигурацию и адрес токена
Детали:
- Event-контракт вызывает
EvmTvmEventConfiguration.getDetails()— получает адрес Proxy - Event-контракт вызывает
ProxyMultiVaultAlien.deriveEvmAlienTokenRoot()— вычисляет адрес alien токена - Proxy возвращает детерминированный адрес alien токена
- Event-контракт проверяет, существует ли токен (
getInfo)
Шаг 5: Event-контракт получает публичные ключи relay-нод
Действие: Event-контракт запрашивает у RoundDeployer публичные ключи активных relay-нод
Детали:
- Event-контракт вызывает
RoundDeployer.getRelayRoundAddressFromTimestamp() - Получает список публичных ключей relay-нод текущего раунда
- Эти ключи будут использоваться для проверки подписей (
confirm)
Шаг 6: Relay-ноды подтверждают событие
Действие: Relay-ноды вызывают MultiVaultEvmTvmEventAlien.confirm()
Детали:
- Каждая relay-нода проверяет данные события
- Relay-нода вызывает
confirm()с внешним сообщением, подписанным своим приватным ключом - Event-контракт проверяет подпись через публичный ключ relay-ноды
- Event-контракт записывает голос relay-ноды
- Когда достигнут порог подтверждений → Event-контракт переходит в статус
Confirmed
Шаг 7: Событие Confirmed
Действие: Event-контракт эмитирует событие Confirmed и вызывает callback
Детали:
- Event-контракт эмитирует событие
Confirmed - Вызывается callback в ProxyMultiVaultAlien:
onEventConfirmedExtended()
Шаг 8: Proxy минтит alien токены
Действие: ProxyMultiVaultAlien получает callback и минтит alien токены пользователю
Детали:
ProxyMultiVaultAlien.onEventConfirmedExtended()получает данные события- Если alien токен ещё не задеплоен — деплоит
TokenRootAlienEvm - Вызывает
TokenRootAlienEvm.mint(recipient, amount) - JettonMinter/TokenRoot деплоит JettonWallet/TokenWallet для пользователя (если ещё не существует)
- JettonWallet/TokenWallet получает alien токены
Шаг 9: Frontend обновляет статус
Действие: Frontend через Aggregator API получает статус "Completed"
Поток 2: TVM → EVM (Alien токен)

Сценарий: Пользователь возвращает Alien токен из TVM сети обратно в EVM сеть.
Шаг 1: Сжигание alien токена в TVM
Действие: Пользователь вызывает TokenWallet.burn()
Детали:
- User вызывает
burn({amount, recipient, payload}) - TokenWallet сжигает токены
- TokenWallet вызывает callback в TokenRootAlienEvm:
onAcceptTokensBurn() - TokenRoot вызывает callback в ProxyMultiVaultAlien:
onAcceptTokensBurn()
Шаг 2: Proxy деплоит Event-контракт
Действие: ProxyMultiVaultAlien деплоит MultiVaultTvmEvmEventAlien
Детали:
ProxyMultiVaultAlien.onAcceptTokensBurn()получает данные burn- Декодирует payload:
{nonce, network, burnPayload{recipient, callback}, remainingGasTo} - Формирует eventData с параметрами трансфера
- Вызывает
TvmEvmEventConfiguration.deployEvent(eventVoteData) - Event-контракт создаётся с данными:
{nonce, proxy, token, remainingGasTo, amount, recipient, sender, callback}
Шаг 3: Event-контракт верифицирует токен
Действие: MultiVaultTvmEvmEventAlien проверяет, что токен действительно alien
Детали:
- Event-контракт запрашивает метаданные токена (
getInfo/takeInfo) - Получает
{base_chainId, base_token, name, symbol, decimals} - Вызывает
ProxyMultiVaultAlien.deriveEvmAlienTokenRoot()для проверки - Сравнивает полученный адрес с
expectedToken
Шаг 4: Event-контракт получает публичные ключи relay-нод
Действие: Аналогично потоку 1 — запрос у RoundDeployer
Шаг 5: Relay-ноды подтверждают с подписями
Действие: Relay-ноды вызывают MultiVaultTvmEvmEventAlien.confirm(signature, voteReceiver)
Детали:
- Каждая relay-нода проверяет данные события
- Relay-нода вызывает
confirm(signature, voteReceiver)— записывает голос + криптографическую подпись в Event-контракт - Подписи сохраняются в маппинге
signatures[relay]для последующей верификации в EVM - Когда достигнут порог подтверждений → Event-контракт переходит в статус
Confirmed
Шаг 6: Событие Confirmed
Действие: Event-контракт эмитирует событие Confirmed
Детали:
- BridgeAPI индексирует событие
Confirmedв TVM - Aggregator API получает данные о подтверждённом Event-контракте
- Aggregator API собирает подписи из маппинга
signaturesдля передачи в EVM
Шаг 7: Aggregator API предоставляет payload
Действие: Frontend запрашивает payload через /transfers/status
Детали:
- Aggregator API возвращает
payload(закодированные данные события) иsignatures(массив подписей relay-нод) - Frontend получает готовые данные для вызова
MultiVault.saveWithdrawAlien()
Шаг 8: Вывод токенов в EVM
Действие: User (или relay) вызывает MultiVault.saveWithdrawAlien(payload, signatures)
Детали:
- MultiVault декодирует
payloadи верифицируетsignaturesчерез публичные ключи relay-нод - Проверяет, что withdrawal ещё не был выполнен (
withdrawalIds[payloadId]) - Декодирует данные:
{token, amount, recipient, chainId, callback} - Проверяет, что
chainIdсоответствует текущей сети - Проверяет, что токен не в blacklist
- Вычисляет комиссию
- Проверяет withdrawal limits (может создать pending withdrawal вместо немедленного вывода)
- Если лимиты пройдены и баланс достаточен:
IERC20(token).safeTransfer(recipient, amount - fee)— токены разлокованы, эмитируетWithdraw - Иначе создаёт
PendingWithdrawal
Важно
Подписи relay-нод необходимы для EVM стороны — EVM-контракт верифицирует эти подписи, чтобы убедиться, что трансфер был подтверждён достаточным количеством relay-нод. Подписи передаются в параметре метода confirm(signature, voteReceiver) и сохраняются в Event-контракте.
Поток 3: TVM → EVM (Native токен)

Сценарий: Пользователь переводит Native токен (изначально выпущенный в TVM) из TVM сети в EVM сеть.
Шаг 1: Transfer native токена в Proxy
Действие: Пользователь вызывает TokenWallet.transfer() на адрес ProxyMultiVaultNative
Детали:
- User вызывает
transfer({amount, recipient: ProxyWallet, payload}) - JettonWallet/TokenWallet переводит токены на token wallet Proxy (lock)
- JettonWallet/TokenWallet вызывает callback в ProxyMultiVaultNative:
transferNotification()
Шаг 2: Proxy деплоит Event-контракт
Действие: ProxyMultiVaultNative получает callback и деплоит MultiVaultTvmEvmEventNative
Детали:
ProxyMultiVaultNative.transferNotification()получает данные transfer- Декодирует payload:
{nonce, network, transferPayload{recipient, chainId, callback}, remainingGasTo} - Получает метаданные токена (
name,symbol,decimals) - Формирует eventData
- Вызывает
TvmEvmEventConfiguration.deployEvent(eventVoteData)
Шаг 3: Event-контракт верифицирует token wallet
Действие: MultiVaultTvmEvmEventNative проверяет, что tokenWallet принадлежит Proxy
Детали:
- Event-контракт вызывает
TokenRoot.walletOf(proxy) - Получает
expectedTokenWallet - Сравнивает с
tokenWalletиз eventData - Если совпадает — Event валиден, иначе — Rejected
Шаги 4-6: Аналогично потоку 2
- Event-контракт получает публичные ключи relay-нод
- Relay-ноды вызывают
confirm(signature, voteReceiver)— записывают голос + подпись для EVM - Событие
Confirmedэмитируется
Шаг 7: Aggregator API предоставляет payload
Действие: Frontend получает payload и signatures через Aggregator API
Шаг 8: Минтинг Native токена в EVM
Действие: User (или relay) вызывает MultiVault.saveWithdrawNative(payload, signatures)
Детали:
- MultiVault декодирует
payloadи верифицируетsignatures - Декодирует данные:
{native{wid, addr}, meta{name, symbol, decimals}, amount, recipient, chainId, callback} - Проверяет, что
chainIdсоответствует текущей сети - Вычисляет адрес токена:
- Проверяет наличие predeployed токена для данного
nativeадреса - Если predeployed существует — использует его
- Иначе — вычисляет адрес через create2 и деплоит новый MultiVaultToken
- Проверяет наличие predeployed токена для данного
- Проверяет withdrawal limits
- Если лимиты пройдены:
IMultiVaultToken(token).mint(recipient, amount - fee)— токены минтятся, эмитируетWithdraw - Иначе создаёт
PendingWithdrawal
Поток 4: EVM → TVM (Native токен)

Сценарий: Пользователь возвращает Native токен из EVM сети обратно в TVM сеть.
Шаг 1: Сжигание Native токена в EVM
Действие: Пользователь вызывает MultiVault.deposit()
Детали:
- User вызывает
deposit({token: nativeToken, amount, recipient, expected_gas, payload}) - MultiVault проверяет, что токен не в blacklist
- Определяется, что токен Native (
isNative = true) - MultiVault вызывает
IMultiVaultToken(token).burn(user, amount)— Native токены сжигаются - Вычисляется комиссия
- Эмитируется событие
DepositиNativeTransfer
Шаг 2: Индексация события
Действие: multivault-graph индексирует Deposit + NativeTransfer
Шаг 3: Relay деплоит Event-контракт
Действие: relay-нода деплоит MultiVaultEvmTvmEventNative
Детали:
- relay вызывает
EvmTvmEventConfiguration.deployEvent(eventVoteData) - Event-контракт создаётся с данными:
{token_wid, token_addr, amount, recipient_wid, recipient_addr, value, expected_gas, payload}
Шаг 4: Event-контракт запрашивает данные
Действие: MultiVaultEvmTvmEventNative запрашивает конфигурацию и token wallet Proxy
Детали:
- Event-контракт вызывает
EvmTvmEventConfiguration.getDetails()— получает адрес Proxy - Event-контракт вызывает
TokenRoot.walletOf(proxy)— получает адрес token wallet Proxy
Шаги 5-7: Аналогично потоку 1
- Event-контракт получает публичные ключи relay-нод
- Relay-ноды вызывают
confirm() - Событие
Confirmedэмитируется
Шаг 8: Proxy разлокивает native токены
Действие: ProxyMultiVaultNative получает callback и делает transfer пользователю
Детали:
ProxyMultiVaultNative.onEventConfirmedExtended()получает данные события- Вызывает
TokenWallet(proxyWallet).transfer({recipient: userWallet, amount}) - Native токены переводятся с wallet Proxy на wallet пользователя (unlock)
Шаг 9: Frontend обновляет статус
Риски и Edge Cases
| Риск/Ошибка | Причина | Защита/Решение |
|---|---|---|
| Недостаточный баланс для withdraw | Alien токены залокованы, но баланс MultiVault недостаточен | MultiVault создаёт PendingWithdrawal; user может установить bounty для стимулирования ликвидности |
| Превышение withdrawal limits | Слишком большой объём withdrawals за 24-часовой период | MultiVault отслеживает withdrawal volume; при превышении создаёт PendingWithdrawal со статусом ApproveStatus.Required; withdrawGuardian одобряет вручную |
| Event-контракт с недостаточным балансом | Баланс Event-контракта меньше expected_gas | Event-контракт проверяет баланс в _onInit() и делает self-destruct с возвратом средств |
| Подделка подписей relay-нод | Злонамеренный актёр пытается вывести токены | MultiVault верифицирует подписи через публичные ключи relay-нод; проверяется достаточное количество уникальных подписей |
| Blacklisted токен | Токен может быть скомпрометирован | Governance может добавить токен в blacklist; все deposit/withdraw блокируются; emergency shutdown останавливает все операции |
| Неверный chainId | Withdraw event подписан для одной сети, вызван в другой | MultiVault проверяет withdrawal.chainId == block.chainid |
| Replay attack (повторный withdraw) | Один и тот же withdrawal event вызывается несколько раз | MultiVault отслеживает withdrawalIds[keccak256(payload)]; повторный вызов блокируется |
| Неверная верификация Event-контракта | Event-контракт с неверным токеном/wallet проходит подтверждение | Для Alien: верификация через deriveEvmAlienTokenRoot(). Для Native: проверка walletOf(proxy) == tokenWallet. При несовпадении — Rejected |
Ошибки EVM-контрактов (require/revert)
Все EVM-контракты используют строковые сообщения об ошибках (не custom errors). Ниже — полный реестр ошибок, сгруппированный по категориям.
Deposit (MultiVaultFacetDeposit)
| Сообщение ошибки | Условие |
|---|---|
Msg value to low | msg.value < amount при депозите нативной валюты (ETH/BNB) |
Deposit: limits violated | Сумма депозита превышает установленные лимиты |
Deposit amount too is large | amount >= type(uint128).max — сумма не помещается в uint128 для TVM |
Pending: already filled | Попытка заполнить уже закрытый pending withdrawal |
Pending: wrong token | Токен депозита не совпадает с токеном pending withdrawal |
Pending: deposit insufficient | Суммы депозита недостаточно для покрытия pending withdrawal |
Withdraw (MultiVaultFacetWithdraw, MultiVaultHelperWithdraw)
| Сообщение ошибки | Условие |
|---|---|
Withdraw: wrong chain id | withdrawal.chainId != block.chainid — payload предназначен для другой сети |
Withdraw: token is blacklisted | Токен добавлен в blacklist governance |
Withdraw: bounty > withdraw amount | Bounty превышает сумму вывода |
Withdraw: already seen | Повторная попытка вывода с тем же payload (replay protection) |
Withdraw: invalid configuration | Event configuration не совпадает с зарегистрированной в контракте |
| (без сообщения) | verifySignedTvmEvent() вернул ненулевой результат — подписи relay-нод невалидны |
Pending Withdrawals (MultiVaultFacetPendingWithdrawals, MultiVaultHelperPendingWithdrawal)
| Сообщение ошибки | Условие |
|---|---|
Pending: amount is zero | Попытка операции с pending withdrawal, у которого amount == 0 |
Pending: native token | Попытка установить bounty или отменить pending withdrawal для native токена |
Pending: bounty too large | Bounty превышает сумму pending withdrawal |
Pending: zero amount | forceWithdraw() вызван для pending withdrawal с нулевой суммой |
Pending: wrong amount | Сумма отмены <= 0 или > суммы pending withdrawal |
Pending: wrong current approve status | Операция одобрения/отклонения вызвана для withdrawal не в статусе Required |
Pending: wrong approve status | Передан невалидный статус одобрения (не Approved/Rejected) |
Pending: params mismatch | В batch-операции массивы pendingWithdrawalId и approveStatus разной длины |
Pending: wrong approve status | Попытка уменьшить сумму pending withdrawal с невалидным статусом одобрения |
Токены (MultiVaultHelperTokens)
| Сообщение ошибки | Условие |
|---|---|
Tokens: invalid token meta | decimals > DECIMALS_LIMIT или symbol.length > SYMBOL_LENGTH_LIMIT или name.length > NAME_LENGTH_LIMIT |
Tokens: token is blacklisted | Операция с токеном из blacklist |
Tokens: weth is blacklisted | WETH токен добавлен в blacklist |
Tokens: invalid token | Адрес задеплоенного токена не совпадает с ожидаемым (create2 mismatch) |
Ликвидность (MultiVaultFacetLiquidity)
| Сообщение ошибки | Условие |
|---|---|
Liquidity: token is native | Попытка создать LP для native токена (LP доступны только для alien) |
Liquidity: only governance or management | Начальный mint LP вызван не governance/management |
Liquidity: amount is too small | Начальный mint LP < 1000 wei |
Liquidity: recipient is not governance | Получатель начального mint LP — не governance |
Liquidity: LP not activated | Операция с LP, который ещё не активирован |
Контроль доступа (MultiVaultHelperActors)
| Сообщение ошибки | Условие |
|---|---|
Actors: only pending governance | Вызов доступен только pending governance |
Actors: only governance | Вызов доступен только governance |
Actors: only governance or management | Вызов доступен только governance или management |
Actors: only governance or withdraw guardian | Вызов доступен только governance или withdrawGuardian |
Настройки (MultiVaultFacetSettings)
| Сообщение ошибки | Условие |
|---|---|
Settings: wrong bridge | Адрес bridge == address(0) при инициализации |
Settings: wrong governance | Адрес governance == address(0) при инициализации |
Settings: wrong weth | Адрес WETH == address(0) при инициализации |
Settings: daily limit < undeclared | Daily withdrawal limit меньше undeclared limit |
Settings: only guardian or governance | Попытка активации emergency shutdown не guardian/governance |
Settings: only governance | Попытка деактивации emergency shutdown не governance |
Emergency и безопасность
| Сообщение ошибки | Условие |
|---|---|
Emergency: shutdown | Операция вызвана во время emergency shutdown |
ReentrancyGuard: reentrant call | Обнаружена попытка реентрантного вызова |
Fee: limit exceeded | Комиссия превышает FEE_LIMIT |
Callback: cant call itself | Попытка callback на адрес самого MultiVault |
Callback: strict call failed | Строгий callback завершился с ошибкой |
Gas: failed to send gas to donor | Не удалось отправить газ донору |
Cache: payload already seen | Повторная обработка уже обработанного payload |
Инициализация (MultiVaultHelperInitializable)
| Сообщение ошибки | Условие |
|---|---|
Initializable: contract is already initialized | Повторная инициализация контракта |
Initializable: contract is not initializing | Вызов onlyInitializing метода вне инициализации |
Initializable: contract is initializing | Вызов _disableInitializers() во время инициализации |
Diamond Pattern (DiamondStorage)
| Сообщение ошибки | Условие |
|---|---|
LibDiamond: Must be contract owner | Вызов доступен только владельцу Diamond |
DiamondStorage: already initialized | Diamond уже инициализирован |
DiamondStorage: Incorrect FacetCutAction | Невалидное действие при diamondCut |
DiamondStorage: No selectors in facet to cut | Пустой массив селекторов при добавлении/замене/удалении facet |
DiamondStorage: Add facet can't be address(0) | Адрес facet == address(0) при добавлении/замене |
DiamondStorage: Can't add function that already exists | Попытка добавить уже существующий селектор |
DiamondStorage: Can't replace function with same function | Замена facet на тот же самый |
DiamondStorage: Remove facet address must be address(0) | Адрес facet != address(0) при удалении |
DiamondStorage: Can't remove function that doesn't exist | Удаление несуществующего селектора |
DiamondStorage: Can't remove immutable function | Удаление функции из immutable facet (address(this)) |
DiamondStorage: _init function reverted | Ошибка при выполнении init-функции после diamondCut |
Bridge и DAO
| Сообщение ошибки | Условие |
|---|---|
Bridge: renounce ownership is not allowed | Попытка отказа от ownership Bridge |
Bridge: initial round end should be in the future | _initialRoundEnd < block.timestamp при инициализации |
Bridge: signature recover failed | Ошибка восстановления подписи (signer == address(0) или RecoverError) |
Bridge: sender not round submitter | Вызов forceRoundRelays() не roundSubmitter |
Bridge: signatures verification failed | Верификация подписей TVM-события провалена |
Bridge: wrong event configuration | Event configuration не совпадает с roundRelaysConfiguration |
Bridge: wrong round | Номер раунда не равен lastRound + 1 (нарушена последовательность) |
Bridge: signatures sequence wrong | Подписи не отсортированы по возрастанию адреса signer |
DAO: renounce ownership is not allowed | Попытка отказа от ownership DAO |
DAO: zero address | Нулевой адрес передан в DAO-метод |
DAO: signatures verification failed | Верификация подписей в DAO провалена |
DAO: wrong event configuration | Event configuration DAO не совпадает |
DAO: wrong chain id | DAO-действие предназначено для другой сети |
DAO: execution fail | Исполнение DAO-действия завершилось ошибкой |
TokenFactory: not self call | Вызов фабрики токенов не через self-call |