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.

When Merge is Needed
Conditions for Triggering on Incoming Transfer
| Condition | Description |
|---|---|
| MergeRouter deployed | Router exists for derive token |
| MergePool specified in Router | pool != address(0) |
| Canon token enabled | canonToken.enabled == true |
| Amount after conversion > 0 | canon_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
| Participant | Role |
|---|---|
| IncomingEventContract | Event contract processing incoming transfer |
| AlienProxy | Proxy contract for alien tokens |
| MergeRouter | Links derive token to MergePool |
| MergePool | Stores derive → canon mapping, performs conversion |
| Token (derive) | Alien token created for specific source network |
| Token (canon) | Canonical token representation |
Step-by-step Description
- Getting derive token address — Event contract requests token address from Proxy
- Checking token deployment — if token not deployed (bounce), deployed via Proxy
- Requesting MergeRouter — Event contract requests router address for this token
- Requesting MergePool — Router returns pool address (or 0 if not configured)
- Requesting canon token — if pool is set, canon token is requested
- Decimals conversion and finalization —
canon_amountcalculated with decimals conversion - Token minting — Proxy mints
target_tokenin amounttarget_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

Example Payload for Swap
For building payload and sending transaction, the following objects are needed:
| Object | Description |
|---|---|
cellEncoder | Helper contract for encoding payload in TVM Cell format. Stateless, used only for encode/decode operations |
userTokenWallet | User's Jetton Wallet for source token (derive or other token in pool) |
// 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 Network | Method |
|---|---|
| EVM | withdrawTokensToEvmByMergePool |
| SVM (Solana) | withdrawTokensToSvmByMergePool |
| TVM | withdrawTokensToTvmByMergePool |
Sequence

Example Payload for Withdraw to TVM
For withdraw, user burns canon token and specifies derive token to determine destination network:
| Object | Description |
|---|---|
cellEncoder | Helper contract for encoding payload in TVM Cell format |
userCanonWallet | User's Jetton Wallet for canon token (canonical representation) |
// 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:
struct Token {
uint8 decimals; // token decimals
bool enabled; // is token enabled in pool
}IMergePool.BurnType:
enum BurnType { Withdraw, Swap }Decimals Conversion Algorithm
When swapping or merging between tokens with different decimals, amount conversion is performed:
Three scenarios:
| Scenario | Condition | Formula | Example |
|---|---|---|---|
| Decimals equal | from_decimals == to_decimals | result = amount | 18 → 18: 1000 → 1000 |
| Decreasing precision | from_decimals > to_decimals | result = amount / 10^(from - to) | 18 → 6: divide by 10^12 |
| Increasing precision | from_decimals < to_decimals | result = 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
| Function | Purpose |
|---|---|
deployMergePool | Deploy new MergePool |
deployMergeRouter | Deploy MergeRouter |
onAcceptTokensBurn | Processing token burning |
mintTokensByMergePool | Minting via MergePool |
receiveMergePoolCanon | Receiving canon on incoming |
Errors and Edge Cases
Error Codes
| Code | Name | Cause |
|---|---|---|
| 2903 | TOKEN_NOT_EXISTS | Token not added to MergePool |
| 2904 | TOKEN_IS_CANON | Attempt to remove canon token |
| 2905 | TOKEN_ALREADY_EXISTS | Token already in pool |
| 2907 | TOKEN_NOT_ENABLED | Token disabled |
| 2908 | TOKEN_DECIMALS_IS_ZERO | Attempt to enable token before getting decimals |
| 2709 | WRONG_MERGE_POOL_NONCE | Call from unauthorized pool |
| 2906 | MERGE_POOL_IS_ZERO_ADDRESS | Zero 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:
OutgoingLimitReachedevent emitted