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 через canonical токен: вывод любого токена из пула с использованием канонического токена для проверки daily limits

Архитектура

Компоненты

КомпонентОписание
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 выполняет соответствующее действие

Версии MergePool

ВерсияОтличия от предыдущей
V1Базовая версия. Поддержка EVM и SVM withdraw.
V2Добавлена поддержка TVM-TVM переводов (метод withdrawTokensToTvmByMergePool, интерфейс ITakeInfoAlienTvm).
V3Передача _canonToken и _canonAmount в методы withdraw для корректного учета daily limits. Параметр _sender вместо _walletOwner в withdraw методах.

V3 изменения (относительно V2):

В V3 при вызове withdrawTokensToEvmByMergePool передаются:

  • _canonToken - адрес канонического токена (из msg.sender - сожженный токен)
  • _canonAmount - оригинальная сумма до конвертации
  • _token - адрес целевого токена
  • _amount - сконвертированная сумма

Это позволяет Proxy использовать канонический токен для проверки daily limits вместо целевого токена.

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

Определение

Канонический токен (canon token) - это выделенный токен в MergePool, который используется как референсный для операций withdraw.

Назначение

Canon token выполняет следующие роли:

  1. Расчет daily limits: при withdraw через MergePool daily limits проверяются именно для канонического токена, а не для целевого токена вывода. Это позволяет унифицировать лимиты для всех токенов пула.

  2. Референсная точка: canon token обычно выбирается как токен из основной/наиболее ликвидной сети (например, USDT из Ethereum для пула USDT).

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

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

Canon token можно изменить через метод setCanon():

solidity
function setCanon(address _token)
    external
    override
    onlyOwnerOrManager
    tokenExists(_token)
    cashBack
{
    require(tokens[_token].enabled, ErrorCodes.TOKEN_NOT_ENABLED);
    canon = _token;
}

Ограничения:

  • Canon token должен существовать в пуле
  • Canon token должен быть включен (enabled)
  • Изменить canon может только owner или manager

Нельзя удалить текущий canon token из пула

Операция Swap

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

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

Пользователь сжигает токен A, передавая payload:

typescript
payload = abi.encode(
    nonce: uint32,
    burnType: BurnType.Swap,  // = 1
    targetToken: address,      // адрес токена B
    operationPayload: TvmCell, // пустая ячейка для swap
    remainingGasTo: address
)

Шаг 2: MergePool получает callback

MergePool декодирует payload в onAcceptTokensBurn

Шаг 3: Конвертация decimals

MergePool вызывает _convertDecimals для нормализации суммы:

solidity
uint128 amount = _convertDecimals(_amount, msg.sender, targetToken);

Шаг 4: Проверки

MergePool проверяет условия:

  1. Целевой токен включен (enabled)
  2. Исходный токен включен
  3. Сконвертированная сумма > 0

Шаг 5: Mint целевого токена

Если проверки пройдены и burnType == BurnType.Swap:

solidity
_mintTokens(targetToken, amount, _walletOwner, remainingGasTo, operationPayload);

MergePool вызывает на Proxy

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

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

solidity
function _convertDecimals(
    uint128 _amount,
    address _from,
    address _to
) internal view returns (uint128) {
    uint8 from_decimals = tokens[_from].decimals;
    uint8 to_decimals = tokens[_to].decimals;
    
    uint128 base = 10;
    
    if (from_decimals == to_decimals) {
        return _amount;
    } else if (from_decimals > to_decimals) {
        return _amount / (base ** uint128(from_decimals - to_decimals));
    } else {
        return _amount * (base ** uint128(to_decimals - from_decimals));
    }
}

Примеры:

  • 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:

typescript
payload = abi.encode(
    nonce: uint32,
    burnType: BurnType.Withdraw,  // = 0
    targetToken: address,          // адрес целевого alien токена
    operationPayload: TvmCell,     // = abi.encode(Network.EVM, withdrawPayload)
    remainingGasTo: address
)

где withdrawPayload = abi.encode(recipient: uint160, callback: EvmCallback)

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

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

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

