Skip to content

Merge Logic (Alien Token Merging)

What is Merge

Fragmentation Problem

When the same token (e.g., USDT) exists on different EVM networks and is bridged to a TVM network, a separate alien representation is created in TVM for each EVM variant:

  • USDT from Ethereum network → alien token A in TVM
  • USDT from BSC network → alien token B in TVM
  • USDT from Avalanche network → alien token C in TVM

This creates a liquidity fragmentation problem: users cannot directly use token A instead of token B, even though they are essentially the same asset.

Solution via Merge Pool

Merge Pool allows combining these alien representations of one asset into a single pool, providing:

  1. Swap between alien representations: exchange token A for token B at a 1:1 ratio (with decimals normalization)
  2. Withdraw with conversion: withdraw any token from the pool to EVM with decimals conversion

Architecture

Components

ComponentDescription
MergePoolMain pool contract. Stores token list with their decimals and enabled status. Handles token burns and performs swap or withdraw.
MergeRouterRouter contract associated with a specific token. Stores the MergePool address to which the token belongs. Deployed separately for each alien token.
MergePoolPlatformPlatform contract for deploying MergePool via TVM state init mechanism. Accepts MergePool code and initializes it with given parameters.
ProxyMultiVaultAlienProxy contract that deploys MergePool and MergeRouter, manages versions, handles mint/withdraw requests from MergePool.

Interaction Diagram



Flow Description:

  1. User burns token A, passing payload with operation type (Swap/Withdraw) and target token
  2. MergePool receives onAcceptTokensBurn callback, decodes payload, converts decimals
  3. Depending on operation type:
    • Swap: MergePool calls mintTokensByMergePool on Proxy to mint target token
    • Withdraw: MergePool calls withdrawTokensToEvmByMergePool on Proxy to create withdraw event
  4. Proxy executes the corresponding action

Canonical Token (Canon Token)

Definition

Canonical token (canon token) is a designated token in MergePool that serves as the pool's reference point. Usually chosen as the token from the main/most liquid network (e.g., USDT from Ethereum for USDT pool).

Purpose

Canon token is used as the pool identifier and configuration anchor. The canon token cannot be removed from the pool — this protects configuration integrity.

Canon Token Management

Canon token is set during MergePool deployment via the _canonId parameter — index in the tokens array.

Canon token can be changed via the setCanon(address _token) method. Accessible to owner or manager. Validates that the token exists in the pool and is enabled.

Swap Operation

Swap Operation Flow

Step 1: User initiates burn

User burns token A, passing payload with burnType = Swap and the target token address.

Step 2: MergePool decodes and converts

MergePool calls _convertDecimals to normalize the amount between source and target tokens.

Step 3: Checks and mint

MergePool validates conditions (see Checks and Restrictions) and on success calls _mintTokens(targetToken, amount, walletOwner, remainingGasTo, operationPayload) on Proxy.

Decimals Normalization

The _convertDecimals function normalizes amounts when exchanging tokens with different decimals:

  • If decimals match — amount stays the same
  • If source token has more decimals — amount is divided by 10^(difference)
  • If source token has fewer decimals — amount is multiplied by 10^(difference)

Examples:

  • USDT (Ethereum, 6 decimals) → USDT (BSC, 18 decimals): amount × 10^12
  • USDT (BSC, 18 decimals) → USDT (Ethereum, 6 decimals): amount / 10^12
  • USDT (Ethereum, 6 decimals) → USDT (Avalanche, 6 decimals): amount (no change)

Swap Checks and Restrictions

1. Token is disabled

If target or source token is disabled, swap is not executed, burned tokens are returned instead

2. Amount after conversion = 0

If _convertDecimals results in 0, swap is not executed, original tokens are returned

This can occur when exchanging very small amounts from a token with large decimals to a token with small decimals.

Edge case example:

  • Burn 1 wei of token with 18 decimals
  • Target token with 6 decimals
  • Conversion: 1 / 10^12 = 0 (integer division)
  • Result: mint back 1 wei of original token

3. Token does not exist in pool

If a token not in MergePool is burned, transaction reverts via tokenExists modifier

Withdraw Operation via Merge Pool

EVM Withdraw Flow

Step 1: User initiates burn

User burns token A, passing payload with burnType = Withdraw, the target token address, and EVM data (recipient, callback).

Step 2: MergePool decodes and converts

MergePool decodes the payload and calls _convertDecimals to normalize the amount between source and target tokens.

Step 3: MergePool calls withdraw on Proxy

When burnType == BurnType.Withdraw and network == Network.EVM:

MergePool calls withdrawTokensToEvmByMergePool on Proxy, passing:

  • token and amount — target token and converted amount
  • recipient — EVM recipient address
  • callback — callback data after withdrawal

The method is protected by the onlyMergePool modifier.

Step 4: Proxy deploys EVM Event

Proxy creates an EVM event with the target token and converted amount. Relay nodes then sign the event and the user calls saveWithdraw*() on EVM (see Limits).

Difference from Regular Withdraw

AspectRegular WithdrawWithdraw via MergePool
Target tokenAlways the same token being burnedCan be any token from the pool
Decimals conversionNot requiredPerformed when necessary
Entry pointDirect burn → eventBurn → MergePool → Proxy → event

Limits

MergePool does not check limits on the TVM side. Withdrawal limits are checked on the EVM side when saveWithdraw*() is called on MultiVault — see Limits.

Data Structures

Token struct

FieldTypeDescription
decimalsuint8Number of decimal places for the token
enabledboolFlag allowing swap/withdraw for the token

Getting decimals:

