Skip to content

Архитектура 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→TVMconfirm(voteReceiver)❌ НетПодсчёт голосовCallback в Proxy (TVM)
TVM→EVMconfirm(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-контракте

Типы трансферов

  1. EVM → TVM (Alien): lock alien в EVM (MultiVault), mint alien в TVM — перевод EVM-токена в TVM
  2. TVM → EVM (Alien): burn alien в TVM, unlock alien в EVM — возврат EVM-токена обратно
  3. TVM → EVM (Native): lock native в TVM (Proxy), mint/create2 в EVM — перевод TVM-токена в EVM
  4. 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) — депозит токенов для перевода в TVM
  • depositByNativeToken(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 токена в TVM
  • mint(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 токена.

Жизненный цикл:

  1. Relay деплоит контракт через EvmTvmEventConfiguration
  2. Контракт запрашивает у Proxy адрес alien токена (deriveEvmAlienTokenRoot)
  3. Контракт получает публичные ключи relay-нод из RoundDeployer
  4. Relay-ноды вызывают confirm (записывают подтверждение)
  5. При достаточном количестве подписей → событие Confirmed
  6. Callback в ProxyMultiVaultAlien → onEventConfirmedExtended → mint alien токена

Ключевые методы:

  • confirm() — relay-нода подтверждает событие
  • receiveAlienTokenRoot(address _token) — получение адреса alien токена
  • receiveConfigurationDetails() — получение конфигурации

Взаимодействует с:

  • EvmTvmEventConfiguration (получение конфигурации)
  • ProxyMultiVaultAlien (callback после подтверждения)
  • RoundDeployer (получение публичных ключей relay-нод)

MultiVaultEvmTvmEventNative

Назначение: Event-контракт для EVM→TVM трансфера Native токена (возврат Native токена).

Жизненный цикл:

  1. Relay деплоит контракт через EvmTvmEventConfiguration
  2. Контракт запрашивает адрес token wallet Proxy (walletOf)
  3. Контракт получает публичные ключи relay-нод из RoundDeployer
  4. Relay-ноды вызывают confirm
  5. При достаточном количестве подписей → событие Confirmed
  6. Callback в ProxyMultiVaultNative → onEventConfirmedExtended → transfer (unlock) native токена пользователю

MultiVaultTvmEvmEventAlien

Назначение: Event-контракт для TVM→EVM трансфера Alien токена (возврат Alien токена).

Жизненный цикл:

  1. ProxyMultiVaultAlien деплоит контракт через TvmEvmEventConfiguration (при burn alien токена)
  2. Контракт верифицирует alien токен (deriveEvmAlienTokenRoot)
  3. Контракт получает публичные ключи relay-нод из RoundDeployer
  4. Relay-ноды вызывают confirm(signature, voteReceiver) — записывают голос и криптографическую подпись для EVM
  5. При достаточном количестве подписей → событие Confirmed
  6. Подписи собираются Aggregator API и передаются в EVM для MultiVault.saveWithdrawAlien()

Ключевые методы:

  • confirm(signature, voteReceiver) — relay-нода записывает голос + подпись для EVM
  • receiveAlienTokenRoot() — верификация alien токена
  • getDecodedData() — получение данных события для подписи

MultiVaultTvmEvmEventNative

Назначение: Event-контракт для TVM→EVM трансфера Native токена.

Жизненный цикл:

  1. ProxyMultiVaultNative деплоит контракт через TvmEvmEventConfiguration (при transfer native токена)
  2. Контракт верифицирует token wallet Proxy (walletOf)
  3. Контракт получает публичные ключи relay-нод из RoundDeployer
  4. Relay-ноды вызывают confirm(signature, voteReceiver) — записывают голос и криптографическую подпись для EVM
  5. При достаточном количестве подписей → событие Confirmed
  6. Подписи собираются 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() — вычисление адреса wallet
  • mint() — минтинг токенов (вызывается 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 токен)

EVM → TVM | Alien token

Сценарий: Пользователь переводит 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 запрашивает конфигурацию и адрес токена

Детали:

  1. Event-контракт вызывает EvmTvmEventConfiguration.getDetails() — получает адрес Proxy
  2. Event-контракт вызывает ProxyMultiVaultAlien.deriveEvmAlienTokenRoot() — вычисляет адрес alien токена
  3. Proxy возвращает детерминированный адрес alien токена
  4. 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 токены пользователю

Детали:

  1. ProxyMultiVaultAlien.onEventConfirmedExtended() получает данные события
  2. Если alien токен ещё не задеплоен — деплоит TokenRootAlienEvm
  3. Вызывает TokenRootAlienEvm.mint(recipient, amount)
  4. JettonMinter/TokenRoot деплоит JettonWallet/TokenWallet для пользователя (если ещё не существует)
  5. JettonWallet/TokenWallet получает alien токены

Шаг 9: Frontend обновляет статус

Действие: Frontend через Aggregator API получает статус "Completed"


Поток 2: TVM → EVM (Alien токен)

TVM → EVM | Alien token

Сценарий: Пользователь возвращает 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

Детали:

  1. ProxyMultiVaultAlien.onAcceptTokensBurn() получает данные burn
  2. Декодирует payload: {nonce, network, burnPayload{recipient, callback}, remainingGasTo}
  3. Формирует eventData с параметрами трансфера
  4. Вызывает TvmEvmEventConfiguration.deployEvent(eventVoteData)
  5. Event-контракт создаётся с данными: {nonce, proxy, token, remainingGasTo, amount, recipient, sender, callback}

Шаг 3: Event-контракт верифицирует токен

Действие: MultiVaultTvmEvmEventAlien проверяет, что токен действительно alien

Детали:

  1. Event-контракт запрашивает метаданные токена (getInfo / takeInfo)
  2. Получает {base_chainId, base_token, name, symbol, decimals}
  3. Вызывает ProxyMultiVaultAlien.deriveEvmAlienTokenRoot() для проверки
  4. Сравнивает полученный адрес с 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)

Детали:

  1. MultiVault декодирует payload и верифицирует signatures через публичные ключи relay-нод
  2. Проверяет, что withdrawal ещё не был выполнен (withdrawalIds[payloadId])
  3. Декодирует данные: {token, amount, recipient, chainId, callback}
  4. Проверяет, что chainId соответствует текущей сети
  5. Проверяет, что токен не в blacklist
  6. Вычисляет комиссию
  7. Проверяет withdrawal limits (может создать pending withdrawal вместо немедленного вывода)
  8. Если лимиты пройдены и баланс достаточен: IERC20(token).safeTransfer(recipient, amount - fee) — токены разлокованы, эмитирует Withdraw
  9. Иначе создаёт PendingWithdrawal

Важно

Подписи relay-нод необходимы для EVM стороны — EVM-контракт верифицирует эти подписи, чтобы убедиться, что трансфер был подтверждён достаточным количеством relay-нод. Подписи передаются в параметре метода confirm(signature, voteReceiver) и сохраняются в Event-контракте.


Поток 3: TVM → EVM (Native токен)

TVM → EVM | Native/Predeployed token

Сценарий: Пользователь переводит 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

Детали:

  1. ProxyMultiVaultNative.transferNotification() получает данные transfer
  2. Декодирует payload: {nonce, network, transferPayload{recipient, chainId, callback}, remainingGasTo}
  3. Получает метаданные токена (name, symbol, decimals)
  4. Формирует eventData
  5. Вызывает TvmEvmEventConfiguration.deployEvent(eventVoteData)

Шаг 3: Event-контракт верифицирует token wallet

Действие: MultiVaultTvmEvmEventNative проверяет, что tokenWallet принадлежит Proxy

Детали:

  1. Event-контракт вызывает TokenRoot.walletOf(proxy)
  2. Получает expectedTokenWallet
  3. Сравнивает с tokenWallet из eventData
  4. Если совпадает — 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)

