Skip to content

Merge Logic (Объединение Alien-представлений)

Что такое Merge

Проблема фрагментации

Когда одинаковый токен (например, USDT) существует на разных EVM сетях и бриджится в TVM сеть, для каждого EVM-варианта создается отдельное alien-представление в TVM:

  • USDT из сети Ethereum → alien-токен A в TVM
  • USDT из сети BSC → alien-токен B в TVM
  • USDT из сети Avalanche → alien-токен C в TVM

Это создает проблему фрагментации ликвидности: пользователь не может напрямую использовать токен A вместо токена B, хотя по сути это одинаковый актив.

Решение через Merge Pool

Merge Pool позволяет объединить эти alien-представления одного актива в единый пул, предоставляя:

  1. Swap между alien-представлениями: обмен токена A на токен B с коэффициентом 1:1 (с учетом нормализации decimals)
  2. Withdraw с конвертацией: вывод любого токена из пула в EVM с конвертацией decimals

Архитектура

Компоненты

КомпонентОписание
MergePoolОсновной контракт пула. Хранит список токенов с их decimals и статусом enabled. Обрабатывает burn токенов и выполняет swap или withdraw.
MergeRouterКонтракт-роутер, связанный с конкретным токеном. Хранит адрес MergePool, к которому относится токен. Деплоится для каждого alien-токена отдельно.
MergePoolPlatformPlatform-контракт для деплоя MergePool через TVM state init механизм. Принимает код MergePool и инициализирует его с заданными параметрами.
ProxyMultiVaultAlienProxy контракт, который деплоит MergePool и MergeRouter, управляет версиями, обрабатывает mint/withdraw запросы от MergePool.

Схема взаимодействия



Описание потока:

  1. Пользователь сжигает (burn) токен A, передавая payload с типом операции (Swap/Withdraw) и целевым токеном
  2. MergePool получает callback onAcceptTokensBurn, декодирует payload, конвертирует decimals
  3. В зависимости от типа операции:
    • Swap: MergePool вызывает mintTokensByMergePool на Proxy для минта целевого токена
    • Withdraw: MergePool вызывает withdrawTokensToEvmByMergePool на Proxy для создания withdraw event
  4. Proxy выполняет соответствующее действие

Канонический токен (Canon Token)

Определение

Канонический токен (canon token) — это выделенный токен в MergePool, который служит референсной точкой пула. Обычно выбирается токен из основной/наиболее ликвидной сети (например, USDT из Ethereum для пула USDT).

Назначение

Canon token используется как идентификатор пула и точка отсчёта. Нельзя удалить canon token из пула — это защищает целостность конфигурации.

Управление Canon Token

Canon token устанавливается при деплое MergePool через параметр _canonId — индекс в массиве токенов.

Canon token можно изменить через метод setCanon(address _token). Метод доступен для owner или manager. Проверяет, что токен существует в пуле и включен (enabled).

Операция Swap

Поток операции Swap

Шаг 1: Пользователь инициирует burn

Пользователь сжигает токен A, передавая payload с burnType = Swap и адресом целевого токена.

Шаг 2: MergePool декодирует и конвертирует

MergePool вызывает _convertDecimals для нормализации суммы между исходным и целевым токенами.

Шаг 3: Проверки и mint

MergePool проверяет условия (см. Проверки и ограничения) и при успехе вызывает _mintTokens(targetToken, amount, walletOwner, remainingGasTo, operationPayload) на Proxy.

Нормализация decimals

Функция _convertDecimals нормализует сумму при обмене токенов с разными decimals:

  • Если decimals совпадают — сумма не меняется
  • Если у исходного токена больше decimals — сумма делится на 10^(разница)
  • Если у исходного токена меньше decimals — сумма умножается на 10^(разница)

Примеры:

  • 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 (без изменений)

Проверки и ограничения Swap

1. Токен не включен (disabled)

Если целевой или исходный токен отключен, swap не выполняется, вместо этого возвращаются сожженные токены

2. Сумма после конвертации = 0

Если после _convertDecimals получается 0, swap не выполняется, возвращаются исходные токены

Это может произойти при обмене очень малых сумм из токена с большим decimals в токен с малым decimals.

