Merge Logic — объединение alien токенов
Что такое Merge
Merge — механизм объединения нескольких alien токенов (представлений одного и того же токена из разных сетей отправления) в единый canon token (каноническое представление) в сети назначения.
Проблема которую решает
Когда один и тот же токен (например, USDT) бриджится из разных сетей в одну TVM сеть, создаются разные alien токены:
- USDT из сети A →
USDT-A(derive token 1) - USDT из сети B →
USDT-B(derive token 2)
Без merge пользователь получает разные токены, которые не взаимозаменяемы. С merge все эти представления объединяются в один USDT-canon, который можно использовать в DeFi приложениях.

Когда нужен Merge
Условия срабатывания при входящем трансфере
| Условие | Описание |
|---|---|
| MergeRouter задеплоен | Для derive-токена существует роутер |
| MergePool указан в Router | pool != address(0) |
| Canon token включён | canonToken.enabled == true |
| Сумма после конвертации > 0 | canon_amount > 0 после decimals |
Когда Merge НЕ происходит
- MergeRouter не задеплоен → пользователь получает derive token
- MergePool не указан в Router (pool == 0) → пользователь получает derive token
- Canon token выключен (
enabled == false) → fallback на derive token - Сумма слишком мала (canon_amount == 0 после decimals) → fallback на derive token
Сценарий 1: Merge при входящем трансфере
Участники
| Участник | Роль |
|---|---|
| IncomingEventContract | Event контракт, обрабатывающий входящий трансфер |
| AlienProxy | Proxy контракт для alien токенов |
| MergeRouter | Связывает derive-токен с MergePool |
| MergePool | Хранит маппинг derive → canon, выполняет конвертацию |
| Token (derive) | Alien токен, созданный для конкретной сети отправления |
| Token (canon) | Каноническое представление токена |
Пошаговое описание
- Получение адреса derive токена — Event контракт запрашивает адрес токена у Proxy
- Проверка деплоя токена — если токен не задеплоен (bounce), деплоится через Proxy
- Запрос MergeRouter — Event контракт запрашивает адрес роутера для данного токена
- Запрос MergePool — Роутер возвращает адрес пула (или 0 если не настроен)
- Запрос canon токена — если пул задан, запрашивается canon токен
- Конвертация decimals и финализация — вычисляется
canon_amountс учетом decimals - Минтинг токенов — Proxy минтит
target_tokenв количествеtarget_amount
Сценарий 2: Swap между токенами в MergePool
Пользователь может обменять один токен из MergePool на другой по курсу 1:1 (с учётом decimals).
Последовательность

Пример payload для swap
Для формирования payload и отправки транзакции необходимы следующие объекты:
| Объект | Описание |
|---|---|
cellEncoder | Контракт-хелпер для кодирования payload в формате TVM Cell. Не хранит состояние, используется только для encode/decode операций |
userTokenWallet | Jetton Wallet пользователя для исходного токена (derive или другой токен в пуле) |
// cellEncoder — инстанс контракта CellEncoder
// Адрес CellEncoder можно получить из конфигурации или задеплоить свой
const cellEncoder = new ever.Contract(CellEncoderAbi, cellEncoderAddress);
// userTokenWallet — Jetton Wallet пользователя для токена, который он хочет обменять
// Адрес получается через Jetton Minter: jettonMinter.getWalletAddress(userAddress)
const userTokenWallet = new ever.Contract(JettonWalletAbi, userJettonWalletAddress);
// Формирование payload для swap
const burnPayload = await cellEncoder.methods
.encodeMergePoolBurnJettonSwapPayload({
_targetToken: targetTokenAddress, // Адрес Jetton Minter целевого токена
_remainingGasTo: userAddress, // Куда вернуть остаток газа
})
.call();
// Сжигание токенов с отправкой в MergePool
await userTokenWallet.methods.burn({
amount: amount,
remainingGasTo: userAddress,
callbackTo: mergePool.address,
payload: burnPayload.value0,
}).send({
from: userAddress,
amount: toNano(2),
});Сценарий 3: Withdraw через MergePool
Пользователь сжигает canon token через MergePool для вывода в другую сеть.
Варианты withdraw
| Сеть назначения | Метод |
|---|---|
| EVM | withdrawTokensToEvmByMergePool |
| SVM (Solana) | withdrawTokensToSvmByMergePool |
| TVM | withdrawTokensToTvmByMergePool |
Последовательность