Детали:

  1. MultiVault декодирует payload и верифицирует signatures
  2. Декодирует данные: {native{wid, addr}, meta{name, symbol, decimals}, amount, recipient, chainId, callback}
  3. Проверяет, что chainId соответствует текущей сети
  4. Вычисляет адрес токена:
    • Проверяет наличие predeployed токена для данного native адреса
    • Если predeployed существует — использует его
    • Иначе — вычисляет адрес через create2 и деплоит новый MultiVaultToken
  5. Проверяет withdrawal limits
  6. Если лимиты пройдены: IMultiVaultToken(token).mint(recipient, amount - fee) — токены минтятся, эмитирует Withdraw
  7. Иначе создаёт PendingWithdrawal

Поток 4: EVM → TVM (Native токен)

EVM → TVM | Native token

Сценарий: Пользователь возвращает 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

Детали:

  1. Event-контракт вызывает EvmTvmEventConfiguration.getDetails() — получает адрес Proxy
  2. Event-контракт вызывает TokenRoot.walletOf(proxy) — получает адрес token wallet Proxy

Шаги 5-7: Аналогично потоку 1

  • Event-контракт получает публичные ключи relay-нод
  • Relay-ноды вызывают confirm()
  • Событие Confirmed эмитируется

Шаг 8: Proxy разлокивает native токены

