Limits
What are Limits
The limit system in MultiVault protects against instant contract drainage in case of relay node compromise or vulnerability exploitation. Limits are applied to two types of operations:
- Deposit Limits — restrict the maximum amount of tokens that can be stored on the vault
- Withdrawal Limits — restrict the withdrawal amount per single transaction and cumulatively over 24 hours
Why are Limits Needed?
- Relay node compromise — limits slow down fund withdrawal and provide time to detect an attack
- Smart contract exploits — limits block large transactions
- Liquidity protection — prevent instant pool drainage
- Fraud detection window — exceeding limits creates a pending withdrawal requiring approval
Types of Limits
1. Deposit Limits
Restrict the maximum token balance on the vault.
| Parameter | Description |
|---|---|
depositLimit | Maximum token balance on vault |
| Applies to | Alien tokens only |
| When exceeded | Deposit is rejected (revert) |
Why only for Alien: Native tokens are burned on deposit, not stored on the vault. Therefore, the vault balance for native tokens is always ~0.
2. Withdrawal Limits
Restrict withdrawal amounts from the vault.
| Parameter | Description |
|---|---|
undeclared | Per-transaction limit |
daily | Per-period limit (24 hours) |
enabled | Flag to enable limits for the token |
| Applies to | Both Alien and Native tokens |
| When exceeded | Pending Withdrawal is created |
Invariant: daily >= undeclared
Deposit Limits
Validation Formula
A deposit is rejected if the sum of the current vault balance and the deposit amount exceeds the established limit. If the limit is not set (equals zero), the check is skipped.
Application by Token Type
| Token Type | Applied? | Reason |
|---|---|---|
| Alien | ✅ Yes | Tokens are stored on vault |
| Native | ❌ No (effectively) | Tokens are burned, vault balance = 0 |
Withdrawal proceeds instantly if both conditions are met simultaneously. If limits are disabled for the token, the check is skipped.
Condition 1: Per-transaction (undeclared)
The current transaction amount must be strictly less than the per-transaction limit (undeclared).
Important: A transaction for exactly the limit amount requires approval.
Condition 2: Per-period (daily)
The sum of the current transaction amount plus already withdrawn during the period minus governance-approved amounts must be strictly less than the daily limit.
- Already withdrawn (total) — cumulative withdrawal for the current 24-hour period
- Approved (considered) — amounts approved by governance (subtracted to avoid blocking subsequent legitimate withdrawals)
Calculation Example
| Parameter | Value |
|---|---|
| Undeclared limit | 10,000 USDT |
| Daily limit | 50,000 USDT |
| Already withdrawn in period (total) | 30,000 USDT |
| Governance approved (considered) | 20,000 USDT |
| Withdrawal request | 15,000 USDT |
Check 1 (per-transaction):
15,000 < 10,000 → ❌ FAILCheck 2 (per-period):
15,000 + 30,000 - 20,000 = 25,000 < 50,000 → ✅ PASSResult: Although check 2 passed, check 1 failed → Pending Withdrawal is created with Required status.
24-Hour Period Mechanism
Period ID Calculation
Period ID is calculated by dividing the timestamp by the period duration (86400 seconds = 24 hours). All transactions within the same day receive the same period ID.
Examples
| Timestamp | Date/time (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 |
Period Parameters
For each period, two values are stored:
- total — cumulative withdrawal for the period (including pending)
- considered — amounts approved by governance
Automatic Reset
When transitioning to a new period, counters are automatically reset to zero (new entry in mapping).
Important: Uses eventTimestamp from the TVM event, not block.timestamp. This prevents manipulation through transaction execution delay.
Reasons for Creating Pending Withdrawal
Withdrawal enters Pending for two reasons:
1. Limit Exceeded
| Token Type | Status | Description |
|---|---|---|
| Native | Required | Always set to Required |
| Alien | Required | Set to Required if limits exceeded |
2. Insufficient Funds on Vault
| Token Type | Occurs? | Status | Reason |
|---|---|---|---|
| Native | ❌ No | — | Token is minted, not stored on vault |
| Alien | ✅ Yes | NotRequired | Vault may not have sufficient balance |
Pending Creation Diagram
Actions with Pending Withdrawal
1. Set New Bounty Value
Who can call: Only recipient
Restrictions:
- Bounty is set only for Alien tokens (for Native in EVM)
- By default, pending is created with bounty = 0
The setPendingWithdrawalBounty function checks that the token is not Native and that the bounty does not exceed the withdrawal amount, then records the new bounty value.
Note: When calling saveWithdrawAlien, you can specify the bounty immediately if the transaction sender is the withdrawal recipient.
2. Cancel Fully or Partially
Who can call: Only recipient
Restrictions:
- Available only for Alien tokens
- Available only with
NotRequiredorApprovedstatus - When partially canceling, can set new bounty
Result: Creates a reverse transfer to the TVM network for the specified amount
The cancelPendingWithdrawal function checks that the token is not Native and that the cancellation amount is correct. Then it decreases the pending amount by the specified value, initiates a reverse transfer to TVM, and optionally sets a new bounty for the remaining amount.
3. Approve or Reject (for Required Status)
Who can call: Only governance or withdrawGuardian
Requirements:
- Current status must be
Required - Can only set to
ApprovedorRejected
Logic on Approve:
- If vault balance is sufficient OR it's a Native token → automatic withdrawal
- Otherwise, just changes status to
Approved
Callback: ❌ NOT called
The setPendingWithdrawalApprove function checks that the current status is Required and that Approved or Rejected is being set. When setting Approved, if the vault balance is sufficient or it's a Native token, withdrawal is automatically executed. In any case, the amount is added to considered for the current period.
4. Force Withdraw Manually
Who can call: Any address
Requirements:
- Status
NotRequiredorApproved amount > 0
Logic:
amountis transferred to the recipient in full- Bounty is not credited to anyone (goes to the recipient)
Callback: ✅ Called
The forceWithdraw function accepts an array of pending withdrawals and for each: resets the pending amount to zero, transfers the full amount to the recipient (without deducting bounty), and calls the callback.
5. Close via Deposit
Who can call: Any address (usually arbitrageur)
Requirements:
- Pending withdrawals status:
NotRequiredorApproved - Deposit token matches the pending token
Logic:
- User sends a deposit specifying pending withdrawals and minimum total bounty
- For each pending: recipient receives amount minus bounty
- Callback is called for each closed pending
- Creates an EVM→TVM transfer for the deposit amount plus accumulated bounties minus fees
The function iterates through specified pending withdrawals, accumulates bounties, transfers amounts minus bounty to recipients, and calls callbacks. Then checks that total bounty is not less than expected and creates a transfer to TVM.
Summary Table of Actions
| Action | Who Can | Required Status | Callback | Bounty |
|---|---|---|---|---|
| Set Bounty | recipient | Any | — | New value is set |
| Cancel | recipient | NotRequired / Approved | — | Can set new |
| Approve/Reject | governance / withdrawGuardian | Required | ❌ No | — |
| Force Withdraw | Any | NotRequired / Approved | ✅ Yes | Not deducted, goes to recipient |
| Close via Deposit | Any | NotRequired / Approved | ✅ Yes | Goes to depositor (arbitrageur) |
Data Structures
WithdrawalLimits
Structure stores withdrawal limits for a token:
- undeclared — per-transaction limit
- daily — 24-hour period limit
- enabled — flag to enable limits
WithdrawalPeriodParams
Structure stores 24-hour period parameters:
- total — cumulative withdrawal for the period
- considered — amounts approved by governance
PendingWithdrawalParams
Structure stores pending withdrawal parameters:
- token — token address
- amount — amount to withdraw (after fees)
- bounty — reward for arbitrageur
- timestamp — TVM event timestamp
- approveStatus — approval status
- chainId — source Chain ID
- callback — callback data after withdrawal
ApproveStatus
Possible approval statuses for pending withdrawal:
- NotRequired (0) — approval not required (insufficient funds on vault)
- Required (1) — approval required (limits exceeded)
- Approved (2) — approved by governance or withdrawGuardian
- Rejected (3) — rejected
Storage Mappings
Data is stored in the following mappings:
- tokens_ — token information, including depositLimit
- withdrawalLimits_ — withdrawal limits by token
- withdrawalPeriods_ — period parameters (token → period ID → parameters)
- pendingWithdrawals_ — pending withdrawals (user → ID → parameters)
- pendingWithdrawalsPerUser — pending count for each user (used as ID)
- pendingWithdrawalsTotal — total pending by token
Period duration — 86400 seconds (24 hours).
Limit Management
Setting Limits
All functions require the onlyGovernance modifier.
Deposit Limit
The setDepositLimit function sets the maximum token balance on the vault.
Withdrawal Limits
The following management functions are available:
- setDailyWithdrawalLimits — sets the daily limit (checks that it's not less than undeclared)
- setUndeclaredWithdrawalLimits — sets the per-transaction limit (checks that it's not more than daily)
- enableWithdrawalLimits — enables limits for the token
- disableWithdrawalLimits — disables limits for the token
Events
| Event | Parameters | When Emitted |
|---|---|---|
UpdateDailyWithdrawalLimits | token, limit | Daily limit changed |
UpdateUndeclaredWithdrawalLimits | token, limit | Undeclared limit changed |
UpdateWithdrawalLimitStatus | token, status | Limits enabled/disabled |
PendingWithdrawalCreated | recipient, id, token, amount, payloadId | Pending created |
PendingWithdrawalUpdateApproveStatus | recipient, id, approveStatus | Status changed |
PendingWithdrawalUpdateBounty | recipient, id, bounty | Bounty changed |
PendingWithdrawalWithdraw | recipient, id, amount | Auto-withdrawal on approve |
PendingWithdrawalForce | recipient, id | Force withdraw |
PendingWithdrawalCancel | recipient, id, amount | Pending cancelled |
PendingWithdrawalFill | recipient, id | Closed via deposit |
Access Rights
| Action | Required Role |
|---|---|
| Set deposit limit | governance |
| Set withdrawal limits | governance |
| Enable/disable limits | governance |
| Approve/Reject pending | governance OR withdrawGuardian |
| Set bounty | recipient of pending withdrawal |
| Cancel pending | recipient of pending withdrawal |
| Force withdraw | Any address |
| Close via deposit | Any address |
Risks and Edge Cases
1. Bypassing Limits via Split Transactions
Risk: Attacker splits a large withdrawal into many small ones.
Protection: Daily limit tracks cumulative withdrawal over 24 hours.
2. daily < undeclared
Risk: Incorrect configuration.
Protection: Validation require(daily >= undeclared) in both setter functions.
3. Limits Disabled (enabled = false)
Risk: If limits are not set for a token, protection doesn't apply.
Mitigation: Governance should set limits for each new token.
4. Timestamp Manipulation
Risk: Attacker delays transaction until new period.
Protection: Uses eventTimestamp from TVM event, not block.timestamp.
5. Race Condition on Approve
Risk: Governance approves pending, but vault balance is insufficient.
Result: Status changes to Approved, but tokens are not transferred. Requires forceWithdraw().
6. Cancel Unavailable for Native Tokens
Limitation: cancelPendingWithdrawal() is available only for Alien tokens.
Reason: Native tokens exist only in the TVM network. Cannot cancel pending.
7. Strict Inequality in Check
Feature: A transaction for exactly the limit amount requires approval (amount < limit, not <=).