Пример payload для withdraw в TVM
Для withdraw пользователь сжигает canon token и указывает derive token для определения сети назначения:
| Объект | Описание |
|---|---|
cellEncoder | Контракт-хелпер для кодирования payload в формате TVM Cell |
userCanonWallet | Jetton Wallet пользователя для canon token (каноническое представление токена) |
// cellEncoder — инстанс контракта CellEncoder
const cellEncoder = new ever.Contract(CellEncoderAbi, cellEncoderAddress);
// userCanonWallet — Jetton Wallet пользователя для CANON токена
// Это кошелёк канонического представления, не derive токена
// Адрес: canonJettonMinter.getWalletAddress(userAddress)
const userCanonWallet = new ever.Contract(JettonWalletAbi, userCanonJettonWalletAddress);
// Формирование payload для withdraw в TVM сеть
const burnPayload = await cellEncoder.methods
.encodeMergePoolBurnJettonWithdrawPayloadTvm({
_targetToken: deriveTokenAddress, // Адрес derive Jetton Minter — определяет сеть назначения
_recipient: recipientAddress, // Адрес получателя в сети назначения
_expectedGas: 0, // Ожидаемый газ (0 = дефолт)
_payload: '', // Дополнительный payload (опционально)
_remainingGasTo: senderAddress, // Куда вернуть остаток газа
})
.call();
// Сжигание canon токенов с отправкой в MergePool
await userCanonWallet.methods.burn({
amount: amount,
remainingGasTo: senderAddress,
callbackTo: mergePool.address,
payload: burnPayload.value0,
}).send({
from: senderAddress,
amount: toNano(10), // Больше газа для cross-chain операции
});Технические детали
Структуры данных
IMergePool.Token:
struct Token {
uint8 decimals; // decimals токена
bool enabled; // включён ли токен в пуле
}IMergePool.BurnType:
enum BurnType { Withdraw, Swap }Алгоритм конвертации decimals
При swap или merge между токенами с разным количеством decimals выполняется конвертация суммы:
Три сценария:
| Сценарий | Условие | Формула | Пример |
|---|---|---|---|
| Decimals равны | from_decimals == to_decimals | result = amount | 18 → 18: 1000 → 1000 |
| Уменьшение точности | from_decimals > to_decimals | result = amount / 10^(from - to) | 18 → 6: делим на 10^12 |
| Увеличение точности | from_decimals < to_decimals | result = amount * 10^(to - from) | 6 → 18: умножаем на 10^12 |
Потеря при уменьшении точности
При конвертации из токена с большим decimals в токен с меньшим decimals возможна потеря младших разрядов. Например:
- Исходная сумма:
1_000_000_000_001(18 decimals) - Целевой токен: 6 decimals
- Результат:
1_000_000_000_001 / 10^12 = 1(младшие 12 разрядов потеряны)
Если результат конвертации равен 0, операция выполняет fallback на derive token (при merge) или отклоняется (при swap).
Ключевые функции
| Функция | Назначение |
|---|---|
deployMergePool | Деплой нового MergePool |
deployMergeRouter | Деплой MergeRouter |
onAcceptTokensBurn | Обработка сжигания токенов |
mintTokensByMergePool | Минтинг через MergePool |
receiveMergePoolCanon | Получение canon при входящем |
Ошибки и Edge Cases
Error Codes
| Код | Название | Причина |
|---|---|---|
| 2903 | TOKEN_NOT_EXISTS | Токен не добавлен в MergePool |
| 2904 | TOKEN_IS_CANON | Попытка удалить canon token |
| 2905 | TOKEN_ALREADY_EXISTS | Токен уже есть в пуле |
| 2907 | TOKEN_NOT_ENABLED | Токен отключён |
| 2908 | TOKEN_DECIMALS_IS_ZERO | Попытка включить токен до получения decimals |
| 2709 | WRONG_MERGE_POOL_NONCE | Вызов от неавторизованного пула |
| 2906 | MERGE_POOL_IS_ZERO_ADDRESS | Нулевой адрес пула в Router |
Edge Cases
Case 1: Слишком малая сумма
- Ситуация:
amount = 1,from_decimals = 18,to_decimals = 6 - Результат:
canon_amount = 1 / 10^12 = 0 - Поведение: Fallback на derive token
Case 2: MergeRouter задеплоен, но pool не установлен
- Результат: Пользователь получает derive token
- Поведение: Штатное, не ошибка
Case 3: Canon token отключён во время трансфера
- Результат: Пользователь получает derive token
- Поведение: Fallback, данные не теряются
Case 4: Превышение дневного лимита при withdraw
- Результат: Токены минтятся обратно пользователю
- Поведение: Event
OutgoingLimitReachedэмитится