Пример edge case:

  • Burn 1 wei токена с 18 decimals
  • Target токен с 6 decimals
  • Конвертация: 1 / 10^12 = 0 (integer division)
  • Результат: mint обратно 1 wei исходного токена

3. Токен не существует в пуле

Если сжигается токен, не входящий в MergePool, транзакция ревертится через модификатор tokenExists

Операция Withdraw через Merge Pool

Поток EVM Withdraw

Шаг 1: Пользователь инициирует burn

Пользователь сжигает токен A, передавая payload с burnType = Withdraw, адресом целевого токена и данными для EVM (recipient, callback).

Шаг 2: MergePool декодирует и конвертирует

MergePool декодирует payload и вызывает _convertDecimals для нормализации суммы между исходным и целевым токенами.

Шаг 3: MergePool вызывает withdraw на Proxy

При burnType == BurnType.Withdraw и network == Network.EVM:

MergePool вызывает withdrawTokensToEvmByMergePool на Proxy, передавая:

  • token и amount — целевой токен и сконвертированная сумма
  • recipient — адрес получателя в EVM
  • callback — данные для callback после вывода

Метод защищён модификатором onlyMergePool.

Шаг 4: Proxy деплоит EVM Event

Proxy создаёт EVM event с целевым токеном и сконвертированной суммой. Далее relay-ноды подписывают событие и пользователь вызывает saveWithdraw*() в EVM (см. Лимиты).

Различие с обычным withdraw

АспектОбычный WithdrawWithdraw через MergePool
Целевой токенВсегда тот же токен, который сжигаетсяМожет быть любой токен из пула
Decimals конвертацияНе требуетсяВыполняется при необходимости
Точка входаПрямой burn → eventBurn → MergePool → Proxy → event

Лимиты

MergePool не проверяет лимиты на стороне TVM. Лимиты на вывод проверяются на стороне EVM при вызове saveWithdraw*() в MultiVault — см. Лимиты.

Структуры данных

Token struct

ПолеТипОписание
decimalsuint8Количество десятичных знаков токена
enabledboolФлаг, разрешён ли swap/withdraw для токена

Получение decimals:

При добавлении токена MergePool запрашивает информацию через JettonUtils.getInfo. Токен отвечает callback'ом takeInfo (для EVM alien токенов) или takeInfoAlienTvm (для TVM alien токенов).

BurnType enum

ЗначениеНазваниеОписание
0WithdrawВывод на другую сеть
1SwapОбмен внутри TVM

Payload структуры

Базовый payload для onAcceptTokensBurn:

ПолеТипОписание
nonceuint32Уникальный идентификатор операции
burnTypeBurnTypeТип операции (Withdraw или Swap)
targetTokenaddressЦелевой токен из пула
operationPayloadTvmCellДополнительные данные (пустая ячейка для Swap, или данные целевой сети для Withdraw)
remainingGasToaddressАдрес для возврата газа

Для Withdraw operationPayload содержит тип целевой сети и данные для неё (withdrawPayload).

Управление MergePool

Деплой

Деплой MergePool:

Функция deployMergePool(uint256 _nonce, address[] _tokens, uint256 _canonId) создаёт новый MergePool с указанным списком токенов и каноническим токеном.

Вызывается только owner или manager.

Деплой MergeRouter:

Функция deployMergeRouter(address _token) деплоит MergeRouter для конкретного alien токена.

Деплоится один MergeRouter для каждого alien токена. После деплоя на MergeRouter вызывается setPool для привязки к MergePool.

Последовательность деплоя:

  1. Деплой alien токенов (если не задеплоены)
  2. Деплой MergeRouter для каждого токена
  3. Деплой MergePool с списком токенов и указанием canon токена
  4. Привязка MergeRouter к MergePool через setPool
  5. Включение всех токенов через enableAll

Управление токенами

ФункцияДоступОписание
addToken(address _token)owner/managerДобавить токен в пул. Токен не должен существовать в пуле. Автоматически запрашивает decimals.
removeToken(address _token)owner/managerУдалить токен из пула. Токен должен существовать и не быть canon токеном.
enableToken(address _token)owner/managerВключить токен (разрешить swap/withdraw). Требует наличия decimals > 0.
disableToken(address _token)owner/managerОтключить токен (запретить swap/withdraw).
enableAll()owner/managerВключить все токены пула. Все токены должны иметь decimals > 0.
disableAll()owner/managerОтключить все токены пула.
setCanon(address _token)owner/managerУстановить канонический токен. Токен должен быть включен.

