Skip to content

Merge Logic — Alien Token Consolidation

What is Merge

Merge is a mechanism for consolidating multiple alien tokens (representations of the same token from different source networks) into a single canon token (canonical representation) in the destination network.

Problem It Solves

When the same token (e.g., USDT) is bridged from different networks to one TVM network, different alien tokens are created:

  • USDT from network A → USDT-A (derive token 1)
  • USDT from network B → USDT-B (derive token 2)

Without merge, users receive different tokens that are not interchangeable. With merge, all these representations are consolidated into one USDT-canon that can be used in DeFi applications.

Merge Logic

When Merge is Needed

Conditions for Triggering on Incoming Transfer

ConditionDescription
MergeRouter deployedRouter exists for derive token
MergePool specified in Routerpool != address(0)
Canon token enabledcanonToken.enabled == true
Amount after conversion > 0canon_amount > 0 after decimals

When Merge Does NOT Happen

  • MergeRouter not deployed → user receives derive token
  • MergePool not specified in Router (pool == 0) → user receives derive token
  • Canon token disabled (enabled == false) → fallback to derive token
  • Amount too small (canon_amount == 0 after decimals) → fallback to derive token

Scenario 1: Merge on Incoming Transfer

Participants

ParticipantRole
IncomingEventContractEvent contract processing incoming transfer
AlienProxyProxy contract for alien tokens
MergeRouterLinks derive token to MergePool
MergePoolStores derive → canon mapping, performs conversion
Token (derive)Alien token created for specific source network
Token (canon)Canonical token representation

Step-by-step Description

  1. Getting derive token address — Event contract requests token address from Proxy
  2. Checking token deployment — if token not deployed (bounce), deployed via Proxy
  3. Requesting MergeRouter — Event contract requests router address for this token
  4. Requesting MergePool — Router returns pool address (or 0 if not configured)
  5. Requesting canon token — if pool is set, canon token is requested
  6. Decimals conversion and finalizationcanon_amount calculated with decimals conversion
  7. Token minting — Proxy mints target_token in amount target_amount

Scenario 2: Swap Between Tokens in MergePool

User can exchange one token from MergePool to another at 1:1 rate (with decimals conversion).

Sequence

Swap Between Tokens in MergePool

Example Payload for Swap

For building payload and sending transaction, the following objects are needed:

ObjectDescription
cellEncoderHelper contract for encoding payload in TVM Cell format. Stateless, used only for encode/decode operations
userTokenWalletUser's Jetton Wallet for source token (derive or other token in pool)
typescript
// cellEncoder — instance of CellEncoder contract
// CellEncoder address can be obtained from configuration or deploy your own
const cellEncoder = new ever.Contract(CellEncoderAbi, cellEncoderAddress);

// userTokenWallet — User's Jetton Wallet for token they want to exchange
// Address obtained via Jetton Minter: jettonMinter.getWalletAddress(userAddress)
const userTokenWallet = new ever.Contract(JettonWalletAbi, userJettonWalletAddress);

// Building payload for swap
const burnPayload = await cellEncoder.methods
  .encodeMergePoolBurnJettonSwapPayload({
    _targetToken: targetTokenAddress,  // Target token Jetton Minter address
    _remainingGasTo: userAddress,      // Where to return remaining gas
  })
  .call();

// Burning tokens with sending to MergePool
await userTokenWallet.methods.burn({
  amount: amount,
  remainingGasTo: userAddress,
  callbackTo: mergePool.address,
  payload: burnPayload.value0,
}).send({
  from: userAddress,
  amount: toNano(2),
});

Scenario 3: Withdraw via MergePool

User burns canon token via MergePool for withdrawal to another network.

Withdraw Variants

Destination NetworkMethod
EVMwithdrawTokensToEvmByMergePool
SVM (Solana)withdrawTokensToSvmByMergePool
TVMwithdrawTokensToTvmByMergePool

Sequence

Withdraw via MergePool

Example Payload for Withdraw to TVM

For withdraw, user burns canon token and specifies derive token to determine destination network:

