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-представления одного актива в единый пул, предоставляя:
- Swap между alien-представлениями: обмен токена A на токен B с коэффициентом 1:1 (с учетом нормализации decimals)
- Withdraw через canonical токен: вывод любого токена из пула с использованием канонического токена для проверки daily limits
Архитектура
Компоненты
| Компонент | Описание |
|---|---|
| MergePool | Основной контракт пула. Хранит список токенов с их decimals и статусом enabled. Обрабатывает burn токенов и выполняет swap или withdraw. |
| MergeRouter | Контракт-роутер, связанный с конкретным токеном. Хранит адрес MergePool, к которому относится токен. Деплоится для каждого alien-токена отдельно. |
| MergePoolPlatform | Platform-контракт для деплоя MergePool через TVM state init механизм. Принимает код MergePool и инициализирует его с заданными параметрами. |
| ProxyMultiVaultAlien | Proxy контракт, который деплоит MergePool и MergeRouter, управляет версиями, обрабатывает mint/withdraw запросы от MergePool. |
Схема взаимодействия
Описание потока:
- Пользователь сжигает (burn) токен A, передавая payload с типом операции (Swap/Withdraw) и целевым токеном
- MergePool получает callback
onAcceptTokensBurn, декодирует payload, конвертирует decimals - В зависимости от типа операции:
- Swap: MergePool вызывает
mintTokensByMergePoolна Proxy для минта целевого токена - Withdraw: MergePool вызывает
withdrawTokensToEvmByMergePoolна Proxy для создания withdraw event
- Swap: MergePool вызывает
- 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 выполняет следующие роли:
Расчет daily limits: при withdraw через MergePool daily limits проверяются именно для канонического токена, а не для целевого токена вывода. Это позволяет унифицировать лимиты для всех токенов пула.
Референсная точка: canon token обычно выбирается как токен из основной/наиболее ликвидной сети (например, USDT из Ethereum для пула USDT).
Управление Canon Token
Canon token устанавливается при деплое MergePool через параметр _canonId - индекс в массиве токенов.
Canon token можно изменить через метод setCanon():
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:
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 для нормализации суммы:
uint128 amount = _convertDecimals(_amount, msg.sender, targetToken);Шаг 4: Проверки
MergePool проверяет условия:
- Целевой токен включен (enabled)
- Исходный токен включен
- Сконвертированная сумма > 0
Шаг 5: Mint целевого токена
Если проверки пройдены и burnType == BurnType.Swap:
_mintTokens(targetToken, amount, _walletOwner, remainingGasTo, operationPayload);MergePool вызывает на Proxy
Нормализация decimals
Функция _convertDecimals обеспечивает корректный обмен токенов с разными decimals:
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:
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:
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:
(
bool isLimitReached,
optional(DailyLimits) newLimits
) = _isOutgoingDailyLimitReached(_canonToken, _canonAmount);Шаг 5: Деплой EVM Event
Если лимиты не достигнуты, Proxy деплоит EVM event с целевым токеном:
_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-93bridge-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-485bridge-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
| Аспект | Обычный Withdraw | Withdraw через MergePool |
|---|---|---|
| Целевой токен | Всегда тот же токен, который сжигается | Может быть любой токен из пула |
| Daily limits | Проверяются для сжигаемого токена | Проверяются для канонического токена |
| Decimals конвертация | Не требуется | Выполняется при необходимости |
| Точка входа | Прямой burn → event | Burn → MergePool → Proxy → event |
Взаимодействие с daily limits
Ключевая идея: Daily limits проверяются для канонического токена независимо от того, какой токен выводится.
Сценарий:
- MergePool содержит: USDT-Ethereum (canon, 6 decimals), USDT-BSC (18 decimals), USDT-Avalanche (6 decimals)
- Daily outgoing limit для USDT-Ethereum: 100,000 токенов
- Пользователь A выводит 50,000 USDT-BSC → EVM
- Canon amount: 50,000 * 10^6 (в decimals канонического токена)
- Daily volume увеличивается на 50,000 USDT-Ethereum
- Пользователь B выводит 40,000 USDT-Avalanche → EVM
- Canon amount: 40,000 * 10^6
- Daily volume увеличивается на 40,000 USDT-Ethereum
- Пользователь 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:
struct Token {
uint8 decimals;
bool enabled;
}bridge-ton-contracts/contracts/bridge/interfaces/alien-token-merge/IMergePool.tsol:4-7
Поля:
decimals- количество десятичных знаков токена (получается через callbacktakeInfoили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-151bridge-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 токена:
enum BurnType {
Withdraw, // = 0
Swap // = 1
}bridge-ton-contracts/contracts/bridge/interfaces/alien-token-merge/IMergePool.tsol:9
Значения:
Withdraw(0) - вывод токенов на другую сеть через bridge eventSwap(1) - обмен на другой токен из пула внутри TVM
Payload структуры
Базовый payload для onAcceptTokensBurn:
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:9bridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:22
operationPayload для Withdraw:
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:
function deployMergePool(
uint256 _nonce, // уникальный nonce для деплоя
address[] _tokens, // список токенов пула
uint256 _canonId // индекс канонического токена в _tokens
) external override reserveAtLeastTargetBalancebridge-ton-contracts/contracts/bridge/proxy/multivault/alien-ton/V3/ProxyMultiVaultAlienJetton_V3_MergePool.tsol:30-52
Вызывается только owner или manager.
Деплой MergeRouter:
function deployMergeRouter(address _token) external override reserveAtLeastTargetBalancebridge-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
Последовательность деплоя (из скрипта):
- Деплой alien токенов (если не задеплоены)
- Деплой MergeRouter для каждого токена
- Деплой MergePool с списком токенов и указанием canon токена
- Привязка MergeRouter к MergePool через
setPool - Включение всех токенов через
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 |
| proxy | Static переменная, задается при деплое | Вызов acceptUpgrade, вызов callback'ов mint/withdraw |
bridge-ton-contracts/contracts/bridge/alien-token-merge/merge-pool/MergePool_V3.tsol:41-65
Модификаторы доступа:
onlyOwner- только owneronlyOwnerOrManager- owner или manageronlyProxy- только 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 права:
| Роль | Возможности |
|---|---|
| owner | setPool, disablePool, setManager |
| manager | setPool, 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):
function encodeMergePoolBurnSwapPayload(address _targetToken) public pure returns (TvmCell)Кодирует:
uint8(1) // burnType = Swap
_targetToken // целевой токен
empty // пустой operationPayloadbridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:8-16
Для jetton с remainingGasTo:
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:
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)) // operationPayloadbridge-ton-contracts/contracts/utils/cell-encoder/MergePoolCellEncoder.tsol:33-48
С remainingGasTo:
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:
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:
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
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 определяющий тип транзитной операции:
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 на фронтенде проверяется тип операции:
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')}`
}Коды ошибок
| Код | Константа | Описание |
|---|---|---|
| 2709 | WRONG_MERGE_POOL_NONCE | Неверный nonce MergePool при вызове методов Proxy (sender не соответствует deriveMergePool) |
| 2901 | WRONG_PROXY | Вызывающий контракт не является proxy контрактом |
| 2902 | ONLY_OWNER_OR_MANAGER | Действие доступно только owner или manager |
| 2903 | TOKEN_NOT_EXISTS | Токен не существует в пуле |
| 2904 | TOKEN_IS_CANON | Попытка удалить канонический токен |
| 2905 | TOKEN_ALREADY_EXISTS | Токен уже существует в пуле |
| 2906 | MERGE_POOL_IS_ZERO_ADDRESS | Попытка установить zero address как адрес merge pool в MergeRouter |
| 2907 | TOKEN_NOT_ENABLED | Токен не включен (disabled) |
| 2908 | TOKEN_DECIMALS_IS_ZERO | Decimals токена равны 0 (еще не получены) |
| 2909 | WRONG_CANON_ID | Неверный индекс канонического токена при деплое |
Риски и Edge Cases
| Риск/Case | Описание | Митигация |
|---|---|---|
| Decimals потеря точности | При swap из токена с большим decimals (18) в токен с малым decimals (6) малые суммы могут округлиться до 0 | MergePool проверяет amount == 0 после конвертации и возвращает исходные токены |
| Token disabled mid-swap | Токен отключается между инициацией burn и обработкой в MergePool | MergePool проверяет enabled при обработке и возвращает токены при enabled == false |
| Неправильный canon при лимитах | Если canon токен имеет малый decimals, а выводится токен с большим decimals, может быть bypass лимитов | TODO: требует уточнения механизма расчета canonAmount |
| MergeRouter с неправильным pool | MergeRouter указывает на несуществующий или неправильный 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 |