When adding a token, MergePool requests information via JettonUtils.getInfo. Token responds with takeInfo callback (for EVM alien tokens) or takeInfoAlienTvm (for TVM alien tokens).

BurnType enum

ValueNameDescription
0WithdrawWithdraw to another network
1SwapExchange within TVM

Payload Structures

Base payload for onAcceptTokensBurn:

FieldTypeDescription
nonceuint32Unique operation identifier
burnTypeBurnTypeOperation type (Withdraw or Swap)
targetTokenaddressTarget token from pool
operationPayloadTvmCellAdditional data (empty cell for Swap, or target network data for Withdraw)
remainingGasToaddressAddress for gas return

For Withdraw, operationPayload contains the target network type and network-specific data (withdrawPayload).

MergePool Management

Deployment

Deploy MergePool:

The deployMergePool(uint256 _nonce, address[] _tokens, uint256 _canonId) function creates a new MergePool with the specified token list and canonical token.

Called only by owner or manager.

Deploy MergeRouter:

The deployMergeRouter(address _token) function deploys a MergeRouter for a specific alien token.

One MergeRouter is deployed for each alien token. After deployment, setPool is called on MergeRouter to bind it to MergePool.

Deployment sequence:

  1. Deploy alien tokens (if not already deployed)
  2. Deploy MergeRouter for each token
  3. Deploy MergePool with token list and canon token specification
  4. Bind MergeRouter to MergePool via setPool
  5. Enable all tokens via enableAll

Token Management

FunctionAccessDescription
addToken(address _token)owner/managerAdd token to pool. Token must not exist in pool. Automatically requests decimals.
removeToken(address _token)owner/managerRemove token from pool. Token must exist and not be canon token.
enableToken(address _token)owner/managerEnable token (allow swap/withdraw). Requires decimals > 0.
disableToken(address _token)owner/managerDisable token (prohibit swap/withdraw).
enableAll()owner/managerEnable all pool tokens. All tokens must have decimals > 0.
disableAll()owner/managerDisable all pool tokens.
setCanon(address _token)owner/managerSet canonical token. Token must be enabled.

Access Rights

RoleAddressCapabilities
ownerSet during deployment via PlatformFull access: token management, change manager, upgrade contract
managerSet during deployment, changed via setManagerToken management (add/remove/enable/disable), set canon
proxyStatic variable, set during deploymentCall acceptUpgrade, call mint/withdraw callbacks

Access modifiers:

  • onlyOwner - owner only
  • onlyOwnerOrManager - owner or manager
  • onlyProxy - proxy contract only

MergeRouter rights:

RoleCapabilities
ownersetPool, disablePool, setManager
managersetPool, disablePool

Payload Encoding

Swap Payload

TIP-3:

The encodeMergePoolBurnSwapPayload(address _targetToken) function encodes the Swap payload:

  • burnType = Swap
  • target token
  • empty operationPayload

Jetton:

The encodeMergePoolBurnJettonSwapPayload(address _targetToken, address _remainingGasTo) function encodes the same payload with added nonce and remainingGasTo:

  • nonce (uint32)
  • burnType = Swap
  • target token
  • empty operationPayload
  • remainingGasTo — address for gas return

Withdraw EVM Payload

TIP-3:

The encodeMergePoolBurnWithdrawPayloadEvm(address _targetToken, uint160 _recipient, EvmCallback _callback) function encodes the Withdraw EVM payload:

  • nonce (uint32)
  • burnType = Withdraw
  • target token
  • operationPayload containing Network.EVM, recipient, and callback

Jetton:

The encodeMergePoolBurnJettonWithdrawPayloadEvm(address _targetToken, uint160 _recipient, EvmCallback _callback, address _remainingGasTo) function encodes the same payload with added remainingGasTo.

Error Codes

CodeConstantDescription
2709WRONG_MERGE_POOL_NONCEIncorrect MergePool nonce when calling Proxy methods (sender does not match deriveMergePool)
2901WRONG_PROXYCalling contract is not the proxy contract
2902ONLY_OWNER_OR_MANAGERAction available only to owner or manager
2903TOKEN_NOT_EXISTSToken does not exist in pool
2904TOKEN_IS_CANONAttempt to remove canonical token
2905TOKEN_ALREADY_EXISTSToken already exists in pool
2906MERGE_POOL_IS_ZERO_ADDRESSAttempt to set zero address as merge pool address in MergeRouter
2907TOKEN_NOT_ENABLEDToken is not enabled (disabled)
2908TOKEN_DECIMALS_IS_ZEROToken decimals are 0 (not yet received)
2909WRONG_CANON_IDIncorrect canonical token index during deployment

Risks and Edge Cases

Risk/CaseDescriptionMitigation
Decimals precision lossWhen swapping from token with large decimals (18) to token with small decimals (6), small amounts may round to 0MergePool checks amount == 0 after conversion and returns original tokens
Token disabled mid-swapToken is disabled between burn initiation and MergePool processingMergePool checks enabled during processing and returns tokens when enabled == false
MergeRouter with incorrect poolMergeRouter points to non-existent or incorrect MergePoolValidation during setup: MERGE_POOL_IS_ZERO_ADDRESS . Administrative responsibility.
Upgrade MergePool without state migrationToken data is lost during upgradeacceptUpgrade mechanism preserves state
Race condition in enableAllIf not all decimals are received, enableAll revertsRequirement decimals > 0 for all tokens
Burn non-existent tokenUser burns token not in pooltokenExists modifier reverts transaction
Withdraw call from unauthorized contractAttempt to call withdrawTokensToEvmByMergePool not from MergePoolonlyMergePool modifier checks nonce correspondence

ChainConnect Bridge Documentation