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
signaturesmapping - 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
| Direction | Confirm Method | Signature | Consensus | Final Action |
|---|---|---|---|---|
| EVM→TVM | confirm(voteReceiver) | ❌ No | Vote counting | Callback to Proxy (TVM) |
| TVM→EVM | confirm(signature, voteReceiver) | ✅ Yes | Signature accumulation | saveWithdraw*() 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
| Term | Meaning |
|---|---|
| Native | Tokens originally issued in the TVM network. The term is used relative to TVM regardless of transfer direction |
| Alien | Tokens 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) |
| Configuration | Event configuration contract in TVM, defines parameters for creating Event contracts and target addresses |
| Proxy | Proxy contract in TVM (ProxyMultiVaultNative/Alien), manages token minting/burning/transfer |
| MultiVault | Main bridge contract in EVM (Diamond pattern), stores locked tokens and manages mint/burn |
| relay node | Independent server that monitors events in both blockchains and confirms transfers: for EVM→TVM calls confirm(), for TVM→EVM calls confirm(signature) |
| confirm | Event contract method for confirming events. For EVM→TVM: confirm(voteReceiver) — vote only. For TVM→EVM: confirm(signature, voteReceiver) — vote + signature |
| signature | Cryptographic signature from a relay node, passed in the confirm() parameter for TVM→EVM; used for verification in EVM contracts |
Transfer Types
- EVM → TVM (Alien): lock alien in EVM (MultiVault), mint alien in TVM — transferring an EVM token to TVM
- TVM → EVM (Alien): burn alien in TVM, unlock alien in EVM — returning an EVM token back
- TVM → EVM (Native): lock native in TVM (Proxy), mint/create2 in EVM — transferring a TVM token to EVM
- 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 TVMdepositByNativeToken(DepositNativeTokenParams)— deposit native currency (ETH, BNB, etc.)saveWithdrawNative(bytes payload, bytes[] signatures)— withdraw Native tokens with relay node signature verificationsaveWithdrawAlien(bytes payload, bytes[] signatures, uint bounty)— withdraw Alien tokens with relay node signature verification
Facets:
| Facet | Purpose |
|---|---|
| MultiVaultFacetDeposit | Deposit methods |
| MultiVaultFacetWithdraw | Withdraw methods with signature verification |
| MultiVaultFacetTokens | Token registry (native/alien, blacklist) |
| MultiVaultFacetFees | Fee management |
| MultiVaultFacetLiquidity | Liquidity pools |
| MultiVaultFacetSettings | Configuration (governance, emergency shutdown) |
| MultiVaultFacetPendingWithdrawals | Pending withdrawal queue: token rate-limiting (sliding window), governance/withdrawGuardian approval/rejection, bounty system, cancellation with return to TVM, forced withdrawal (emergency) |
Events:
Deposit— token depositAlienTransfer— Alien token transfer (EVM → TVM)NativeTransfer— Native token transfer (EVM → TVM)Withdraw— token withdrawalPendingWithdrawalCreated— 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 TVMmint(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:
- Relay deploys contract via EvmTvmEventConfiguration
- Contract requests alien token address from Proxy (
deriveEvmAlienTokenRoot) - Contract receives relay node public keys from RoundDeployer
- Relay nodes call
confirm(record confirmation) - With sufficient signatures →
Confirmedevent - Callback to ProxyMultiVaultAlien →
onEventConfirmedExtended→ mint alien token
Key Methods:
confirm()— relay node confirms eventreceiveAlienTokenRoot(address _token)— receive alien token addressreceiveConfigurationDetails()— 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:
- Relay deploys contract via EvmTvmEventConfiguration
- Contract requests Proxy's token wallet address (
walletOf) - Contract receives relay node public keys from RoundDeployer
- Relay nodes call
confirm - With sufficient signatures →
Confirmedevent - 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:
- ProxyMultiVaultAlien deploys contract via TvmEvmEventConfiguration (on alien token burn)
- Contract verifies alien token (
deriveEvmAlienTokenRoot) - Contract receives relay node public keys from RoundDeployer
- Relay nodes call
confirm(signature, voteReceiver)— record vote and cryptographic signature for EVM - With sufficient signatures →
Confirmedevent - Signatures are collected by Aggregator API and passed to EVM for
MultiVault.saveWithdrawAlien()
Key Methods:
confirm(signature, voteReceiver)— relay node records vote + signature for EVMreceiveAlienTokenRoot()— alien token verificationgetDecodedData()— retrieve event data for signing
MultiVaultTvmEvmEventNative
Purpose: Event contract for TVM→EVM transfer of Native token.
Lifecycle:
- ProxyMultiVaultNative deploys contract via TvmEvmEventConfiguration (on native token transfer)
- Contract verifies Proxy's token wallet (
walletOf) - Contract receives relay node public keys from RoundDeployer
- Relay nodes call
confirm(signature, voteReceiver)— record vote and cryptographic signature for EVM - With sufficient signatures →
Confirmedevent - 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 monitoringeventInitialBalance— Event contract initial balance
Key Methods:
deployEvent(EvmTvmEventVoteData)— deploy new Event contractgetDetails()— 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 confirmationderiveEvmAlienTokenRoot()— calculate deterministic alien token addressdeployEvmAlienToken()— 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 userwalletOf()— calculate wallet addressmint()— mint tokens (called by owner)burn()— burn tokens
Key Methods (Jetton — JettonMinter):
mint()— mint jettonsget_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 oftransfer()in TIP-3)burn_tokens()/burn()— burn jettons (analog ofburn()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)

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) DepositandAlienTransferevents 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) EvmTvmEventConfigurationcreates newMultiVaultEvmTvmEventAliencontract- 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:
- Event contract calls
EvmTvmEventConfiguration.getDetails()— receives Proxy address - Event contract calls
ProxyMultiVaultAlien.deriveEvmAlienTokenRoot()— calculates alien token address - Proxy returns deterministic alien token address
- 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
Confirmedstatus
Step 7: Confirmed Event
Action: Event contract emits Confirmed event and invokes callback
Details:
- Event contract emits
Confirmedevent - Callback is invoked in ProxyMultiVaultAlien:
onEventConfirmedExtended()
Step 8: Proxy Mints Alien Tokens
Action: ProxyMultiVaultAlien receives callback and mints alien tokens to user
Details:
ProxyMultiVaultAlien.onEventConfirmedExtended()receives event data- If alien token not yet deployed — deploys
TokenRootAlienEvm - Calls
TokenRootAlienEvm.mint(recipient, amount) - JettonMinter/TokenRoot deploys JettonWallet/TokenWallet for user (if doesn't exist yet)
- JettonWallet/TokenWallet receives alien tokens
Step 9: Frontend Updates Status
Action: Frontend receives "Completed" status via Aggregator API
Flow 2: 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:
ProxyMultiVaultAlien.onAcceptTokensBurn()receives burn data- Decodes payload:
{nonce, network, burnPayload{recipient, callback}, remainingGasTo} - Forms eventData with transfer parameters
- Calls
TvmEvmEventConfiguration.deployEvent(eventVoteData) - 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:
- Event contract requests token metadata (
getInfo/takeInfo) - Receives
{base_chainId, base_token, name, symbol, decimals} - Calls
ProxyMultiVaultAlien.deriveEvmAlienTokenRoot()for verification - 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
Confirmedstatus
Step 6: Confirmed Event
Action: Event contract emits Confirmed event
Details:
- BridgeAPI indexes
Confirmedevent in TVM - Aggregator API receives data about confirmed Event contract
- Aggregator API collects signatures from
signaturesmapping 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) andsignatures(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:
- MultiVault decodes
payloadand verifiessignaturesvia relay node public keys - Checks that withdrawal has not been executed yet (
withdrawalIds[payloadId]) - Decodes data:
{token, amount, recipient, chainId, callback} - Checks that
chainIdmatches current network - Checks that token is not blacklisted
- Calculates fee
- Checks withdrawal limits (may create pending withdrawal instead of immediate withdrawal)
- If limits passed and balance sufficient:
IERC20(token).safeTransfer(recipient, amount - fee)— tokens unlocked, emitsWithdraw - 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)

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:
ProxyMultiVaultNative.transferNotification()receives transfer data- Decodes payload:
{nonce, network, transferPayload{recipient, chainId, callback}, remainingGasTo} - Retrieves token metadata (
name,symbol,decimals) - Forms eventData
- Calls
TvmEvmEventConfiguration.deployEvent(eventVoteData)
Step 3: Event Contract Verifies Token Wallet
Action: MultiVaultTvmEvmEventNative checks that tokenWallet belongs to Proxy
Details:
- Event contract calls
TokenRoot.walletOf(proxy) - Receives
expectedTokenWallet - Compares with
tokenWalletfrom eventData - 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 Confirmedevent 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:
- MultiVault decodes
payloadand verifiessignatures - Decodes data:
{native{wid, addr}, meta{name, symbol, decimals}, amount, recipient, chainId, callback} - Checks that
chainIdmatches current network - Calculates token address:
- Checks for predeployed token for given
nativeaddress - If predeployed exists — uses it
- Otherwise — calculates address via create2 and deploys new MultiVaultToken
- Checks for predeployed token for given
- Checks withdrawal limits
- If limits passed:
IMultiVaultToken(token).mint(recipient, amount - fee)— tokens minted, emitsWithdraw - Otherwise creates
PendingWithdrawal
Flow 4: 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
DepositandNativeTransferevents 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:
- Event contract calls
EvmTvmEventConfiguration.getDetails()— receives Proxy address - 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() Confirmedevent is emitted
Step 8: Proxy Unlocks Native Tokens
Action: ProxyMultiVaultNative receives callback and transfers to user
Details:
ProxyMultiVaultNative.onEventConfirmedExtended()receives event data- Calls
TokenWallet(proxyWallet).transfer({recipient: userWallet, amount}) - Native tokens are transferred from Proxy wallet to user wallet (unlock)
Step 9: Frontend Updates Status
Risks and Edge Cases
| Risk/Error | Cause | Protection/Solution |
|---|---|---|
| Insufficient balance for withdraw | Alien tokens locked but MultiVault balance insufficient | MultiVault creates PendingWithdrawal; user can set bounty to incentivize liquidity |
| Withdrawal limits exceeded | Too large withdrawal volume in 24-hour period | MultiVault tracks withdrawal volume; when exceeded creates PendingWithdrawal with status ApproveStatus.Required; withdrawGuardian approves manually |
| Event contract with insufficient balance | Event contract balance less than expected_gas | Event contract checks balance in _onInit() and self-destructs with funds return |
| Forged relay node signatures | Malicious actor attempts to withdraw tokens | MultiVault verifies signatures via relay node public keys; checks for sufficient unique signatures |
| Blacklisted token | Token may be compromised | Governance can add token to blacklist; all deposit/withdraw blocked; emergency shutdown stops all operations |
| Wrong chainId | Withdraw event signed for one network, called in another | MultiVault checks withdrawal.chainId == block.chainid |
| Replay attack (repeated withdraw) | Same withdrawal event called multiple times | MultiVault tracks withdrawalIds[keccak256(payload)]; repeated call blocked |
| Incorrect Event contract verification | Event contract with wrong token/wallet passes confirmation | For 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 Message | Condition |
|---|---|
Msg value to low | msg.value < amount when depositing native currency (ETH/BNB) |
Deposit: limits violated | Deposit amount exceeds set limits |
Deposit amount too is large | amount >= type(uint128).max — amount doesn't fit in uint128 for TVM |
Pending: already filled | Attempt to fill already closed pending withdrawal |
Pending: wrong token | Deposit token doesn't match pending withdrawal token |
Pending: deposit insufficient | Deposit amount insufficient to cover pending withdrawal |
Withdraw (MultiVaultFacetWithdraw, MultiVaultHelperWithdraw)
| Error Message | Condition |
|---|---|
Withdraw: wrong chain id | withdrawal.chainId != block.chainid — payload intended for another network |
Withdraw: token is blacklisted | Token added to blacklist by governance |
Withdraw: bounty > withdraw amount | Bounty exceeds withdrawal amount |
Withdraw: already seen | Repeated withdrawal attempt with same payload (replay protection) |
Withdraw: invalid configuration | Event configuration doesn't match registered in contract |
| (no message) | verifySignedTvmEvent() returned non-zero result — relay node signatures invalid |
Pending Withdrawals (MultiVaultFacetPendingWithdrawals, MultiVaultHelperPendingWithdrawal)
| Error Message | Condition |
|---|---|
Pending: amount is zero | Attempt to operate on pending withdrawal with amount == 0 |
Pending: native token | Attempt to set bounty or cancel pending withdrawal for native token |
Pending: bounty too large | Bounty exceeds pending withdrawal amount |
Pending: zero amount | forceWithdraw() called for pending withdrawal with zero amount |
Pending: wrong amount | Cancellation amount <= 0 or > pending withdrawal amount |
Pending: wrong current approve status | Approve/reject operation called for withdrawal not in Required status |
Pending: wrong approve status | Invalid approve status passed (not Approved/Rejected) |
Pending: params mismatch | In batch operation, pendingWithdrawalId and approveStatus arrays of different length |
Pending: wrong approve status | Attempt to decrease pending withdrawal amount with invalid approve status |
Tokens (MultiVaultHelperTokens)
| Error Message | Condition |
|---|---|
Tokens: invalid token meta | decimals > DECIMALS_LIMIT or symbol.length > SYMBOL_LENGTH_LIMIT or name.length > NAME_LENGTH_LIMIT |
Tokens: token is blacklisted | Operation with token from blacklist |
Tokens: weth is blacklisted | WETH token added to blacklist |
Tokens: invalid token | Deployed token address doesn't match expected (create2 mismatch) |
Liquidity (MultiVaultFacetLiquidity)
| Error Message | Condition |
|---|---|
Liquidity: token is native | Attempt to create LP for native token (LP available only for alien) |
Liquidity: only governance or management | Initial LP mint called not by governance/management |
Liquidity: amount is too small | Initial LP mint < 1000 wei |
Liquidity: recipient is not governance | Initial LP mint recipient is not governance |
Liquidity: LP not activated | Operation with LP that is not yet activated |
Access Control (MultiVaultHelperActors)
| Error Message | Condition |
|---|---|
Actors: only pending governance | Call available only to pending governance |
Actors: only governance | Call available only to governance |
Actors: only governance or management | Call available only to governance or management |
Actors: only governance or withdraw guardian | Call available only to governance or withdrawGuardian |
Settings (MultiVaultFacetSettings)
| Error Message | Condition |
|---|---|
Settings: wrong bridge | Bridge address == address(0) at initialization |
Settings: wrong governance | Governance address == address(0) at initialization |
Settings: wrong weth | WETH address == address(0) at initialization |
Settings: daily limit < undeclared | Daily withdrawal limit less than undeclared limit |
Settings: only guardian or governance | Emergency shutdown activation attempt not by guardian/governance |
Settings: only governance | Emergency shutdown deactivation attempt not by governance |
Emergency and Security
| Error Message | Condition |
|---|---|
Emergency: shutdown | Operation called during emergency shutdown |
ReentrancyGuard: reentrant call | Reentrancy attempt detected |
Fee: limit exceeded | Fee exceeds FEE_LIMIT |
Callback: cant call itself | Callback attempt to MultiVault address itself |
Callback: strict call failed | Strict callback failed with error |
Gas: failed to send gas to donor | Failed to send gas to donor |
Cache: payload already seen | Repeated processing of already processed payload |
Initialization (MultiVaultHelperInitializable)
| Error Message | Condition |
|---|---|
Initializable: contract is already initialized | Repeated contract initialization |
Initializable: contract is not initializing | Call to onlyInitializing method outside initialization |
Initializable: contract is initializing | Call to _disableInitializers() during initialization |
Diamond Pattern (DiamondStorage)
| Error Message | Condition |
|---|---|
LibDiamond: Must be contract owner | Call available only to Diamond owner |
DiamondStorage: already initialized | Diamond already initialized |
DiamondStorage: Incorrect FacetCutAction | Invalid action in diamondCut |
DiamondStorage: No selectors in facet to cut | Empty 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 exists | Attempt to add already existing selector |
DiamondStorage: Can't replace function with same function | Replacing 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 exist | Removing non-existent selector |
DiamondStorage: Can't remove immutable function | Removing function from immutable facet (address(this)) |
DiamondStorage: _init function reverted | Error executing init function after diamondCut |
Bridge and DAO
| Error Message | Condition |
|---|---|
Bridge: renounce ownership is not allowed | Bridge ownership renouncement attempt |
Bridge: initial round end should be in the future | _initialRoundEnd < block.timestamp at initialization |
Bridge: signature recover failed | Signature recovery error (signer == address(0) or RecoverError) |
Bridge: sender not round submitter | forceRoundRelays() call not by roundSubmitter |
Bridge: signatures verification failed | TVM event signature verification failed |
Bridge: wrong event configuration | Event configuration doesn't match roundRelaysConfiguration |
Bridge: wrong round | Round number not equal to lastRound + 1 (sequence violated) |
Bridge: signatures sequence wrong | Signatures not sorted by ascending signer address |
DAO: renounce ownership is not allowed | DAO ownership renouncement attempt |
DAO: zero address | Zero address passed to DAO method |
DAO: signatures verification failed | DAO signature verification failed |
DAO: wrong event configuration | DAO event configuration doesn't match |
DAO: wrong chain id | DAO action intended for another network |
DAO: execution fail | DAO action execution failed with error |
TokenFactory: not self call | Token factory call not via self-call |