Liquidity Request (Pending Withdrawal)
What is a Liquidity Request
When a user wants to withdraw tokens from TVM to an EVM network, it usually happens instantly. But sometimes the withdrawal is delayed — a Liquidity Request (LR) is created.
Why might a withdrawal be delayed?
- Limits exceeded — the amount is too large for automatic withdrawal
- Insufficient liquidity — the contract doesn't have enough tokens (only for Alien tokens — originally issued in EVM)
What happens next?
The user doesn't lose their funds — there are several ways to complete the withdrawal:
| Method | Who executes | When it's suitable |
|---|---|---|
| Approve | Administrator | Large withdrawal passed verification |
| Fill | Liquidity provider | LP wants to earn bounty |
| Force Withdraw | Anyone | LR is approved, just need to claim |
| Cancel | User | Changed mind, wants to return to TVM |
Important
Liquidity Request exists only on the EVM network side.
When a Liquidity Request is Created
Creation Conditions
A Liquidity Request is created when calling saveWithdrawNative() or saveWithdrawAlien(), when one of the conditions is not met:
| Token Type | Instant withdrawal condition | If not met |
|---|---|---|
| Native (from TVM) | Withdrawal limits passed | LR created with approveStatus = Required |
| Alien (from EVM) | Withdrawal limits passed AND sufficient liquidity | LR created |
For Different Token Types
Native tokens (originally issued in TVM):
- Only limits are checked
- If limits exceeded → LR with status
Required(approval needed)
Alien tokens (originally issued in EVM):
- Limits AND liquidity availability are checked
- If insufficient liquidity → LR with status
NotRequired(can fill immediately) - If limits exceeded → LR with status
Required(approval needed)
Liquidity Request Creation Flow
┌─────────────────────────────────────────────────────────────────┐
│ saveWithdrawNative() / saveWithdrawAlien() │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Decode event data │
│ 2. Check chainId and blacklist │
│ 3. Calculate and deduct fee │
│ 4. Check withdrawal limits │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Limits passed? │ │
│ │ (For Alien tokens: AND sufficient liquidity?) │ │
│ └─────────────────┬───────────────────────┬───────────────┘ │
│ YES NO │
│ ↓ ↓ │
│ Instant withdrawal Create Liquidity Request │
│ IERC20.transfer() → approveStatus = NotRequired│
│ or mint() → emit PendingWithdrawalCreated│
│ ↓ │
│ Limits violated? │
│ YES → approveStatus = Required│
│ │
└─────────────────────────────────────────────────────────────────┘Example Scenario
User withdraws 100,000 USDT from TVM → EVM
1. Contract checks: transaction limit = 50,000 USDT
2. 100,000 > 50,000 — limit exceeded
3. LR created with approveStatus = Required
4. User waits for administrator approval
5. After Approve — receives their 100,000 USDTData Structure
What is Stored in Each LR
| Field | What it means |
|---|---|
token | Which token is being withdrawn |
amount | How much (already after fee deduction) |
bounty | Reward for LP for fill (set by user) |
timestamp | When the request was created in TVM |
approveStatus | Current status (see below) |
chainId | Which network to withdraw to |
callback | Data for callback after withdrawal |
LR Statuses
| Status | What it means | What can be done |
|---|---|---|
NotRequired | Approval not needed | Fill, Force Withdraw, Cancel |
Required | Waiting for admin approval | Only wait for Approve/Reject |
Approved | Approved | Fill, Force Withdraw, Cancel |
Rejected | Rejected | Cancel (return to TVM) |
How LR is Identified
Each LR has a unique ID within a user:
recipient— recipient addressid— sequential LR number for this user
Lifecycle
Liquidity Request Path
┌──────────────┐
│ Created │
│ (NotRequired │
│ or Required)│
└──────┬───────┘
│
┌────────────────────────────┼────────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Approved │ │ Rejected │ │ Cancelled │
│ (by guardian) │ │ (by guardian) │ │ (by recipient) │
└───────┬────────┘ └────────────────┘ └────────────────┘
│
┌───────┴───────┐
│ │
▼ ▼
┌─────────┐ ┌──────────┐
│ Filled │ │ Withdrawn│
│ (by LP) │ │ (direct) │
└─────────┘ └──────────┘Who Can Do What
| Action | Who can | From which statuses |
|---|---|---|
| Approve/Reject | governance, withdrawGuardian | Required |
| Fill | Anyone (usually LP) | NotRequired, Approved |
| Force Withdraw | Anyone | NotRequired, Approved |
| Cancel | Only LR owner | NotRequired, Approved |
| Change bounty | Only LR owner | Any (while amount > 0) |
Events for Tracking
| Event | When emitted | Parameters |
|---|---|---|
PendingWithdrawalCreated | When LR is created | recipient, id, token, amount, payloadId |
PendingWithdrawalUpdateApproveStatus | When approve status changes | recipient, id, approveStatus |
PendingWithdrawalFill | When filled via deposit | recipient, id |
PendingWithdrawalWithdraw | When withdrawn (approve + liquidity) | recipient, id, amount |
PendingWithdrawalForce | When force withdraw | recipient, id |
PendingWithdrawalCancel | When cancelled | recipient, id, amount |
PendingWithdrawalUpdateBounty | When bounty changes | recipient, id, bounty |
Management: Approve and Reject
Who Makes the Decision
Only these roles can approve or reject LR:
governance— main administratorwithdrawGuardian— special role for managing withdrawals
How to Approve
setPendingWithdrawalApprove(pendingWithdrawalId, ApproveStatus.Approved)What happens on Approve:
- Status changes to
Approved - If contract has liquidity — tokens are immediately sent to user
- If no liquidity — user can wait for Fill or call Force Withdraw later
How to Reject
setPendingWithdrawalApprove(pendingWithdrawalId, ApproveStatus.Rejected)What happens on Reject:
- Status changes to
Rejected - Tokens remain locked
- User can return them to TVM via Cancel
Batch Approval
Multiple LRs can be approved/rejected in one transaction — this saves gas.
Fill Mechanism (LP)
How Fill Works
A liquidity provider (LP) can "fill" someone else's LR:
- LP deposits their tokens
- User receives their funds (minus bounty)
- LP receives bounty + sends tokens to TVM
How It Works
LP deposits 1000 USDT via deposit()
│
▼
User with LR receives 990 USDT (set bounty = 10)
│
▼
LP receives in TVM: 1000 + 10 - fee = ~1008 USDTProcess:
- LP calls deposit with tokens and list of LRs
- For each LR:
- Check that token matches
- Check that sufficient funds
- Recipient receives
amount - bounty - LP accumulates bounty
- LP receives in TVM:
depositAmount + totalBounty - fee
Bounty — Reward for LP
User sets bounty themselves — this is an incentive for LP to fill their specific LR:
- Higher bounty makes LR more attractive to LPs
- Bounty cannot be greater than withdrawal amount
- Can change bounty at any time (while LR is active)
How to set bounty:
setPendingWithdrawalBounty(id, bounty)Cancel — LR Cancellation
Recipient can cancel a Liquidity Request and return tokens to TVM.
cancelPendingWithdrawal(id, amount, tvmRecipient, ...)Restrictions:
- Only for Alien tokens (from EVM)
- Only LR owner
- Status must be
NotRequiredorApproved
Risks and Edge Cases
| Risk/Error | Cause | Protection/Solution |
|---|---|---|
| Stuck LR | Rejected status, no cancel | Recipient can cancel and return to TVM |
| Bounty > amount | Attempt to set too large bounty | Check bounty <= amount |
| Fill with wrong token | LP specified LR of different token | Check pendingWithdrawal.token == d.token |
| Fill already filled | Attempt to fill LR with amount = 0 | Check pendingWithdrawal.amount > 0 |
| Double approve | Attempt to approve again | Check approveStatus == Required |
Error Codes
| Error | Cause |
|---|---|
"Pending: amount is zero" | LR already filled or cancelled |
"Pending: native token" | Bounty cannot be set for Native tokens (from TVM) |
"Pending: bounty too large" | Bounty greater than withdrawal amount |
"Pending: wrong current approve status" | Cannot approve — status is not Required |
"Pending: wrong approve status" | Wrong new status specified |
"Pending: wrong amount" | Incorrect amount for cancel |
"Pending: already filled" | LR already filled |
"Pending: wrong token" | Token doesn't match |
"Pending: deposit insufficient" | Insufficient funds for fill |