Skip to content

Architecture of EVM-TVM Relay-Based Bridge

General Overview

The relay-based bridge is a multi-chain bridge system that enables token transfers between different blockchain types: EVM networks and TVM networks.

Users send tokens on one side and receive their equivalent on the other. The correctness of each transfer is ensured by a network of relay nodes — independent servers that monitor events in both blockchains.

Key Architectural Features

1. Consensus via Event Contracts in TVM Network

For each transfer, a special Event contract is created in the TVM network. The consensus mechanism differs depending on the transfer direction:

EVM→TVM: Consensus via Confirmations (confirm)

Relay nodes call the confirm() method without a signature — voting only:

  • Consensus is achieved by counting votes confirms >= requiredVotes
  • After confirmation, the Event contract immediately invokes a callback to the Proxy contract
  • Proxy executes the final action (mint/unlock tokens) in the TVM network
  • Signatures are not needed because the action happens within TVM
TVM→EVM: Consensus via Signatures (sign)

Relay nodes call the confirm() method with a signature of the event data:

  • Consensus is achieved by accumulating cryptographic signatures from relay nodes
  • Event contract stores signatures in the signatures mapping
  • Signatures are passed to the EVM contract MultiVault.saveWithdraw*()
  • EVM contract verifies signatures via relay node public keys
  • Signatures are required because EVM contracts cannot read TVM state
Comparison of Mechanisms
DirectionConfirm MethodSignatureConsensusFinal Action
EVM→TVMconfirm(voteReceiver)❌ NoVote countingCallback to Proxy (TVM)
TVM→EVMconfirm(signature, voteReceiver)✅ YesSignature accumulationsaveWithdraw*() with signature verification (EVM)

3. Diamond Pattern (EIP-2535)

MultiVault in EVM uses the Diamond pattern — a single contract address with multiple modules (facets):

  • MultiVaultFacetDeposit — token deposits
  • MultiVaultFacetWithdraw — token withdrawals with signature verification
  • MultiVaultFacetTokens — token registry (native/alien)
  • MultiVaultFacetFees — fee management
  • MultiVaultFacetLiquidity — liquidity pools
  • MultiVaultFacetSettings — configuration
  • MultiVaultFacetPendingWithdrawals — pending withdrawal queue: token rate-limiting (sliding window), governance or withdrawGuardian approval/rejection, bounty system for execution incentives, cancellation with return to TVM, forced withdrawal (emergency)

4. Two-Tier API Architecture



  • multivault-graph (multivault-graph-v2/) — indexes EVM contract events (Deposit, Withdraw, PendingWithdrawal). One instance is created for each EVM-TVM network pair. Two implementations: The Graph (subgraph) and Envio
  • BridgeAPI (ton-api/) — indexes TVM contract state (Event contracts, configurations, confirmation statuses) and fetches EVM events from multivault-graph. One instance per TVM network
  • Aggregator API (chainconnect-history-api/) — aggregator over BridgeAPI instances, provides a unified interface /payload/build, /transfers/search, /transfers/status

Glossary

TermMeaning
NativeTokens originally issued in the TVM network. The term is used relative to TVM regardless of transfer direction
AlienTokens originally issued in the EVM network. When bridged to TVM, they are minted as alien representations
Event (Event contract)Smart contract in the TVM network, created for each transfer. Serves as a "voting log": relay nodes call the confirm() method. When sufficient confirmations are reached — the contract is considered confirmed and triggers the next step (mint/unlock tokens)
ConfigurationEvent configuration contract in TVM, defines parameters for creating Event contracts and target addresses
ProxyProxy contract in TVM (ProxyMultiVaultNative/Alien), manages token minting/burning/transfer
MultiVaultMain bridge contract in EVM (Diamond pattern), stores locked tokens and manages mint/burn
relay nodeIndependent server that monitors events in both blockchains and confirms transfers: for EVM→TVM calls confirm(), for TVM→EVM calls confirm(signature)
confirmEvent contract method for confirming events. For EVM→TVM: confirm(voteReceiver) — vote only. For TVM→EVM: confirm(signature, voteReceiver) — vote + signature
signatureCryptographic signature from a relay node, passed in the confirm() parameter for TVM→EVM; used for verification in EVM contracts

Transfer Types

  1. EVM → TVM (Alien): lock alien in EVM (MultiVault), mint alien in TVM — transferring an EVM token to TVM
  2. TVM → EVM (Alien): burn alien in TVM, unlock alien in EVM — returning an EVM token back
  3. TVM → EVM (Native): lock native in TVM (Proxy), mint/create2 in EVM — transferring a TVM token to EVM
  4. EVM → TVM (Native): burn in EVM, unlock native in TVM — returning a TVM token back