solidity
IProxyMultiVaultAlienJetton_V3(proxy)
    .withdrawTokensToEvmByMergePool{
        bounce: false,
        value: 0,
        flag: MsgFlag.ALL_NOT_RESERVED
    }(
        _randomNonce,      // merge pool nonce
        nonce,             // operation nonce
        msg.sender,        // canon token (сожженный токен)
        _amount,           // canon amount (исходная сумма)
        targetToken,       // token (целевой токен)
        amount,            // amount (сконвертированная сумма)
        recipient,         // EVM recipient
        remainingGasTo,
        _walletOwner,      // sender
        callback
    );

Шаг 4: Proxy проверяет daily limits

Proxy получает метод withdrawTokensToEvmByMergePool с модификатором onlyMergePool

Ключевой момент: daily limits проверяются для _canonToken и _canonAmount, а не для _token и _amount:

solidity
(
    bool isLimitReached,
    optional(DailyLimits) newLimits
) = _isOutgoingDailyLimitReached(_canonToken, _canonAmount);

Шаг 5: Деплой EVM Event

Если лимиты не достигнуты, Proxy деплоит EVM event с целевым токеном:

solidity
_deployEvmEvent(
    _nonce,
    _token,        // целевой токен (не канонический!)
    _amount,       // сконвертированная сумма
    _recipient,
    _remainingGasTo,
    _sender,
    _callback
);

Если лимит достигнут:

Proxy минтит обратно канонический токен отправителю

Поток SVM Withdraw

Аналогичен EVM Withdraw, но использует метод withdrawTokensToSvmByMergePool

Payload для SVM:

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:69-93
bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:95-121

Поток TVM Withdraw

Для TVM-TVM переводов используется метод withdrawTokensToTvmByMergePool:

bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:468-485
bridge-ton-contracts/contracts/bridge/proxy/multivault/alien-ton/V3/ProxyMultiVaultAlienJetton_V3_MergePool.tsol:175-217

Payload для TVM:

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:123-141

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

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

Взаимодействие с daily limits

Ключевая идея: Daily limits проверяются для канонического токена независимо от того, какой токен выводится.

Сценарий:

  1. MergePool содержит: USDT-Ethereum (canon, 6 decimals), USDT-BSC (18 decimals), USDT-Avalanche (6 decimals)
  2. Daily outgoing limit для USDT-Ethereum: 100,000 токенов
  3. Пользователь A выводит 50,000 USDT-BSC → EVM
    • Canon amount: 50,000 * 10^6 (в decimals канонического токена)
    • Daily volume увеличивается на 50,000 USDT-Ethereum
  4. Пользователь B выводит 40,000 USDT-Avalanche → EVM
    • Canon amount: 40,000 * 10^6
    • Daily volume увеличивается на 40,000 USDT-Ethereum
  5. Пользователь C пытается вывести 20,000 USDT-Ethereum → EVM
    • Лимит достигнут (50,000 + 40,000 + 20,000 > 100,000)
    • Транзакция отклоняется, токены возвращаются

TODO: требует уточнения - как именно вычисляется canonAmount при конвертации decimals? Используется ли _amount или _canonAmount для расчета?

bridge-ton-contracts/contracts/bridge/proxy/multivault/alien-ton/V3/ProxyMultiVaultAlienJetton_V3_MergePool.tsol:91-94

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

Token struct

Структура, описывающая токен в MergePool:

solidity
struct Token {
    uint8 decimals;
    bool enabled;
}

bridge-ton-contracts/contracts/bridge/interfaces/alien-token-merge/IMergePool.tsol:4-7

Поля:

  • decimals - количество десятичных знаков токена (получается через callback takeInfo или takeInfoAlienTvm)
  • enabled - флаг, разрешен ли swap/withdraw для токена

bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:45

Получение decimals:

При добавлении токена MergePool запрашивает информацию через JettonUtils.getInfo:

bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:146-151
bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:489-498

Токен отвечает callback'ом takeInfo (для EVM/SVM alien токенов) или takeInfoAlienTvm (для TVM alien токенов):

bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:155-187

BurnType enum

Перечисление типов операций при burn токена:

solidity
enum BurnType { 
    Withdraw,  // = 0
    Swap       // = 1
}

bridge-ton-contracts/contracts/bridge/interfaces/alien-token-merge/IMergePool.tsol:9

Значения:

  • Withdraw (0) - вывод токенов на другую сеть через bridge event
  • Swap (1) - обмен на другой токен из пула внутри TVM

