Лимиты (Limits)
Что такое лимиты
Система лимитов в MultiVault защищает от мгновенного опустошения контракта при компрометации relay-нод или эксплойте уязвимости. Лимиты применяются к двум типам операций:
- Лимиты на депозит — ограничивают максимальное количество токенов, которое может храниться на vault
- Лимиты на вывод — ограничивают сумму вывода за одну транзакцию и суммарно за 24 часа
Зачем нужны лимиты?
- Компрометация relay-нод — лимиты замедлят вывод средств и дадут время на обнаружение атаки
- Эксплойты смарт-контрактов — лимиты блокируют крупные транзакции
- Защита ликвидности — предотвращают моментальное опустошение пулов
- Fraud detection window — превышение лимитов создаёт pending withdrawal, требующий одобрения
Типы лимитов
1. Лимиты на депозит (Deposit Limits)
Ограничивают максимальный баланс токена на vault.
| Параметр | Описание |
|---|---|
depositLimit | Максимальный баланс токена на vault |
| Применяется | Только для Alien токенов |
| При превышении | Депозит отклоняется (revert) |
Почему только для Alien: Native токены при депозите сжигаются (burn), а не хранятся на vault. Поэтому баланс vault для native токенов всегда ~0.
2. Лимиты на вывод (Withdrawal Limits)
Ограничивают суммы вывода токенов из vault.
| Параметр | Описание |
|---|---|
undeclared | Лимит на одну транзакцию (per-transaction limit) |
daily | Лимит на сумму за 24 часа (per-period limit) |
enabled | Флаг включения лимитов для токена |
| Применяется | Для Alien и Native токенов |
| При превышении | Создаётся Pending Withdrawal |
Инвариант: daily >= undeclared
Лимиты на депозит
Формула проверки
Депозит отклоняется, если сумма текущего баланса vault и суммы депозита превысит установленный лимит. Если лимит не установлен (равен нулю), проверка пропускается.
Применение по типу токена
| Тип токена | Применяется? | Причина |
|---|---|---|
| Alien | ✅ Да | Токены хранятся на vault |
| Native | ❌ Нет (фактически) | Токены сжигаются, баланс vault = 0 |
Вывод проходит мгновенно, если выполняются оба условия одновременно. Если лимиты отключены для токена, проверка пропускается.
Условие 1: Per-transaction (undeclared)
Сумма текущей транзакции должна быть строго меньше лимита на одну транзакцию (undeclared).
Важно: Транзакция ровно на сумму лимита требует одобрения.
Условие 2: Per-period (daily)
Сумма текущей транзакции плюс уже выведенное за период минус одобренное governance должна быть строго меньше суточного лимита (daily).
- Уже выведено (total) — суммарный вывод за текущий 24-часовой период
- Одобрено (considered) — суммы, одобренные governance (вычитаются, чтобы не блокировать последующие легитимные выводы)
Пример расчёта
| Параметр | Значение |
|---|---|
| Undeclared limit | 10,000 USDT |
| Daily limit | 50,000 USDT |
| Уже выведено за период (total) | 30,000 USDT |
| Одобрено governance (considered) | 20,000 USDT |
| Запрос на вывод | 15,000 USDT |
Проверка 1 (per-transaction):
15,000 < 10,000 → ❌ FAILПроверка 2 (per-period):
15,000 + 30,000 - 20,000 = 25,000 < 50,000 → ✅ PASSРезультат: Хотя проверка 2 прошла, проверка 1 провалилась → создаётся Pending Withdrawal со статусом Required.
Механизм 24-часовых периодов
Вычисление ID периода
ID периода вычисляется делением timestamp на длительность периода (86400 секунд = 24 часа). Все транзакции в рамках одних суток получают одинаковый ID периода.
Примеры
| Timestamp | Дата/время (UTC) | Period ID |
|---|---|---|
| 1704067200 | 2024-01-01 00:00:00 | 19723 |
| 1704153599 | 2024-01-01 23:59:59 | 19723 |
| 1704153600 | 2024-01-02 00:00:00 | 19724 |
Параметры периода
Для каждого периода хранятся два значения:
- total — суммарный вывод за период (включая pending)
- considered — суммы, одобренные governance
Автоматический сброс
При переходе в новый период счётчики автоматически обнуляются (новая запись в маппинге).
Важно: Используется eventTimestamp из TVM события, а не block.timestamp. Это предотвращает манипуляции через задержку исполнения транзакции.
Причины создания Pending Withdrawal
Вывод переходит в Pending по двум причинам:
1. Превышение лимитов
| Тип токена | Статус | Описание |
|---|---|---|
| Native | Required | Всегда устанавливается Required |
| Alien | Required | Устанавливается Required если лимиты превышены |
2. Недостаток средств на vault
| Тип токена | Происходит? | Статус | Причина |
|---|---|---|---|
| Native | ❌ Нет | — | Токен минтится, не хранится на vault |
| Alien | ✅ Да | NotRequired | Vault может не иметь достаточного баланса |
Диаграмма создания Pending
Действия с Pending Withdrawal
1. Задать новое значение для bounty
Кто может вызвать: Только recipient
Ограничения:
- Bounty задаётся только для Alien токенов (для Native в EVM)
- По умолчанию pending создаётся с bounty = 0
Функция setPendingWithdrawalBounty проверяет, что токен не Native и что bounty не превышает сумму вывода, затем записывает новое значение bounty.
Примечание: При вызове saveWithdrawAlien можно сразу указать bounty, если отправитель транзакции является получателем вывода.
2. Отменить полностью или частично (Cancel)
Кто может вызвать: Только recipient
Ограничения:
- Доступно только для Alien токенов
- Доступно только при статусе
NotRequiredилиApproved - При частичной отмене можно задать новую bounty
Результат: Создаётся обратный трансфер в TVM сеть на указанный amount
Функция cancelPendingWithdrawal проверяет, что токен не Native и что сумма отмены корректна. Затем уменьшает сумму pending на указанную величину, инициирует обратный трансфер в TVM и опционально устанавливает новую bounty для оставшейся суммы.
3. Approve или Reject (для статуса Required)
Кто может вызвать: Только governance или withdrawGuardian
Требования:
- Текущий статус должен быть
Required - Можно установить только
ApprovedилиRejected
Логика при Approve:
- Если баланс vault достаточен ИЛИ это Native токен → автоматический вывод
- Иначе просто меняется статус на
Approved
Callback: ❌ НЕ вызывается
Функция setPendingWithdrawalApprove проверяет, что текущий статус Required и что устанавливается Approved или Rejected. При установке Approved, если баланс vault достаточен или это Native токен, автоматически выполняется вывод. В любом случае сумма добавляется к considered для текущего периода.
4. Протолкнуть вручную (Force Withdraw)
Кто может вызвать: Любой адрес
Требования:
- Статус
NotRequiredилиApproved amount > 0
Логика:
amountпереводится получателю в полном объёме- Bounty никому не зачисляется (идёт получателю)
Callback: ✅ Вызывается
Функция forceWithdraw принимает массив pending withdrawals и для каждого: обнуляет сумму pending, переводит получателю полную сумму (без вычета bounty) и вызывает callback.
5. Закрыть через депозит
Кто может вызвать: Любой адрес (обычно арбитражёр)
Требования:
- Статус pending withdrawals:
NotRequiredилиApproved - Токен депозита совпадает с токеном pending
Логика:
- Пользователь отправляет депозит с указанием pending withdrawals и минимальной суммарной bounty
- Для каждого pending: получателю переводится сумма за вычетом bounty
- Callback вызывается для каждого закрытого pending
- Создаётся EVM→TVM трансфер на сумму депозита плюс накопленные bounties за вычетом комиссии
Функция перебирает указанные pending withdrawals, накапливает bounties, переводит получателям их суммы за вычетом bounty и вызывает callback. Затем проверяет, что суммарная bounty не меньше ожидаемой, и создаёт трансфер в TVM.
Сводная таблица действий
| Действие | Кто может | Требуемый статус | Callback | Bounty |
|---|---|---|---|---|
| Set Bounty | recipient | Любой | — | Задаётся новое значение |
| Cancel | recipient | NotRequired / Approved | — | Можно задать новое |
| Approve/Reject | governance / withdrawGuardian | Required | ❌ Нет | — |
| Force Withdraw | Любой | NotRequired / Approved | ✅ Да | Не вычитается, идёт recipient |
| Close via Deposit | Любой | NotRequired / Approved | ✅ Да | Идёт депозитору (арбитражёру) |
Структура данных
WithdrawalLimits
Структура хранит лимиты на вывод для токена:
- undeclared — лимит на одну транзакцию
- daily — лимит за 24-часовой период
- enabled — флаг включения лимитов
WithdrawalPeriodParams
Структура хранит параметры 24-часового периода:
- total — суммарный вывод за период
- considered — суммы, одобренные governance
PendingWithdrawalParams
Структура хранит параметры отложенного вывода:
- token — адрес токена
- amount — сумма к выводу (после комиссии)
- bounty — награда для арбитражёра
- timestamp — timestamp TVM события
- approveStatus — статус одобрения
- chainId — Chain ID источника
- callback — данные для callback после вывода
ApproveStatus
Возможные статусы одобрения pending withdrawal:
- NotRequired (0) — одобрение не требуется (недостаток средств на vault)
- Required (1) — требуется одобрение (превышение лимитов)
- Approved (2) — одобрено governance или withdrawGuardian
- Rejected (3) — отклонено
Storage маппинги
Данные хранятся в следующих маппингах:
- tokens_ — информация о токенах, включая depositLimit
- withdrawalLimits_ — лимиты на вывод по токенам
- withdrawalPeriods_ — параметры периодов (токен → ID периода → параметры)
- pendingWithdrawals_ — отложенные выводы (пользователь → ID → параметры)
- pendingWithdrawalsPerUser — счётчик pending для каждого пользователя (используется как ID)
- pendingWithdrawalsTotal — суммарный pending по токену
Длительность периода — 86400 секунд (24 часа).
Управление лимитами
Установка лимитов
Все функции требуют модификатор onlyGovernance.
Deposit Limit
Функция setDepositLimit устанавливает максимальный баланс токена на vault.
Withdrawal Limits
Доступны следующие функции управления:
- setDailyWithdrawalLimits — устанавливает суточный лимит (проверяет, что он не меньше undeclared)
- setUndeclaredWithdrawalLimits — устанавливает лимит на транзакцию (проверяет, что он не больше daily)
- enableWithdrawalLimits — включает лимиты для токена
- disableWithdrawalLimits — отключает лимиты для токена
События
| Событие | Параметры | Когда эмитится |
|---|---|---|
UpdateDailyWithdrawalLimits | token, limit | Изменён daily limit |
UpdateUndeclaredWithdrawalLimits | token, limit | Изменён undeclared limit |
UpdateWithdrawalLimitStatus | token, status | Включены/отключены лимиты |
PendingWithdrawalCreated | recipient, id, token, amount, payloadId | Создан pending |
PendingWithdrawalUpdateApproveStatus | recipient, id, approveStatus | Изменён статус |
PendingWithdrawalUpdateBounty | recipient, id, bounty | Изменена bounty |
PendingWithdrawalWithdraw | recipient, id, amount | Автовывод при approve |
PendingWithdrawalForce | recipient, id | Force withdraw |
PendingWithdrawalCancel | recipient, id, amount | Отмена pending |
PendingWithdrawalFill | recipient, id | Закрыт через депозит |
Права доступа
| Действие | Требуемая роль |
|---|---|
| Установить deposit limit | governance |
| Установить withdrawal limits | governance |
| Включить/отключить лимиты | governance |
| Approve/Reject pending | governance ИЛИ withdrawGuardian |
| Set bounty | recipient pending withdrawal |
| Cancel pending | recipient pending withdrawal |
| Force withdraw | Любой адрес |
| Close via deposit | Любой адрес |
Риски и Edge Cases
1. Обход лимитов через split-транзакции
Риск: Атакующий разбивает крупный вывод на множество мелких.
Защита: Daily limit отслеживает суммарный вывод за 24 часа.
2. daily < undeclared
Риск: Некорректная настройка.
Защита: Валидация require(daily >= undeclared) в обоих setter-функциях.
3. Лимиты отключены (enabled = false)
Риск: Если лимиты не установлены для токена, защита не действует.
Митигация: Governance должен устанавливать лимиты для каждого нового токена.
4. Манипуляция timestamp
Риск: Атакующий задерживает транзакцию до нового периода.
Защита: Используется eventTimestamp из TVM события, а не block.timestamp.
5. Race condition при approve
Риск: Governance одобряет pending, но баланс vault недостаточен.
Результат: Статус меняется на Approved, но токены не переводятся. Требуется forceWithdraw().
6. Cancel недоступен для Native токенов
Ограничение: cancelPendingWithdrawal() доступен только для Alien токенов.
Причина: Native токены существуют только в TVM сети. Отменить pending невозможно.
7. Строгое неравенство в проверке
Особенность: Транзакция ровно на сумму лимита требует одобрения (amount < limit, а не <=).