Architectural Layers

The system is divided into 3 layers:

1. OFF-CHAIN (green layer in diagrams)

Indexers, API services, Frontend, relay nodes. This is the "glue" between blockchains: tracks events, builds transactions, signs confirmations.

2. EVM (yellow layer)

EVM network smart contracts: MultiVault (Diamond), ERC20 tokens.

3. TVM (blue layer)

TVM network smart contracts, including Event contracts (where on-chain relay node consensus is achieved), Proxy contracts, JettonMinter/TokenRoot, JettonWallet/TokenWallet.

System Components

OFF-CHAIN Layer

relay

Purpose: Main relay node — a full TVM node that monitors events in both blockchains.

Key Functions:

  • For EVM→TVM: deploys Event contract and calls confirm(voteReceiver) — records vote in TVM
  • For TVM→EVM: calls confirm(signature, voteReceiver) — records vote + cryptographic signature for EVM
  • Subscribes to Bridge contract events to receive configurations
  • Monitors events in both blockchains

Interacts With:

  • Event contracts in TVM (confirm calls)
  • Bridge contract in TVM (configuration retrieval)
  • EVM RPC endpoints (MultiVault event monitoring)

Gas Credit Backend

Purpose: Service for automatic gas pre-funding for cross-chain transfers between EVM and TVM networks.

How It Works: User pays for gas in the source network (including Event contract deployment payment), and the service automatically deploys the Event contract in the destination TVM network, converting gas costs at current USD exchange rates between native tokens.

Feature: Does not require a full TVM node, works via RPC. Unlike the main relay node (octusbridge-relay), it does not participate in event signing — only in Event contract deployment.


multivault-graph

Purpose: MultiVault event indexer in EVM network. Tracks Deposit, Withdraw, PendingWithdrawal events, etc.

Two Interchangeable Implementations:

  • The Graph (subgraph) — first implementation
  • Envio (multivault-graph-v2/) — second implementation, presented in this repository

Both implementations solve the same task: index MultiVault events and provide data via GraphQL API. Only one implementation is needed for operation. Two are supported for risk diversification, as they are external services.

Interacts With:

  • EVM RPC endpoints (event indexing)
  • BridgeAPI (data provision)

Bridge API

Purpose: Bridge backend API for indexing the TVM side and its associated EVM networks. Indexes TVM contract state (Event contracts, configurations, confirmation statuses) and fetches EVM events from multivault-graph instances.

Feature: One BridgeAPI instance serves one TVM network and all its associated EVM networks (via corresponding multivault-graphs).

Interacts With:

  • TVM network (Event contract indexing)
  • multivault-graph (fetching EVM events from associated EVM networks)
  • Aggregator API (data provision)

Bridge Aggregator API

Purpose: Aggregator over BridgeAPI instances. Combines data from multiple TVM bridges (each BridgeAPI serves its own TVM network with linked EVM networks) and provides a unified endpoint for bridge operations.

Key Endpoints:

  • /payload/build (POST) — build payload for transfer transaction
  • /transfers/search (POST) — search transfers with filtering
  • /transfers/status (POST) — get status of a specific transfer

Interacts With:

  • BridgeAPI instances (aggregating data from multiple TVM bridges)
  • Frontend (providing unified interface)

Frontend

Purpose: User web interface for executing transfers.

Interacts With:

  • Aggregator API (data retrieval and transaction building)
  • Wallet connections (Metamask, EverWallet, etc.)

gas-price-api

Purpose: Service for retrieving current gas prices for EVM networks.


EVM Layer

MultiVault

Purpose: Main bridge contract in EVM network. Implemented using Diamond pattern (EIP-2535) with multiple facets.

Key Methods:

  • deposit(DepositParams) — deposit tokens for transfer to TVM
  • depositByNativeToken(DepositNativeTokenParams) — deposit native currency (ETH, BNB, etc.)
  • saveWithdrawNative(bytes payload, bytes[] signatures) — withdraw Native tokens with relay node signature verification
  • saveWithdrawAlien(bytes payload, bytes[] signatures, uint bounty) — withdraw Alien tokens with relay node signature verification

Facets:

FacetPurpose
MultiVaultFacetDepositDeposit methods
MultiVaultFacetWithdrawWithdraw methods with signature verification
MultiVaultFacetTokensToken registry (native/alien, blacklist)
MultiVaultFacetFeesFee management
MultiVaultFacetLiquidityLiquidity pools
MultiVaultFacetSettingsConfiguration (governance, emergency shutdown)
MultiVaultFacetPendingWithdrawalsPending withdrawal queue: token rate-limiting (sliding window), governance/withdrawGuardian approval/rejection, bounty system, cancellation with return to TVM, forced withdrawal (emergency)