Payload структуры

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

solidity
payload = abi.encode(
    nonce: uint32,              // уникальный nonce операции
    burnType: BurnType,         // тип операции (0 = Withdraw, 1 = Swap)
    targetToken: address,       // целевой токен из пула
    operationPayload: TvmCell,  // дополнительные данные в зависимости от операции
    remainingGasTo: address     // адрес для возврата оставшегося газа
)

bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:357-369

operationPayload для Swap:

Пустая ячейка (empty cell):

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:9
bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:22

operationPayload для Withdraw:

solidity
operationPayload = abi.encode(
    network: Network,        // тип целевой сети (EVM/SVM/TVM)
    withdrawPayload: TvmCell // данные для конкретной сети
)

bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:409

Управление MergePool

Деплой

Деплой MergePool:

solidity
function deployMergePool(
    uint256 _nonce,      // уникальный nonce для деплоя
    address[] _tokens,   // список токенов пула
    uint256 _canonId     // индекс канонического токена в _tokens
) external override reserveAtLeastTargetBalance

bridge-ton-contracts/contracts/bridge/proxy/multivault/alien-ton/V3/ProxyMultiVaultAlienJetton_V3_MergePool.tsol:30-52

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

Деплой MergeRouter:

solidity
function deployMergeRouter(address _token) external override reserveAtLeastTargetBalance

bridge-ton-contracts/contracts/bridge/proxy/multivault/alien-ton/V3/ProxyMultiVaultAlienJetton_V3_MergeRouter.tsol:19-28

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

bridge-ton-contracts/scripts/bootstrap/5-setup-merge-routers.ts:333-360

Последовательность деплоя (из скрипта):

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

bridge-ton-contracts/scripts/bootstrap/5-setup-merge-routers.ts:86-391

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

ФункцияДоступОписание
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Установить канонический токен. Токен должен быть включен.

bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:198-304

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

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

bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:41-65

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

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

bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:189-196 (setManager - только owner)
bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:219-233 (addToken - owner/manager)

MergeRouter права:

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

bridge-ton-contracts/contracts/bridge/alien-token-merge/MergeRouter.tsol:18-50

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

Все функции кодирования находятся в MergePoolCellEncoder.tsol:

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:7

Swap payload

Для старых jetton (без remainingGasTo):

solidity
function encodeMergePoolBurnSwapPayload(address _targetToken) public pure returns (TvmCell)

Кодирует:

uint8(1)          // burnType = Swap
_targetToken      // целевой токен
empty             // пустой operationPayload

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:8-16

Для jetton с remainingGasTo:

solidity
function encodeMergePoolBurnJettonSwapPayload(
    address _targetToken,
    address _remainingGasTo
) public pure returns (TvmCell)

Кодирует:

uint32(0)         // nonce
uint8(1)          // burnType = Swap
_targetToken      // целевой токен
empty             // пустой operationPayload
_remainingGasTo   // адрес для возврата газа

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:18-31

Withdraw EVM payload

Без remainingGasTo:

solidity
function encodeMergePoolBurnWithdrawPayloadEvm(
    address _targetToken,
    uint160 _recipient,
    EvmCallback _callback
) public pure functionID(0x570f6016) returns (TvmCell)

Кодирует:

uint32(0)                                      // nonce
uint8(0)                                       // burnType = Withdraw
_targetToken                                   // целевой токен
abi.encode(Network.EVM, abi.encode(_recipient, _callback))  // operationPayload

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:33-48

С remainingGasTo:

solidity
function encodeMergePoolBurnJettonWithdrawPayloadEvm(
    address _targetToken,
    uint160 _recipient,
    EvmCallback _callback,
    address _remainingGasTo
) public pure functionID(0x07bc42ad) returns (TvmCell)

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:50-67

Withdraw SVM payload

Без remainingGasTo:

solidity
function encodeMergePoolBurnWithdrawPayloadSvm(
    address _targetToken,
    uint256 _recipient,
    ITvmSvmEvent.TvmSvmExecuteAccount[] _executeAccounts,
    bool _executePayloadNeeded,
    ITvmSvmEvent.TvmSvmExecuteAccount[] _executePayloadAccounts,
    bytes _payload
) public pure functionID(0x79e64187) returns (TvmCell)

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:69-93

