Withdraw
You hand back ksUSD and get USDC. There are two normal ways to do that, plus a third that only applies if the vault is ever shut down.
Instant (withdraw_instant)
Vault has enough idle USDC
One transaction
Queued (request_withdrawal + process_withdrawal)
Withdrawal larger than the liquidity buffer
Burned immediately at locked price; USDC paid out (FIFO, strict) after the keeper unwinds enough position
Wind-down (claim_wind_down)
Active only when position_mode == WindDown
Pro-rata redemption against the vault's idle USDC; no queue, no FIFO
Instant is the path almost everyone uses. The vault keeps a liquidity buffer of idle USDC on hand, 10% of what it holds by default, which is enough to cover about 99% of real withdrawals on the spot.
Instant withdrawal
withdraw_instant(shares: u64)Burns
sharesofksUSDPays out USDC from the vault's idle balance at the current share price
vault
mut
Vault PDA
vault_usdc_account
mut
Vault USDC ATA — source of payout
ksusd_mint
mut
ksUSD share mint
user_ksusd_account
mut
User's ksUSD ATA (must hold ≥ shares)
user_usdc_account
mut
User's USDC destination
user
signer
Withdrawer
phoenix_trader
—
Phoenix trader account — read to true up the venue's contribution to NAV before pricing. Pinned to vault.phoenix_trader
sol_oracle
—
Pyth SOL/USD — values the perp leg for that same correction. Pinned to vault.sol_oracle
token_program
—
SPL token program
Math — the deposit division, run backwards:
Burn 97.09 ksUSD while a share is worth $1.05 and you get $101.94. There is no withdrawal fee and no exit penalty, so what comes back is exactly your slice of NAV at the moment the transaction lands. Like the mint, the division truncates in the vault's favour by under one base unit.
If vault_usdc_account.amount < usdc_owed, this reverts with InsufficientLiquidityBuffer. Use the queued path instead.
Queued withdrawal
This is what happens when the vault doesn't have enough idle USDC to pay you on the spot.
Your shares burn right away at the current share price, so the dollar amount you're owed is locked in at that moment and can't drift while you wait. The request is recorded in its own account. Once the keeper has unwound enough of the position to refill the buffer, anyone can trigger the payout, including you.
Step 1 — request
Burns shares
Creates a request PDA at
[b"withdrawal_request", vault, queue_next_id]Locks
usdc_owed = shares × share_price_1e9 / 1e9on the request record — the same math as the instant path, priced at the moment you queue
vault
mut
Vault PDA
withdrawal_request
init
New per-request PDA (payer = user)
ksusd_mint
mut
ksUSD share mint
user_ksusd_account
mut
User's ksUSD ATA (burned from)
user_usdc_account
—
User's USDC destination (recorded on the request)
user
signer, mut
Requester (pays rent)
token_program
—
SPL token program
system_program
—
Required — the request PDA is created by this instruction
Step 2 — process (permissionless crank)
In practice you do nothing here — the keeper calls this for you. It checks the queue every tick (~5 minutes) and pays out whatever the vault can cover, oldest first, up to 8 requests per tick. Refilling the buffer is part of the same loop: the keeper folds what the queue owes into its idle-USDC target and pulls the shortfall back out of Kamino, and only if that is still not enough does it de-lever the position to raise the rest. If the head of the queue has been waiting more than an hour, the keeper pages an operator.
"Permissionless" is the guarantee behind that, not the normal path: the payout does not depend on the keeper being alive. If it stops, anyone can crank the queue — you, another depositor, a bot — and the money still moves. Nobody can be paid ahead of you, because the ordering is enforced on-chain rather than by whoever sends the transaction.
The mechanics:
Anyone may call it, including the person who queued the request.
Strictly first-in, first-out: the next request processed must have
request_id == vault.queue_processed_through. Anything else reverts withWithdrawalNotNextInQueue, so a cranker cannot skip the head of the queue to reach their own request.The payout is capped at the live share price. If the vault's value has fallen since you queued, you get your fair share of it and no more.
The request account closes on payout, refunding its rent to whoever opened it.
It is blocked while the vault is paused. Pending requests wait — the shares are already burned and the amount owed is locked — until an admin unpauses.
vault
mut
Vault PDA
withdrawal_request
mut
The queued request (will be closed)
vault_usdc_account
mut
Vault USDC ATA — source of payout
user_usdc_account
mut
Recorded destination from the request
recipient_for_rent
mut
Original requester (rent refund destination)
cranker
signer
Anyone can crank
token_program
—
—
TypeScript — instant path
TypeScript — queued path
Wind-down path
This only applies if the vault is being shut down for good. It is not a normal operating mode.
Once the admin calls init_wind_down, the vault stops taking deposits and stops opening positions. Everyone then redeems with:
Pays out your share of the vault's idle USDC at the live
effective_navshare price.No queue, no ordering, and no rent refund.
Reverts unless
position_mode == WindDown.
Events
WithdrawEvent— emitted bywithdraw_instantWithdrawalRequested— emitted byrequest_withdrawalWithdrawalProcessed— emitted byprocess_withdrawalWindDownClaimed— emitted byclaim_wind_down
Related
Fees — no withdrawal fees on either path
Last updated