Events:

  • Deposit — token deposit
  • AlienTransfer — Alien token transfer (EVM → TVM)
  • NativeTransfer — Native token transfer (EVM → TVM)
  • Withdraw — token withdrawal
  • PendingWithdrawalCreated — pending withdrawal created

Interacts With:

  • ERC20 tokens (lock/unlock alien tokens)
  • MultiVaultToken (burn/mint native representations)
  • relay nodes (signature verification via public keys)

MultiVaultToken

Purpose: ERC20 token created by MultiVault via create2 to represent Native TVM tokens.

Feature: Created deterministically on first transfer of Native token from TVM to EVM.

Key Methods:

  • burn(address, uint256) — burning when returning Native token to TVM
  • mint(address, uint256) — minting when receiving Native token from TVM

TVM Layer

Key Principle

Relay node consensus is achieved on-chain in the TVM network. For each transfer, a separate Event contract is created that serves as a "voting ballot" — relay nodes call the confirm() method. For EVM→TVM it's just voting, for TVM→EVM — voting with a cryptographic signature. When sufficient confirmations are reached, the Event contract is considered confirmed.

Event Contracts

An Event contract is a one-time smart contract in TVM, created for a specific transfer. It's needed because two different blockchains (EVM and TVM) cannot directly "see" each other. The Event contract solves this problem: it stores transfer data and collects relay node confirmations.

Each transfer type uses its own Event contract type (Alien/Native x EVM→TVM/TVM→EVM). After confirmation, the Event contract invokes a callback to the Proxy contract to execute the final action (mint/transfer/unlock tokens).


MultiVaultEvmTvmEventAlien

Purpose: Event contract for EVM→TVM transfer of Alien token.

Lifecycle:

  1. Relay deploys contract via EvmTvmEventConfiguration
  2. Contract requests alien token address from Proxy (deriveEvmAlienTokenRoot)
  3. Contract receives relay node public keys from RoundDeployer
  4. Relay nodes call confirm (record confirmation)
  5. With sufficient signatures → Confirmed event
  6. Callback to ProxyMultiVaultAlien → onEventConfirmedExtended → mint alien token

Key Methods:

  • confirm() — relay node confirms event
  • receiveAlienTokenRoot(address _token) — receive alien token address
  • receiveConfigurationDetails() — receive configuration

Interacts With:

  • EvmTvmEventConfiguration (configuration retrieval)
  • ProxyMultiVaultAlien (callback after confirmation)
  • RoundDeployer (relay node public key retrieval)

MultiVaultEvmTvmEventNative

Purpose: Event contract for EVM→TVM transfer of Native token (Native token return).

Lifecycle:

  1. Relay deploys contract via EvmTvmEventConfiguration
  2. Contract requests Proxy's token wallet address (walletOf)
  3. Contract receives relay node public keys from RoundDeployer
  4. Relay nodes call confirm
  5. With sufficient signatures → Confirmed event
  6. Callback to ProxyMultiVaultNative → onEventConfirmedExtended → transfer (unlock) native token to user

MultiVaultTvmEvmEventAlien

Purpose: Event contract for TVM→EVM transfer of Alien token (Alien token return).

Lifecycle:

  1. ProxyMultiVaultAlien deploys contract via TvmEvmEventConfiguration (on alien token burn)
  2. Contract verifies alien token (deriveEvmAlienTokenRoot)
  3. Contract receives relay node public keys from RoundDeployer
  4. Relay nodes call confirm(signature, voteReceiver) — record vote and cryptographic signature for EVM
  5. With sufficient signatures → Confirmed event
  6. Signatures are collected by Aggregator API and passed to EVM for MultiVault.saveWithdrawAlien()

Key Methods:

  • confirm(signature, voteReceiver) — relay node records vote + signature for EVM
  • receiveAlienTokenRoot() — alien token verification
  • getDecodedData() — retrieve event data for signing

MultiVaultTvmEvmEventNative

Purpose: Event contract for TVM→EVM transfer of Native token.

Lifecycle:

  1. ProxyMultiVaultNative deploys contract via TvmEvmEventConfiguration (on native token transfer)
  2. Contract verifies Proxy's token wallet (walletOf)
  3. Contract receives relay node public keys from RoundDeployer
  4. Relay nodes call confirm(signature, voteReceiver) — record vote and cryptographic signature for EVM
  5. With sufficient signatures → Confirmed event
  6. Signatures are collected by Aggregator API and passed to EVM for MultiVault.saveWithdrawNative()

EvmTvmEventConfiguration