ObjectDescription
cellEncoderHelper contract for encoding payload in TVM Cell format
userCanonWalletUser's Jetton Wallet for canon token (canonical representation)
typescript
// cellEncoder — instance of CellEncoder contract
const cellEncoder = new ever.Contract(CellEncoderAbi, cellEncoderAddress);

// userCanonWallet — User's Jetton Wallet for CANON token
// This is canonical representation wallet, not derive token
// Address: canonJettonMinter.getWalletAddress(userAddress)
const userCanonWallet = new ever.Contract(JettonWalletAbi, userCanonJettonWalletAddress);

// Building payload for withdraw to TVM network
const burnPayload = await cellEncoder.methods
  .encodeMergePoolBurnJettonWithdrawPayloadTvm({
    _targetToken: deriveTokenAddress,  // Derive Jetton Minter address — determines destination network
    _recipient: recipientAddress,       // Recipient address in destination network
    _expectedGas: 0,                    // Expected gas (0 = default)
    _payload: '',                       // Additional payload (optional)
    _remainingGasTo: senderAddress,     // Where to return remaining gas
  })
  .call();

// Burning canon tokens with sending to MergePool
await userCanonWallet.methods.burn({
  amount: amount,
  remainingGasTo: senderAddress,
  callbackTo: mergePool.address,
  payload: burnPayload.value0,
}).send({
  from: senderAddress,
  amount: toNano(10),  // More gas for cross-chain operation
});

Technical Details

Data Structures

IMergePool.Token:

solidity
struct Token {
    uint8 decimals;  // token decimals
    bool enabled;    // is token enabled in pool
}

IMergePool.BurnType:

solidity
enum BurnType { Withdraw, Swap }

Decimals Conversion Algorithm

When swapping or merging between tokens with different decimals, amount conversion is performed:

Three scenarios:

ScenarioConditionFormulaExample
Decimals equalfrom_decimals == to_decimalsresult = amount18 → 18: 1000 → 1000
Decreasing precisionfrom_decimals > to_decimalsresult = amount / 10^(from - to)18 → 6: divide by 10^12
Increasing precisionfrom_decimals < to_decimalsresult = amount * 10^(to - from)6 → 18: multiply by 10^12

Loss on Precision Decrease

When converting from token with higher decimals to token with lower decimals, lower digits may be lost. For example:

  • Source amount: 1_000_000_000_001 (18 decimals)
  • Target token: 6 decimals
  • Result: 1_000_000_000_001 / 10^12 = 1 (lower 12 digits lost)

If conversion result equals 0, operation falls back to derive token (on merge) or is rejected (on swap).

Key Functions

FunctionPurpose
deployMergePoolDeploy new MergePool
deployMergeRouterDeploy MergeRouter
onAcceptTokensBurnProcessing token burning
mintTokensByMergePoolMinting via MergePool
receiveMergePoolCanonReceiving canon on incoming

Errors and Edge Cases

Error Codes

CodeNameCause
2903TOKEN_NOT_EXISTSToken not added to MergePool
2904TOKEN_IS_CANONAttempt to remove canon token
2905TOKEN_ALREADY_EXISTSToken already in pool
2907TOKEN_NOT_ENABLEDToken disabled
2908TOKEN_DECIMALS_IS_ZEROAttempt to enable token before getting decimals
2709WRONG_MERGE_POOL_NONCECall from unauthorized pool
2906MERGE_POOL_IS_ZERO_ADDRESSZero pool address in Router

Edge Cases

Case 1: Amount too small

  • Situation: amount = 1, from_decimals = 18, to_decimals = 6
  • Result: canon_amount = 1 / 10^12 = 0
  • Behavior: Fallback to derive token

Case 2: MergeRouter deployed but pool not set

  • Result: User receives derive token
  • Behavior: Normal, not an error

Case 3: Canon token disabled during transfer

  • Result: User receives derive token
  • Behavior: Fallback, no data loss

Case 4: Daily limit exceeded on withdraw

  • Result: Tokens minted back to user
  • Behavior: OutgoingLimitReached event emitted

ChainConnect Bridge Documentation