С remainingGasTo:

solidity
function encodeMergePoolBurnJettonWithdrawPayloadSvm(
    address _targetToken,
    uint256 _recipient,
    ITvmSvmEvent.TvmSvmExecuteAccount[] _executeAccounts,
    bool _executePayloadNeeded,
    ITvmSvmEvent.TvmSvmExecuteAccount[] _executePayloadAccounts,
    bytes _payload,
    address _remainingGasTo
) public pure functionID(0x675e16bc) returns (TvmCell)

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:95-121

Withdraw TVM payload

solidity
function encodeMergePoolBurnJettonWithdrawPayloadTvm(
    address _targetToken,
    address _recipient,
    uint128 _expectedGas,
    optional(TvmCell) _payload,
    address _remainingGasTo
) public pure functionID(0x0f66760c) returns (TvmCell)

Кодирует:

uint32(0)                                           // nonce
uint8(0)                                            // burnType = Withdraw
_targetToken                                        // целевой токен
abi.encode(Network.TVM, abi.encode(_recipient, _expectedGas, _payload))  // operationPayload
_remainingGasTo                                     // адрес для возврата газа

bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:123-141

Интеграция с фронтендом

TransitOperation

Enum определяющий тип транзитной операции:

typescript
export enum TransitOperation {
    BurnToAlienProxy = '0',
    BurnToMergePool = '1',
    TransferToNativeProxy = '2',
}

frontend/src/modules/Transfers/types.ts:49-53

Значение BurnToMergePool = '1' соответствует операции через MergePool.

getEvmTokenMergeDetails

Утилита из библиотеки @broxus/js-bridge-essentials для получения информации о merge pool токена.

Используется в transfer stores:

frontend/src/modules/Transfers/stores/TvmEvmTransferStore.ts:322 (AlienProxyV8.Utils.getEvmTokenMergeDetails)
frontend/src/modules/Transfers/stores/TvmEvmTvmTransferStore.ts:353 (AlienProxyV8.Utils.getEvmTokenMergeDetails)
frontend/src/modules/Transfers/stores/TvmEvmTonTransferStore.ts:362 (AlienProxyV8.Utils.getEvmTokenMergeDetails)
frontend/src/modules/Transfers/stores/TonEvmTransferStore.ts:340 (TonAlienProxyV1.Utils.getEvmTokenMergeDetails)
frontend/src/modules/Transfers/stores/TonEvmTvmTransferStore.ts:361 (TonAlienProxyV1.Utils.getEvmTokenMergeDetails)

TODO: требует уточнения - какие именно данные возвращает getEvmTokenMergeDetails, как они используются?

Декодирование payload

При декодировании burn payload на фронтенде проверяется тип операции:

typescript
if (operationPayload.data.operation === TransitOperation.BurnToMergePool) {
    const burnPayload = await connection.unpackFromCell({
        allowPartial: true,
        boc: operationPayload.data.payload,
        structure: [
            { name: 'nonce', type: 'uint32' },
            { name: 'burnType', type: 'uint8' },
            { name: 'targetToken', type: 'address' },
            { name: 'payload', type: 'cell' },
        ] as const,
    })

    const callback = await connection.unpackFromCell({
        allowPartial: true,
        boc: burnPayload.data.payload,
        structure: [
            { name: 'network', type: 'uint8' },
            { name: 'payload', type: 'cell' },
        ] as const,
    })

    const payload = await connection.unpackFromCell({
        allowPartial: true,
        boc: callback.data.payload,
        structure: [
            { name: 'targetAddress', type: 'uint160' },
            {
                components: [
                    { name: 'recipient', type: 'uint160' },
                    { name: 'payload', type: 'cell' },
                    { name: 'strict', type: 'bool' },
                ],
                name: 'callback',
                type: 'tuple',
            },
        ] as const,
    })

    return `0x${BigNumber(payload.data.targetAddress).toString(16).padStart(40, '0')}`
}

Коды ошибок

КодКонстантаОписание
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
Неправильный canon при лимитахЕсли canon токен имеет малый decimals, а выводится токен с большим decimals, может быть bypass лимитовTODO: требует уточнения механизма расчета canonAmount
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