Права доступа

РольАдресВозможности
ownerУстанавливается при деплое через PlatformПолный доступ: управление токенами, смена manager, upgrade контракта
managerУстанавливается при деплое, меняется через setManagerУправление токенами (add/remove/enable/disable), установка canon
proxyStatic переменная, задается при деплоеВызов acceptUpgrade, вызов callback'ов mint/withdraw

Модификаторы доступа:

  • onlyOwner - только owner
  • onlyOwnerOrManager - owner или manager
  • onlyProxy - только proxy контракт

MergeRouter права:

РольВозможности
ownersetPool, disablePool, setManager
managersetPool, disablePool

Кодирование payload

Swap payload

TIP-3:

Функция encodeMergePoolBurnSwapPayload(address _targetToken) кодирует payload для Swap:

  • burnType = Swap
  • целевой токен
  • пустой operationPayload

Jetton:

Функция encodeMergePoolBurnJettonSwapPayload(address _targetToken, address _remainingGasTo) кодирует тот же payload с добавлением nonce и remainingGasTo:

  • nonce
  • burnType = Swap
  • целевой токен
  • пустой operationPayload
  • remainingGasTo — адрес для возврата газа

Withdraw EVM payload

TIP-3:

Функция encodeMergePoolBurnWithdrawPayloadEvm(address _targetToken, uint160 _recipient, EvmCallback _callback) кодирует payload для Withdraw в EVM:

  • nonce
  • burnType = Withdraw
  • целевой токен
  • operationPayload с Network.EVM, адресом получателя и callback

Jetton:

Функция encodeMergePoolBurnJettonWithdrawPayloadEvm(address _targetToken, uint160 _recipient, EvmCallback _callback, address _remainingGasTo) кодирует тот же payload с добавлением remainingGasTo.

Коды ошибок

КодКонстантаОписание
2709WRONG_MERGE_POOL_NONCEНеверный nonce MergePool при вызове методов Proxy (sender не соответствует deriveMergePool)
2901WRONG_PROXYВызывающий контракт не является proxy контрактом
2902ONLY_OWNER_OR_MANAGERДействие доступно только owner или manager
2903TOKEN_NOT_EXISTSТокен не существует в пуле
2904TOKEN_IS_CANONПопытка удалить канонический токен
2905TOKEN_ALREADY_EXISTSТокен уже существует в пуле
2906MERGE_POOL_IS_ZERO_ADDRESSПопытка установить zero address как адрес merge pool в MergeRouter
2907TOKEN_NOT_ENABLEDТокен не включен (disabled)
2908TOKEN_DECIMALS_IS_ZERODecimals токена равны 0 (еще не получены)
2909WRONG_CANON_IDНеверный индекс канонического токена при деплое

Риски и Edge Cases

Риск/CaseОписаниеМитигация
Decimals потеря точностиПри swap из токена с большим decimals (18) в токен с малым decimals (6) малые суммы могут округлиться до 0MergePool проверяет amount == 0 после конвертации и возвращает исходные токены
Token disabled mid-swapТокен отключается между инициацией burn и обработкой в MergePoolMergePool проверяет enabled при обработке и возвращает токены при enabled == false
MergeRouter с неправильным poolMergeRouter указывает на несуществующий или неправильный MergePoolПроверка при настройке: MERGE_POOL_IS_ZERO_ADDRESS . Административная ответственность.
Upgrade MergePool без миграции состоянияПри upgrade теряются данные токеновМеханизм acceptUpgrade сохраняет состояние
Race condition при enableAllЕсли не все decimals получены, enableAll ревертитсяТребование decimals > 0 для всех токенов
Burn несуществующего токенаПользователь сжигает токен, не входящий в poolМодификатор tokenExists ревертирует транзакцию
Вызов withdraw от неавторизованного контрактаПопытка вызвать withdrawTokensToEvmByMergePool не от MergePoolМодификатор onlyMergePool проверяет соответствие nonce

ChainConnect Bridge Documentation