Withdraw (Async Redeem)
Overview
King's Vault V2 uses the ERC-7540 asynchronous redeem pattern. Direct withdraw() by asset amount is disabled (always reverts with OnlyRedeemSupported()). All exit flows use the three-phase lifecycle:
- Request — user locks shares via
requestRedeem(). - Execute — Keeper/Admin processes the request off-chain (cross-chain retrieval + CCIP).
- Claim — user calls
redeem()to receive USDC.
withdraw() is Disabled
withdraw(uint256, address, address) always reverts. Frontend must never expose a withdraw-by-assets flow.
State Lifecycle
stateDiagram-v2
[*] --> Pending : requestRedeem()
Pending --> Executed : Keeper executes request<br/>(sets executionPrice, isExecuted=true)
Executed --> Claimed : redeem() by user
Claimed --> [*]
note right of Pending
shares are locked
(deducted from maxRedeem)
executionPrice = 0
isExecuted = false
end note
note right of Executed
shares remain locked
executionPrice > 0
isExecuted = true
user can now claim
end note
Phase 1: Request Redeem
sequenceDiagram
actor User as Investor
participant UI as Frontend
participant Vault as KingsVault
User->>UI: Enter shares to redeem
UI->>Vault: maxRedeem(owner)
Vault-->>UI: available shares (excluding existing requests)
alt shares <= maxRedeem
UI->>Vault: requestRedeem(shares, owner)
Note over Vault: Create RedeemRequest:<br/>shares, exceptedPrice=markedPrice,<br/>executionPrice=0, isExecuted=false
Vault-->>UI: requestId
UI-->>User: "Request submitted, requestId: N"
else shares > maxRedeem
UI-->>User: Error: exceeds redeemable shares
end
| Step | Actor | Action | Detail |
|---|---|---|---|
| 1 | User | Check available shares. | maxRedeem(owner) = balanceOf(owner) - sum(all request shares) |
| 2 | User | Submit redeem request. | requestRedeem(shares, owner) — records exceptedPrice at current marked price. |
| 3 | Vault | Store request. | Creates RedeemRequest struct with executionPrice = 0, isExecuted = false. |
| 4 | Vault | Emit event. | WithdrawQueued(owner, shares) |
Phase 2: Execution (Off-Chain)
The Keeper/Admin backend handles execution. This phase is not triggered by on-chain user transactions.
| Step | Actor | Action |
|---|---|---|
| 1 | Keeper | Detects pending redeem requests. |
| 2 | Keeper | Triggers HyperEVM withdrawal (Action 2: HLP → Core Spot). |
| 3 | Keeper | Bridges Core Spot → HyperEVM USDC (Action 6). |
| 4 | Keeper | Sends USDC back to Ethereum via CCIP. |
| 5 | Keeper | Marks request as executed on-chain (isExecuted = true, sets executionPrice). |
Estimated latency: 30-60 minutes (cross-chain settlement + CCIP finality).
Phase 3: Claim
sequenceDiagram
actor User as Investor
participant UI as Frontend
participant Vault as KingsVault
participant Token as USDC (ERC-20)
User->>UI: Click "Claim"
UI->>Vault: claimableRedeemRequest(owner)
Vault-->>UI: claimableShares
UI->>Vault: previewClaimableRedeem(claimableShares, owner)
Vault-->>UI: estimatedAssets
UI-->>User: Preview: "You will receive ~X USDC"
User->>UI: Confirm claim
UI->>Vault: redeem(shares, receiver, owner)
Note over Vault: FIFO: iterate executed requests,<br/>assets += claimed * executionPrice
Vault->>Token: transfer(receiver, assets)
Vault->>Vault: _burn(owner, shares)
Vault-->>UI: assets returned
UI-->>User: "Claimed X USDC successfully"
| Step | Actor | Action | Detail |
|---|---|---|---|
| 1 | User | Check claimable. | claimableRedeemRequest(owner) — total shares in executed requests. |
| 2 | User | Preview assets. | previewClaimableRedeem(shares, owner) — FIFO calculation of expected USDC. |
| 3 | User | Execute claim. | redeem(shares, receiver, owner) — transfers USDC and burns shares. |
| 4 | Vault | Source liquidity. | If idle cash is insufficient, triggers _replenish(shortage) from strategy pools. |
Key View Functions
| Function | Returns | Description |
|---|---|---|
maxRedeem(owner) |
uint256 |
Shares available for new requests = balanceOf - sum(all request shares) |
pendingRedeemRequest(owner) |
uint256 |
Total shares in pending (not yet executed) requests |
claimableRedeemRequest(owner) |
uint256 |
Total shares in executed (ready-to-claim) requests |
previewClaimableRedeem(shares, owner) |
uint256 |
Estimated USDC output for given claimable shares (FIFO) |
FIFO Claim Order
previewClaimableRedeem and redeem consume executed requests in First-In-First-Out order. Each request may have a different executionPrice depending on the epoch in which it was executed. Partial claims across multiple requests are supported.
Events
| Event | Parameters |
|---|---|
WithdrawQueued |
owner (indexed), shares |
Withdraw |
sender (indexed), receiver (indexed), owner (indexed), assets, shares |
Errors
| Error | Trigger |
|---|---|
OnlyRedeemSupported() |
Caller invoked withdraw() (disabled). |
InsufficientQueuedShares(requested, available) |
Redeem amount exceeds claimable shares. |
NoSharesToRedeem() |
Emergency redeem called with zero balance. |
Security Considerations
- No Share Escrow: Shares are not locked/escrowed upon
requestRedeem. They remain in the owner's balance.maxRedeemprevents double-commitment by subtracting request shares, but shares could theoretically be transferred to another address. Users should not transfer shares while requests are pending. - Execution Price Divergence: The
exceptedPrice(recorded at request time) may differ from theexecutionPrice(set at execution time). Users bear the risk of PPS change between request and execution. - Irrevocable Requests: Once a CCIP cross-chain process is triggered, the withdrawal cannot be cancelled.
- Emergency Redeem:
emergencyRedeem()is available only whenisShutdown == trueand allowsROLE_INVESTORholders to exit at a pro-rata share of remaining assets.