Liquidity Request (Pending Withdrawal)
Что такое Liquidity Request
Когда пользователь хочет вывести токены из TVM в EVM сеть, обычно это происходит мгновенно. Но иногда вывод откладывается — создаётся Liquidity Request (LR).
Почему вывод может быть отложен?
- Превышены лимиты — сумма слишком большая для автоматического вывода
- Не хватает ликвидности — на контракте недостаточно токенов (только для Alien токенов — изначально выпущенных в EVM)
Что происходит дальше?
Пользователь не остаётся без денег — есть несколько способов завершить вывод:
| Способ | Кто выполняет | Когда подходит |
|---|---|---|
| Approve | Администратор | Крупный вывод прошёл проверку |
| Fill | Провайдер ликвидности | LP хочет заработать bounty |
| Force Withdraw | Любой | LR одобрен, нужно просто забрать |
| Cancel | Пользователь | Передумал, хочет вернуть в TVM |
Важно
Liquidity Request существует только на стороне EVM сети.
Когда создаётся Liquidity Request
Условия создания
Liquidity Request создаётся при вызове saveWithdrawNative() или saveWithdrawAlien(), когда одно из условий не выполнено:
| Тип токена | Условие мгновенного вывода | Если не выполнено |
|---|---|---|
| Native (из TVM) | Withdrawal limits пройдены | Создаётся LR с approveStatus = Required |
| Alien (из EVM) | Withdrawal limits пройдены И достаточно ликвидности | Создаётся LR |
Для разных типов токенов
Native токены (изначально выпущены в TVM):
- Проверяются только лимиты
- Если лимиты превышены → LR со статусом
Required(нужно одобрение)
Alien токены (изначально выпущены в EVM):
- Проверяются лимиты И наличие ликвидности
- Если не хватает ликвидности → LR со статусом
NotRequired(можно сразу fill) - Если превышены лимиты → LR со статусом
Required(нужно одобрение)
Схема создания Liquidity Request
┌─────────────────────────────────────────────────────────────────┐
│ saveWithdrawNative() / saveWithdrawAlien() │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Декодировать event data │
│ 2. Проверить chainId и blacklist │
│ 3. Рассчитать и вычесть fee │
│ 4. Проверить withdrawal limits │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Limits пройдены? │ │
│ │ (Для Alien токенов: И достаточно ликвидности?) │ │
│ └─────────────────┬───────────────────────┬───────────────┘ │
│ YES NO │
│ ↓ ↓ │
│ Мгновенный вывод Создать Liquidity Request │
│ IERC20.transfer() → approveStatus = NotRequired│
│ или mint() → emit PendingWithdrawalCreated│
│ ↓ │
│ Limits нарушены? │
│ YES → approveStatus = Required│
│ │
└─────────────────────────────────────────────────────────────────┘Пример сценария
Пользователь выводит 100,000 USDT из TVM → EVM
1. Контракт проверяет: лимит на транзакцию = 50,000 USDT
2. 100,000 > 50,000 — лимит превышен
3. Создаётся LR с approveStatus = Required
4. Пользователь ждёт одобрения администратора
5. После Approve — получает свои 100,000 USDTСтруктура данных
Что хранится в каждом LR
| Поле | Что означает |
|---|---|
token | Какой токен выводится |
amount | Сколько (уже за вычетом комиссии) |
bounty | Награда для LP за fill (устанавливает пользователь) |
timestamp | Когда был создан запрос в TVM |
approveStatus | Текущий статус (см. ниже) |
chainId | В какую сеть выводить |
callback | Данные для вызова после вывода |
Статусы LR
| Статус | Что значит | Что можно делать |
|---|---|---|
NotRequired | Одобрение не нужно | Fill, Force Withdraw, Cancel |
Required | Ждёт одобрения админа | Только ждать Approve/Reject |
Approved | Одобрено | Fill, Force Withdraw, Cancel |
Rejected | Отклонено | Cancel (вернуть в TVM) |
Как идентифицируется LR
Каждый LR имеет уникальный ID в рамках пользователя:
recipient— адрес получателяid— порядковый номер LR у этого пользователя
Жизненный цикл
Путь Liquidity Request
┌──────────────┐
│ Created │
│ (NotRequired │
│ or Required)│
└──────┬───────┘
│
┌────────────────────────────┼────────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Approved │ │ Rejected │ │ Cancelled │
│ (by guardian) │ │ (by guardian) │ │ (by recipient) │
└───────┬────────┘ └────────────────┘ └────────────────┘
│
┌───────┴───────┐
│ │
▼ ▼
┌─────────┐ ┌──────────┐
│ Filled │ │ Withdrawn│
│ (by LP) │ │ (direct) │
└─────────┘ └──────────┘Кто что может делать
| Действие | Кто может | Из каких статусов |
|---|---|---|
| Approve/Reject | governance, withdrawGuardian | Required |
| Fill | Любой (обычно LP) | NotRequired, Approved |
| Force Withdraw | Любой | NotRequired, Approved |
| Cancel | Только владелец LR | NotRequired, Approved |
| Изменить bounty | Только владелец LR | Любой (пока amount > 0) |
События для отслеживания
| Event | Когда эмитится | Параметры |
|---|---|---|
PendingWithdrawalCreated | При создании LR | recipient, id, token, amount, payloadId |
PendingWithdrawalUpdateApproveStatus | При изменении approve status | recipient, id, approveStatus |
PendingWithdrawalFill | При fill через deposit | recipient, id |
PendingWithdrawalWithdraw | При выводе (approve + ликвидность) | recipient, id, amount |
PendingWithdrawalForce | При force withdraw | recipient, id |
PendingWithdrawalCancel | При отмене | recipient, id, amount |
PendingWithdrawalUpdateBounty | При изменении bounty | recipient, id, bounty |
Управление: Approve и Reject
Кто принимает решение
Одобрить или отклонить LR могут только:
governance— главный администраторwithdrawGuardian— специальная роль для управления выводами
Как одобрить
setPendingWithdrawalApprove(pendingWithdrawalId, ApproveStatus.Approved)Что происходит при Approve:
- Статус меняется на
Approved - Если на контракте есть ликвидность — токены сразу отправляются пользователю
- Если ликвидности нет — пользователь может ждать Fill или вызвать Force Withdraw позже
Как отклонить
setPendingWithdrawalApprove(pendingWithdrawalId, ApproveStatus.Rejected)Что происходит при Reject:
- Статус меняется на
Rejected - Токены остаются заблокированными
- Пользователь может вернуть их в TVM через Cancel
Массовое одобрение
Можно одобрить/отклонить несколько LR за одну транзакцию — это экономит газ.
Механизм Fill (LP)
Как работает Fill
Провайдер ликвидности (LP) может "заполнить" чужой LR:
- LP вносит свои токены
- Пользователь получает свои деньги (минус bounty)
- LP получает bounty + отправляет токены в TVM
Как это работает
LP вносит 1000 USDT через deposit()
│
▼
Пользователь с LR получает 990 USDT (установил bounty = 10)
│
▼
LP получает в TVM: 1000 + 10 - fee = ~1008 USDTПроцесс:
- LP вызывает deposit с токенами и списком LR
- Для каждого LR:
- Проверяется что токен совпадает
- Проверяется что достаточно средств
- Получатель получает
amount - bounty - LP накапливает bounty
- LP получает в TVM:
depositAmount + totalBounty - fee
Bounty — вознаграждение для LP
Пользователь сам устанавливает bounty — это стимул для LP заполнить именно его LR:
- Чем выше bounty, тем привлекательнее LR для LP
- Bounty не может быть больше суммы вывода
- Можно изменить bounty в любой момент (пока LR активен)
Как установить bounty:
setPendingWithdrawalBounty(id, bounty)Cancel — отмена LR
Получатель может отменить Liquidity Request и вернуть токены в TVM.
cancelPendingWithdrawal(id, amount, tvmRecipient, ...)Ограничения:
- Только для Alien токенов (из EVM)
- Только владелец LR
- Статус должен быть
NotRequiredилиApproved
Риски и Edge Cases
| Риск/Ошибка | Причина | Защита/Решение |
|---|---|---|
| Застрявший LR | Rejected статус, нет cancel | Recipient может cancel и вернуть в TVM |
| Bounty > amount | Попытка установить слишком большой bounty | Проверка bounty <= amount |
| Fill с неправильным токеном | LP указал LR другого токена | Проверка pendingWithdrawal.token == d.token |
| Fill уже заполненного | Попытка fill LR с amount = 0 | Проверка pendingWithdrawal.amount > 0 |
| Двойной approve | Попытка повторно одобрить | Проверка approveStatus == Required |
Коды ошибок
| Ошибка | Причина |
|---|---|
"Pending: amount is zero" | LR уже заполнен или отменён |
"Pending: native token" | Bounty нельзя установить для Native токенов (из TVM) |
"Pending: bounty too large" | Bounty больше суммы вывода |
"Pending: wrong current approve status" | Нельзя одобрить — статус не Required |
"Pending: wrong approve status" | Указан неправильный новый статус |
"Pending: wrong amount" | Неверная сумма для cancel |
"Pending: already filled" | LR уже заполнен |
"Pending: wrong token" | Токен не совпадает |
"Pending: deposit insufficient" | Недостаточно средств для fill |