Действие: ProxyMultiVaultNative получает callback и делает transfer пользователю

Детали:

  1. ProxyMultiVaultNative.onEventConfirmedExtended() получает данные события
  2. Вызывает TokenWallet(proxyWallet).transfer({recipient: userWallet, amount})
  3. Native токены переводятся с wallet Proxy на wallet пользователя (unlock)

Шаг 9: Frontend обновляет статус


Риски и Edge Cases

Риск/ОшибкаПричинаЗащита/Решение
Недостаточный баланс для withdrawAlien токены залокованы, но баланс MultiVault недостаточенMultiVault создаёт PendingWithdrawal; user может установить bounty для стимулирования ликвидности
Превышение withdrawal limitsСлишком большой объём withdrawals за 24-часовой периодMultiVault отслеживает withdrawal volume; при превышении создаёт PendingWithdrawal со статусом ApproveStatus.Required; withdrawGuardian одобряет вручную
Event-контракт с недостаточным балансомБаланс Event-контракта меньше expected_gasEvent-контракт проверяет баланс в _onInit() и делает self-destruct с возвратом средств
Подделка подписей relay-нодЗлонамеренный актёр пытается вывести токеныMultiVault верифицирует подписи через публичные ключи relay-нод; проверяется достаточное количество уникальных подписей
Blacklisted токенТокен может быть скомпрометированGovernance может добавить токен в blacklist; все deposit/withdraw блокируются; emergency shutdown останавливает все операции
Неверный chainIdWithdraw 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 lowmsg.value < amount при депозите нативной валюты (ETH/BNB)
Deposit: limits violatedСумма депозита превышает установленные лимиты
Deposit amount too is largeamount >= 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 idwithdrawal.chainId != block.chainid — payload предназначен для другой сети
Withdraw: token is blacklistedТокен добавлен в blacklist governance
Withdraw: bounty > withdraw amountBounty превышает сумму вывода
Withdraw: already seenПовторная попытка вывода с тем же payload (replay protection)
Withdraw: invalid configurationEvent 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 largeBounty превышает сумму pending withdrawal
Pending: zero amountforceWithdraw() вызван для 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 metadecimals > DECIMALS_LIMIT или symbol.length > SYMBOL_LENGTH_LIMIT или name.length > NAME_LENGTH_LIMIT
Tokens: token is blacklistedОперация с токеном из blacklist
Tokens: weth is blacklistedWETH токен добавлен в 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 < undeclaredDaily 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 initializedDiamond уже инициализирован
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 configurationEvent 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 configurationEvent configuration DAO не совпадает
DAO: wrong chain idDAO-действие предназначено для другой сети
DAO: execution failИсполнение DAO-действия завершилось ошибкой
TokenFactory: not self callВызов фабрики токенов не через self-call

ChainConnect Bridge Documentation