Purpose: Configuration for EVM→TVM direction events. Defines parameters for Event contract creation.

Key Parameters:

  • proxy — Proxy contract address (ProxyMultiVaultAlien or ProxyMultiVaultNative)
  • startBlockNumber / endBlockNumber — EVM block range for monitoring
  • eventInitialBalance — Event contract initial balance

Key Methods:

  • deployEvent(EvmTvmEventVoteData) — deploy new Event contract
  • getDetails() — retrieve configuration parameters

TvmEvmEventConfiguration

Purpose: Configuration for TVM→EVM direction events.


ProxyMultiVaultAlien

Purpose: Proxy for managing Alien tokens in TVM (EVM token representations).

For EVM→TVM (mint):

  • onEventConfirmedExtended() — callback from MultiVaultEvmTvmEventAlien after confirmation
  • deriveEvmAlienTokenRoot() — calculate deterministic alien token address
  • deployEvmAlienToken() — deploy alien token (if doesn't exist yet)
  • _mintTokens() — mint alien tokens to user

For TVM→EVM (burn):

  • onAcceptTokensBurn() — callback from TokenWallet on burn
  • _deployEvmEvent() — deploy MultiVaultTvmEvmEventAlien to initiate transfer

ProxyMultiVaultNative

Purpose: Proxy for managing Native tokens in TVM (lock/unlock TVM tokens).

For TVM→EVM (lock):

  • transferNotification() — callback from TokenWallet on transfer
  • Proxy locks (accepts to its wallet) native tokens
  • _deployEvmEvent() — deploy MultiVaultTvmEvmEventNative to initiate transfer

For EVM→TVM (unlock):

  • onEventConfirmedExtended() — callback from MultiVaultEvmTvmEventNative after confirmation
  • Transfer native tokens from Proxy wallet to user wallet

TokenRootAlienEvm

Purpose: Root contract of Alien token in TVM (represents EVM token).

Key Methods:

  • mint() — mint alien tokens (called by ProxyMultiVaultAlien)
  • deployWallet() — deploy TokenWallet for user

JettonMinter/TokenRoot

Purpose: Root contract of Native TVM token. In the TVM ecosystem, there are two versions of token standards: TIP-3 (TokenRoot/TokenWallet) and Jetton (JettonMinter/JettonWallet). Contracts are functionally equivalent.

Key Methods (TIP-3 — TokenRoot):

  • deployWallet() — deploy TokenWallet for user
  • walletOf() — calculate wallet address
  • mint() — mint tokens (called by owner)
  • burn() — burn tokens

Key Methods (Jetton — JettonMinter):

  • mint() — mint jettons
  • get_jetton_data() — retrieve jetton data (total_supply, admin, content, wallet_code)
  • get_wallet_address(owner) — calculate JettonWallet address by owner address

JettonWallet/TokenWallet

Purpose: User wallet for a specific token.

Key Methods (TIP-3 — TokenWallet):

  • transfer() — transfer tokens (for Native tokens → initiates TVM→EVM transfer via ProxyMultiVaultNative)
  • burn() — burn tokens (for Alien tokens → initiates TVM→EVM transfer via ProxyMultiVaultAlien)
  • balance() — get balance

Key Methods (Jetton — JettonWallet):

  • send_tokens() / transfer() — transfer jettons (analog of transfer() in TIP-3)
  • burn_tokens() / burn() — burn jettons (analog of burn() in TIP-3)
  • get_wallet_data() — retrieve wallet data (balance, owner, jetton_master, wallet_code)

RoundDeployer

Purpose: Manages relay node rounds. Provides public keys of active relay nodes for signature verification.

Feature: Relay node rounds change periodically, ensuring rotation of the relay node set for security.


CellEncoderStandalone

Purpose: Encodes event data for relay node signing.


MergeRouter / MergePool

Purpose: Router and pool for merge pool operations (optional path for Alien tokens).

Feature: Allows combining tokens from different networks via liquidity pools.


Transfer Flows

Flow 1: EVM → TVM (Alien Token)

EVM → TVM | Alien token

Scenario: User transfers an Alien token (originally issued in EVM) from EVM network to TVM network.

Step 1: Deposit in EVM

Action: User calls MultiVault.deposit()

Details:

  • User calls deposit({token, amount, recipient, expected_gas, payload})
  • MultiVault checks that token is not blacklisted
  • Determines that token is Alien (isNative = false)
  • MultiVault calls IERC20(token).safeTransferFrom(user, MultiVault, amount) — tokens locked in MultiVault
  • Fee is calculated (_calculateMovementFee)
  • Deposit and AlienTransfer events are emitted

Step 2: Event Indexing

Action: multivault-graph indexes Deposit + AlienTransfer event

Details:

  • Envio indexer monitors MultiVault events
  • Data is saved to GraphQL database
  • BridgeAPI and Aggregator API receive information about new deposit
  • Frontend shows user "Pending" status

Step 3: Relay Deploys Event Contract

Action: Relay node receives event and deploys MultiVaultEvmTvmEventAlien

Details:

  • Relay monitors EVM events via RPC
  • Relay calls EvmTvmEventConfiguration.deployEvent(eventVoteData)
  • EvmTvmEventConfiguration creates new MultiVaultEvmTvmEventAlien contract
  • Event contract is initialized with data: {base_chainId, base_token, amount, recipient, name, symbol, decimals}

Step 4: Event Contract Requests Data

Action: MultiVaultEvmTvmEventAlien requests configuration and token address

Details:

  1. Event contract calls EvmTvmEventConfiguration.getDetails() — receives Proxy address
  2. Event contract calls ProxyMultiVaultAlien.deriveEvmAlienTokenRoot() — calculates alien token address
  3. Proxy returns deterministic alien token address
  4. Event contract checks if token exists (getInfo)

Step 5: Event Contract Receives Relay Node Public Keys

Action: Event contract requests active relay node public keys from RoundDeployer

Details:

  • Event contract calls RoundDeployer.getRelayRoundAddressFromTimestamp()
  • Receives list of public keys of current round relay nodes
  • These keys will be used for signature verification (confirm)

Step 6: Relay Nodes Confirm Event

Action: Relay nodes call MultiVaultEvmTvmEventAlien.confirm()

Details:

  • Each relay node verifies event data
  • Relay node calls confirm() with external message signed by its private key
  • Event contract verifies signature via relay node public key
  • Event contract records relay node vote
  • When confirmation threshold is reached → Event contract transitions to Confirmed status

Step 7: Confirmed Event

Action: Event contract emits Confirmed event and invokes callback

Details:

  • Event contract emits Confirmed event
  • Callback is invoked in ProxyMultiVaultAlien: onEventConfirmedExtended()

Step 8: Proxy Mints Alien Tokens

Action: ProxyMultiVaultAlien receives callback and mints alien tokens to user

Details:

  1. ProxyMultiVaultAlien.onEventConfirmedExtended() receives event data
  2. If alien token not yet deployed — deploys TokenRootAlienEvm
  3. Calls TokenRootAlienEvm.mint(recipient, amount)
  4. JettonMinter/TokenRoot deploys JettonWallet/TokenWallet for user (if doesn't exist yet)
  5. JettonWallet/TokenWallet receives alien tokens

Step 9: Frontend Updates Status

Action: Frontend receives "Completed" status via Aggregator API


Flow 2: TVM → EVM (Alien Token)

TVM → EVM | Alien token

Scenario: User returns an Alien token from TVM network back to EVM network.

Step 1: Burn Alien Token in TVM

Action: User calls TokenWallet.burn()

Details:

  • User calls burn({amount, recipient, payload})
  • TokenWallet burns tokens
  • TokenWallet calls callback in TokenRootAlienEvm: onAcceptTokensBurn()
  • TokenRoot calls callback in ProxyMultiVaultAlien: onAcceptTokensBurn()

Step 2: Proxy Deploys Event Contract

Action: ProxyMultiVaultAlien deploys MultiVaultTvmEvmEventAlien

Details:

  1. ProxyMultiVaultAlien.onAcceptTokensBurn() receives burn data
  2. Decodes payload: {nonce, network, burnPayload{recipient, callback}, remainingGasTo}
  3. Forms eventData with transfer parameters
  4. Calls TvmEvmEventConfiguration.deployEvent(eventVoteData)
  5. Event contract is created with data: {nonce, proxy, token, remainingGasTo, amount, recipient, sender, callback}

Step 3: Event Contract Verifies Token

Action: MultiVaultTvmEvmEventAlien checks that token is actually alien

Details:

  1. Event contract requests token metadata (getInfo / takeInfo)
  2. Receives {base_chainId, base_token, name, symbol, decimals}
  3. Calls ProxyMultiVaultAlien.deriveEvmAlienTokenRoot() for verification
  4. Compares received address with expectedToken

Step 4: Event Contract Receives Relay Node Public Keys

Action: Similar to Flow 1 — request from RoundDeployer

Step 5: Relay Nodes Confirm with Signatures

Action: Relay nodes call MultiVaultTvmEvmEventAlien.confirm(signature, voteReceiver)

Details:

  • Each relay node verifies event data
  • Relay node calls confirm(signature, voteReceiver) — records vote + cryptographic signature in Event contract
  • Signatures are saved in signatures[relay] mapping for subsequent EVM verification
  • When confirmation threshold is reached → Event contract transitions to Confirmed status

Step 6: Confirmed Event

Action: Event contract emits Confirmed event

Details:

  • BridgeAPI indexes Confirmed event in TVM
  • Aggregator API receives data about confirmed Event contract
  • Aggregator API collects signatures from signatures mapping for passing to EVM

Step 7: Aggregator API Provides Payload

Action: Frontend requests payload via /transfers/status

Details:

  • Aggregator API returns payload (encoded event data) and signatures (array of relay node signatures)
  • Frontend receives ready data for calling MultiVault.saveWithdrawAlien()

Step 8: Token Withdrawal in EVM

Action: User (or relay) calls MultiVault.saveWithdrawAlien(payload, signatures)

Details:

  1. MultiVault decodes payload and verifies signatures via relay node public keys
  2. Checks that withdrawal has not been executed yet (withdrawalIds[payloadId])
  3. Decodes data: {token, amount, recipient, chainId, callback}
  4. Checks that chainId matches current network
  5. Checks that token is not blacklisted
  6. Calculates fee
  7. Checks withdrawal limits (may create pending withdrawal instead of immediate withdrawal)
  8. If limits passed and balance sufficient: IERC20(token).safeTransfer(recipient, amount - fee) — tokens unlocked, emits Withdraw
  9. Otherwise creates PendingWithdrawal

Important

Relay node signatures are necessary for the EVM side — the EVM contract verifies these signatures to ensure the transfer was confirmed by a sufficient number of relay nodes. Signatures are passed in the confirm(signature, voteReceiver) method parameter and stored in the Event contract.


Flow 3: TVM → EVM (Native Token)

TVM → EVM | Native/Predeployed token

Scenario: User transfers a Native token (originally issued in TVM) from TVM network to EVM network.

Step 1: Transfer Native Token to Proxy

Action: User calls TokenWallet.transfer() to ProxyMultiVaultNative address

Details:

  • User calls transfer({amount, recipient: ProxyWallet, payload})
  • JettonWallet/TokenWallet transfers tokens to Proxy token wallet (lock)
  • JettonWallet/TokenWallet calls callback in ProxyMultiVaultNative: transferNotification()

Step 2: Proxy Deploys Event Contract

Action: ProxyMultiVaultNative receives callback and deploys MultiVaultTvmEvmEventNative

Details:

  1. ProxyMultiVaultNative.transferNotification() receives transfer data
  2. Decodes payload: {nonce, network, transferPayload{recipient, chainId, callback}, remainingGasTo}
  3. Retrieves token metadata (name, symbol, decimals)
  4. Forms eventData
  5. Calls TvmEvmEventConfiguration.deployEvent(eventVoteData)

Step 3: Event Contract Verifies Token Wallet

Action: MultiVaultTvmEvmEventNative checks that tokenWallet belongs to Proxy

Details:

  1. Event contract calls TokenRoot.walletOf(proxy)
  2. Receives expectedTokenWallet
  3. Compares with tokenWallet from eventData
  4. If matches — Event is valid, otherwise — Rejected

Steps 4-6: Similar to Flow 2

  • Event contract receives relay node public keys
  • Relay nodes call confirm(signature, voteReceiver) — record vote + signature for EVM
  • Confirmed event is emitted

Step 7: Aggregator API Provides Payload

Action: Frontend receives payload and signatures via Aggregator API

Step 8: Mint Native Token in EVM

Action: User (or relay) calls MultiVault.saveWithdrawNative(payload, signatures)

Details:

  1. MultiVault decodes payload and verifies signatures
  2. Decodes data: {native{wid, addr}, meta{name, symbol, decimals}, amount, recipient, chainId, callback}
  3. Checks that chainId matches current network
  4. Calculates token address:
    • Checks for predeployed token for given native address
    • If predeployed exists — uses it
    • Otherwise — calculates address via create2 and deploys new MultiVaultToken
  5. Checks withdrawal limits
  6. If limits passed: IMultiVaultToken(token).mint(recipient, amount - fee) — tokens minted, emits Withdraw
  7. Otherwise creates PendingWithdrawal

Flow 4: EVM → TVM (Native Token)

EVM → TVM | Native token

Scenario: User returns a Native token from EVM network back to TVM network.

Step 1: Burn Native Token in EVM

Action: User calls MultiVault.deposit()

Details:

  • User calls deposit({token: nativeToken, amount, recipient, expected_gas, payload})
  • MultiVault checks that token is not blacklisted
  • Determines that token is Native (isNative = true)
  • MultiVault calls IMultiVaultToken(token).burn(user, amount) — Native tokens burned
  • Fee is calculated
  • Deposit and NativeTransfer events are emitted

Step 2: Event Indexing

Action: multivault-graph indexes Deposit + NativeTransfer

Step 3: Relay Deploys Event Contract

Action: Relay node deploys MultiVaultEvmTvmEventNative

Details:

  • Relay calls EvmTvmEventConfiguration.deployEvent(eventVoteData)
  • Event contract is created with data: {token_wid, token_addr, amount, recipient_wid, recipient_addr, value, expected_gas, payload}

Step 4: Event Contract Requests Data

Action: MultiVaultEvmTvmEventNative requests configuration and Proxy token wallet

Details:

  1. Event contract calls EvmTvmEventConfiguration.getDetails() — receives Proxy address
  2. Event contract calls TokenRoot.walletOf(proxy) — receives Proxy token wallet address

Steps 5-7: Similar to Flow 1

  • Event contract receives relay node public keys
  • Relay nodes call confirm()
  • Confirmed event is emitted

Step 8: Proxy Unlocks Native Tokens

Action: ProxyMultiVaultNative receives callback and transfers to user

Details:

  1. ProxyMultiVaultNative.onEventConfirmedExtended() receives event data
  2. Calls TokenWallet(proxyWallet).transfer({recipient: userWallet, amount})
  3. Native tokens are transferred from Proxy wallet to user wallet (unlock)

Step 9: Frontend Updates Status


Risks and Edge Cases

Risk/ErrorCauseProtection/Solution
Insufficient balance for withdrawAlien tokens locked but MultiVault balance insufficientMultiVault creates PendingWithdrawal; user can set bounty to incentivize liquidity
Withdrawal limits exceededToo large withdrawal volume in 24-hour periodMultiVault tracks withdrawal volume; when exceeded creates PendingWithdrawal with status ApproveStatus.Required; withdrawGuardian approves manually
Event contract with insufficient balanceEvent contract balance less than expected_gasEvent contract checks balance in _onInit() and self-destructs with funds return
Forged relay node signaturesMalicious actor attempts to withdraw tokensMultiVault verifies signatures via relay node public keys; checks for sufficient unique signatures
Blacklisted tokenToken may be compromisedGovernance can add token to blacklist; all deposit/withdraw blocked; emergency shutdown stops all operations
Wrong chainIdWithdraw event signed for one network, called in anotherMultiVault checks withdrawal.chainId == block.chainid
Replay attack (repeated withdraw)Same withdrawal event called multiple timesMultiVault tracks withdrawalIds[keccak256(payload)]; repeated call blocked
Incorrect Event contract verificationEvent contract with wrong token/wallet passes confirmationFor Alien: verification via deriveEvmAlienTokenRoot(). For Native: check walletOf(proxy) == tokenWallet. If mismatch — Rejected

EVM Contract Errors (require/revert)

All EVM contracts use string error messages (not custom errors). Below is a complete registry of errors, grouped by category.

Deposit (MultiVaultFacetDeposit)

Error MessageCondition
Msg value to lowmsg.value < amount when depositing native currency (ETH/BNB)
Deposit: limits violatedDeposit amount exceeds set limits
Deposit amount too is largeamount >= type(uint128).max — amount doesn't fit in uint128 for TVM
Pending: already filledAttempt to fill already closed pending withdrawal
Pending: wrong tokenDeposit token doesn't match pending withdrawal token
Pending: deposit insufficientDeposit amount insufficient to cover pending withdrawal

Withdraw (MultiVaultFacetWithdraw, MultiVaultHelperWithdraw)

Error MessageCondition
Withdraw: wrong chain idwithdrawal.chainId != block.chainid — payload intended for another network
Withdraw: token is blacklistedToken added to blacklist by governance
Withdraw: bounty > withdraw amountBounty exceeds withdrawal amount
Withdraw: already seenRepeated withdrawal attempt with same payload (replay protection)
Withdraw: invalid configurationEvent configuration doesn't match registered in contract
(no message)verifySignedTvmEvent() returned non-zero result — relay node signatures invalid

Pending Withdrawals (MultiVaultFacetPendingWithdrawals, MultiVaultHelperPendingWithdrawal)

Error MessageCondition
Pending: amount is zeroAttempt to operate on pending withdrawal with amount == 0
Pending: native tokenAttempt to set bounty or cancel pending withdrawal for native token
Pending: bounty too largeBounty exceeds pending withdrawal amount
Pending: zero amountforceWithdraw() called for pending withdrawal with zero amount
Pending: wrong amountCancellation amount <= 0 or > pending withdrawal amount
Pending: wrong current approve statusApprove/reject operation called for withdrawal not in Required status
Pending: wrong approve statusInvalid approve status passed (not Approved/Rejected)
Pending: params mismatchIn batch operation, pendingWithdrawalId and approveStatus arrays of different length
Pending: wrong approve statusAttempt to decrease pending withdrawal amount with invalid approve status

Tokens (MultiVaultHelperTokens)

Error MessageCondition
Tokens: invalid token metadecimals > DECIMALS_LIMIT or symbol.length > SYMBOL_LENGTH_LIMIT or name.length > NAME_LENGTH_LIMIT
Tokens: token is blacklistedOperation with token from blacklist
Tokens: weth is blacklistedWETH token added to blacklist
Tokens: invalid tokenDeployed token address doesn't match expected (create2 mismatch)

Liquidity (MultiVaultFacetLiquidity)

Error MessageCondition
Liquidity: token is nativeAttempt to create LP for native token (LP available only for alien)
Liquidity: only governance or managementInitial LP mint called not by governance/management
Liquidity: amount is too smallInitial LP mint < 1000 wei
Liquidity: recipient is not governanceInitial LP mint recipient is not governance
Liquidity: LP not activatedOperation with LP that is not yet activated

Access Control (MultiVaultHelperActors)

Error MessageCondition
Actors: only pending governanceCall available only to pending governance
Actors: only governanceCall available only to governance
Actors: only governance or managementCall available only to governance or management
Actors: only governance or withdraw guardianCall available only to governance or withdrawGuardian

Settings (MultiVaultFacetSettings)

Error MessageCondition
Settings: wrong bridgeBridge address == address(0) at initialization
Settings: wrong governanceGovernance address == address(0) at initialization
Settings: wrong wethWETH address == address(0) at initialization
Settings: daily limit < undeclaredDaily withdrawal limit less than undeclared limit
Settings: only guardian or governanceEmergency shutdown activation attempt not by guardian/governance
Settings: only governanceEmergency shutdown deactivation attempt not by governance

Emergency and Security

Error MessageCondition
Emergency: shutdownOperation called during emergency shutdown
ReentrancyGuard: reentrant callReentrancy attempt detected
Fee: limit exceededFee exceeds FEE_LIMIT
Callback: cant call itselfCallback attempt to MultiVault address itself
Callback: strict call failedStrict callback failed with error
Gas: failed to send gas to donorFailed to send gas to donor
Cache: payload already seenRepeated processing of already processed payload

Initialization (MultiVaultHelperInitializable)

Error MessageCondition
Initializable: contract is already initializedRepeated contract initialization
Initializable: contract is not initializingCall to onlyInitializing method outside initialization
Initializable: contract is initializingCall to _disableInitializers() during initialization

Diamond Pattern (DiamondStorage)

Error MessageCondition
LibDiamond: Must be contract ownerCall available only to Diamond owner
DiamondStorage: already initializedDiamond already initialized
DiamondStorage: Incorrect FacetCutActionInvalid action in diamondCut
DiamondStorage: No selectors in facet to cutEmpty selector array when adding/replacing/removing facet
DiamondStorage: Add facet can't be address(0)Facet address == address(0) when adding/replacing
DiamondStorage: Can't add function that already existsAttempt to add already existing selector
DiamondStorage: Can't replace function with same functionReplacing facet with the same one
DiamondStorage: Remove facet address must be address(0)Facet address != address(0) when removing
DiamondStorage: Can't remove function that doesn't existRemoving non-existent selector
DiamondStorage: Can't remove immutable functionRemoving function from immutable facet (address(this))
DiamondStorage: _init function revertedError executing init function after diamondCut

Bridge and DAO

Error MessageCondition
Bridge: renounce ownership is not allowedBridge ownership renouncement attempt
Bridge: initial round end should be in the future_initialRoundEnd < block.timestamp at initialization
Bridge: signature recover failedSignature recovery error (signer == address(0) or RecoverError)
Bridge: sender not round submitterforceRoundRelays() call not by roundSubmitter
Bridge: signatures verification failedTVM event signature verification failed
Bridge: wrong event configurationEvent configuration doesn't match roundRelaysConfiguration
Bridge: wrong roundRound number not equal to lastRound + 1 (sequence violated)
Bridge: signatures sequence wrongSignatures not sorted by ascending signer address
DAO: renounce ownership is not allowedDAO ownership renouncement attempt
DAO: zero addressZero address passed to DAO method
DAO: signatures verification failedDAO signature verification failed
DAO: wrong event configurationDAO event configuration doesn't match
DAO: wrong chain idDAO action intended for another network
DAO: execution failDAO action execution failed with error
TokenFactory: not self callToken factory call not via self-call

ChainConnect Bridge Documentation