Overview
The new withdrawal system allows users to withdraw funds through a flexible multi-provider architecture. Users can withdraw funds across multiple chains with different timing options--from deallocation-based cooldowns to instant express withdrawals. The withdrawal cooldown is tied to the user's last deallocation time, not the withdrawal initiation--if funds have been idle for the full cooldown period (typically 12 hours), withdrawals can be finalized immediately.
Legacy System Limitations
- Fragmented Withdrawal Paths: Withdrawals were split between the Bridge facet (instant withdrawals with no cooldown) and the standard withdrawal function (12-hour cooldown)
- Cooldown Reset Behavior: Each deallocation reset the 12-hour cooldown timer, creating friction for PartyBs and automated trading systems
- Single Queue Limitation: Only one withdrawal could be queued at a time
- Incompatibility with New Features: Virtual funds and cross-chain withdrawals required a fundamental redesign
Architecture
A withdrawal request consists of one or more receiver parts, allowing users to split a single withdrawal across different chains and providers within one atomic request.
Provider Types
| Provider | Purpose |
|---|---|
| Express Provider | Bypasses the 12-hour cooldown by paying the user immediately. Fronts funds and reclaims them after cooldown expires. Typically charges a fee. |
| Virtual Provider | Enables cross-chain withdrawals within the Virtual Fund System. Delivers funds on the user's chosen destination chain. |
Providers must be registered within Symmio. A provider cannot serve both roles simultaneously.
Withdrawal Types
| Type | Express | Virtual | Description |
|---|---|---|---|
| Normal (Classic) | ✗ | ✗ | Standard 12-hour cooldown, same-chain delivery |
| Pure Express | ✓ | ✗ | Instant same-chain withdrawal |
| Pure Virtual | ✗ | ✓ | Standard cooldown, cross-chain delivery |
| Virtual Express | ✓ | ✓ | Instant cross-chain withdrawal |
Cooldown Mechanics
The withdrawal cooldown is calculated from the user's
last deallocation time (deallocateTimestamp), not from the withdrawal initiation. When
initiateWithdraw is called, the system computes:
cooldownEndTime = max(deallocateTimestamp + withdrawCooldownPeriod, block.timestamp)
This means:
- If the user deallocated recently, they wait the remaining cooldown from that deallocation
- If the cooldown period has already elapsed since the last deallocation, the withdrawal can be finalized immediately after initiation
- The cooldown period is stored in
MAStorage.withdrawCooldownPeriod(typically 12 hours)
The cooldownEndTime is stored in the WithdrawRequest struct and returned by
initiateWithdraw. Subsequent deallocations do not affect the cooldown of already-initiated
withdrawals.
The getWithdrawableTime(address user) view function returns the earliest time a withdrawal initiated now could be
finalized.
Withdrawal Flows
Normal (Classic) Withdraw
The simplest withdrawal type with no providers involved.
sequenceDiagram
participant User
participant Symmio
User->>Symmio: initiateWithdraw()
Note over Symmio: Status: PENDING
Note over Symmio: Wait for cooldown<br/>(from last deallocation)
User->>Symmio: finalizeWithdrawRequest()
Symmio->>User: Transfer funds
Note over Symmio: Status: COMPLETED
Cancellation: User may cancel at any time before finalization without external approval.
Pure Virtual Withdraw
Only a virtual provider is specified for cross-chain delivery.
sequenceDiagram
participant User
participant Symmio
participant Virtual as Virtual Provider
User->>Symmio: initiateWithdraw()
Note over Symmio: Status: PENDING
Symmio->>Virtual: onWithdrawRequest()
Virtual->>Symmio: acceptWithdrawRequest()
Note over Symmio: Status: PROVIDER_ACCEPTED
Note over Symmio: Wait for cooldown<br/>(from last deallocation)
User->>Symmio: finalizeWithdrawRequest()
Symmio->>Virtual: onWithdrawComplete()
Virtual->>User: Transfer funds (cross-chain)
Symmio->>User: Transfer funds (non-provider parts)
Note over Symmio: Status: COMPLETED
Cancellation: - Before provider accepts (PENDING): Cancels immediately - After acceptance,
outside blackout: Cancels immediately - After acceptance, inside blackout: Transitions to CANCEL_REQUESTED,
awaits provider approval
Pure Express Withdraw
Only an express provider is specified for instant same-chain withdrawal.
sequenceDiagram
participant User
participant Symmio
participant Express as Express Provider
User->>Symmio: initiateWithdraw()
Note over Symmio: Status: PENDING
Symmio->>Express: onWithdrawRequest()
Express->>Symmio: acceptWithdrawRequest()
Note over Symmio: Status: PROVIDER_ACCEPTED
Express->>User: Instant payment
Note over Symmio: Wait for cooldown<br/>(from last deallocation)
User->>Symmio: finalizeWithdrawRequest()
Symmio->>Express: onWithdrawComplete()
Symmio->>Express: Reimburse funds
Symmio->>User: Transfer funds (non-provider parts)
Note over Symmio: Status: COMPLETED
Virtual Express Withdraw
Both provider types for instant cross-chain withdrawal.
sequenceDiagram
participant User
participant Symmio
participant Express as Express Provider
participant Virtual as Virtual Provider
User->>Symmio: initiateWithdraw()
Note over Symmio: Status: PENDING
Symmio->>Express: onWithdrawRequest()
Note over Express,Virtual: Off-chain coordination
Express->>Symmio: acceptWithdrawRequest()
Note over Symmio: Status: PROVIDER_ACCEPTED
Express->>User: Instant payment (cross-chain)
Note over Symmio: Wait for cooldown<br/>(from last deallocation)
User->>Symmio: finalizeWithdrawRequest()
Symmio->>Express: onWithdrawComplete()
Virtual->>Express: Reimburse funds
Symmio->>User: Transfer funds (non-provider parts)
Note over Symmio: Status: COMPLETED
Virtual providers coordinate with express providers off-chain for acceptance and payment logistics.
Summary Table
| Type | Acceptance Required | Instant Payment | Finalization Payout |
|---|---|---|---|
| Normal | No | — | Symmio → User |
| Pure Virtual | Virtual provider | — | Virtual → User, Symmio → User |
| Pure Express | Express provider | Express → User | Symmio → Express, Symmio → User |
| Virtual Express | Express provider | Express → User | Virtual → Express, Symmio → User |
Administrative Features
Force Cancel
Administrators with WITHDRAW_FORCE_CANCEL_ROLE can force-cancel any withdrawal request
before the cooldown expires.
- Works on all withdrawal types (classic, virtual, express)
- Valid for
PENDING,PROVIDER_ACCEPTED, orCANCEL_REQUESTEDstatus - Returns locked balance to user's available balance
- Notifies providers via
onForceWithdrawCancelcallback
Pure Virtual Cancel Blackout
The pureVirtualCancelBlackout parameter creates a window before cooldown expiry during which users cannot cancel
pure virtual withdrawals. This gives virtual providers certainty that accepted requests won't be cancelled at the last moment.
Withdrawal Suspension
A global flag can suspend users, preventing them from calling balance-changing functions. Suspended users' withdrawal requests can be cancelled, returning locked balance to their available balance.
Withdrawal Speed-Up
Designated users can reduce their cooldown period:
- Admin adds user to speed-up whitelist
- User initiates withdrawal with
speedUpflag enabled - Admin accepts speed-up via
acceptSpeedUpRequestwith new cooldown duration
A system-wide minWithdrawCooldown enforces the minimum allowed cooldown.
Technical Reference
Enums
enum WithdrawStatus {
PENDING, // 0: Created, awaiting provider acceptance
PROVIDER_ACCEPTED, // 1: Provider approved the request
PROVIDER_REJECTED, // 2: Provider declined; request refunded
COMPLETED, // 3: Finalized successfully
CANCEL_REQUESTED, // 4: User requested cancellation, awaiting provider
CANCELLED, // 5: Request fully cancelled and refunded
SUSPENDED // 6: Suspended due to user-level restriction
}
Structs
struct WithdrawReceiverPart {
uint256 id; // Part ID (set by caller)
uint256 amount; // Amount of collateral to withdraw
int256 chainId; // Destination chain ID
bytes receiver; // Destination address (20 bytes for EVM)
address virtualProvider; // Zero for classic/pure-express
address expressProvider; // Zero for classic/pure-virtual
}
struct WithdrawRequest {
uint256 id;
address user;
WithdrawReceiverPart[] parts;
uint256 timestamp;
uint256 cooldownEndTime;
WithdrawStatus status;
bool speedUp;
bool isCooldownModified;
address provider;
bool isPureVirtual;
bytes providerData;
uint256 totalAmount;
uint256 totalVirtualAmount;
}
Read Functions
function getWithdrawRequests(address user, uint256 requestId) external view returns (WithdrawRequest memory);
function getLastWithdrawRequestId(address user) external view returns (uint256);
function getWithdrawRequestsBatch(address user, uint256 start, uint256 size) external view returns (WithdrawRequest[] memory);
function getPendingWithdrawRequests(address user, uint256 start, uint256 size) external view returns (WithdrawRequest[] memory);
function getWithdrawableTime(address user) external view returns (uint256);
function isExpressProviderRegistered(address provider) external view returns (bool);
function isVirtualProviderRegistered(address provider) external view returns (bool);
function isSpeedUpEligible(address user) external view returns (bool);
function getModifiedCooldownEndTime(address user, uint256 requestId) external view returns (uint256);
Listing Withdraw Requests
To retrieve a user's unfinished withdrawal requests from a frontend:
-
Filtered: Use
getPendingWithdrawRequests(user, start, size)to scan a range of request IDs and return only those with non-terminal status (PENDING,PROVIDER_ACCEPTED,CANCEL_REQUESTED,SUSPENDED). Requests that areCOMPLETED,CANCELLED, orPROVIDER_REJECTEDare excluded. -
Unfiltered: Use
getWithdrawRequestsBatch(user, start, size)to get all requests in a range regardless of status, and filter client-side.
Both approaches are paginated. Call getLastWithdrawRequestId(user) first to get the ID range, then page through
with start/size. Request IDs are sequential starting from 1.
User Functions
// Initiate a withdrawal request
// Returns the request ID and the cooldown end timestamp
function initiateWithdraw(
WithdrawReceiverPart[] calldata parts,
bool speedUp,
bytes calldata providerData
) external returns (uint256 requestId, uint256 cooldownEndTime);
// Finalize after cooldown expires
function finalizeWithdrawRequest(address user, uint256 requestId) external;
// Request cancellation
// - Classic/pure-virtual (outside blackout): cancels immediately
// - Express or pure-virtual (inside blackout): transitions to CANCEL_REQUESTED
function requestCancelWithdraw(uint256 requestId) external;
Admin Functions
// Force-cancel before cooldown expires (requires WITHDRAW_FORCE_CANCEL_ROLE)
function forceCancelWithdraw(address user, uint256 requestId) external;
// Set blackout period for pure virtual cancellations (requires COOLDOWN_ADMIN_ROLE)
function setPureVirtualCancelBlackout(uint256 blackout) external;
Provider Callbacks
Providers must implement these callbacks:
IVirtualProvider
function onWithdrawRequest(WithdrawRequest calldata request) external;
function onWithdrawComplete(WithdrawRequest calldata request) external;
function onWithdrawCancelRequest(WithdrawRequest calldata request) external;
function onForceWithdrawCancel(WithdrawRequest calldata request) external;
IExpressProvider
function onWithdrawRequest(WithdrawRequest calldata request) external;
function onWithdrawComplete(WithdrawRequest calldata request) external;
function onWithdrawCancelRequest(WithdrawRequest calldata request) external;
function onForceWithdrawCancel(WithdrawRequest calldata request) external;