Skip to main content

Withdrawal Flow

Emerald Vault replaces instant ERC4626 withdrawals with an instant-or-queued system. A withdrawal settles immediately when the vault holds enough liquid assets and there is no queue backlog; otherwise it joins a FIFO queue and is paid out later — either by an admin batch release or by the user claiming once their request reaches the head of the queue.

info

Standard ERC4626 withdraw() and redeem() always revert. All exits go through requestWithdrawal → (instant) or (queue → release/claim).

Lifecycle

liquid & no backlog
requestWithdrawal() ──────────────────────────────────► Paid instantly
│ (WithdrawalInstant)
│ otherwise: shares locked, queued (FIFO)

Queued ──► releaseWithdrawals() (admin, FIFO) ──► Paid
│ └─► claimWithdrawal() (you, at queue head) ──► Paid

└─► cancelWithdrawal() (returns shares; blocked once NAV updates)

There is no time delay and no expiry. A queued request stays in the queue until it is released, claimed, or cancelled. The asset amount is recomputed at payout time (convertToAssets(shares)), so a queued user bears NAV movement while waiting.

Step 1: Request a withdrawal

const shares = parseUnits("500", await vault.read.decimals()); // shares follow the asset's decimals

const hash = await walletClient.writeContract({
address: VAULT_ADDRESS,
abi: vaultAbi,
functionName: "requestWithdrawal",
args: [shares],
});
await publicClient.waitForTransactionReceipt({ hash });

Partners: to attribute the withdrawal to your partnerId, use the overload requestWithdrawal(shares, partnerId) — it emits a ReferralWithdrawal event the indexer attributes to you.

If the vault is liquid and the queue is empty, the call pays out immediately and returns type(uint256).max (emitting WithdrawalInstant). Otherwise it locks your shares, returns a requestIndex, and emits WithdrawalRequested. Retrieve your indices any time:

const indices = await publicClient.readContract({
address: VAULT_ADDRESS, abi: vaultAbi, functionName: "getUserRequests", args: [userAddress],
});

Step 2: Get paid (queued requests)

A queued request is settled either way below — you do not need to wait a fixed period; you need the vault to be liquid and your request to reach the front of the FIFO queue.

  • Admin releasereleaseWithdrawals() processes the queue in strict FIFO order, paying each request while liquidity lasts.
  • Self-claimclaimWithdrawal() lets you settle your own request when it is at the head of the queue and the vault is liquid. It takes no argument.
// Claim your own request once it is at the FIFO head and the vault is liquid.
const hash = await walletClient.writeContract({
address: VAULT_ADDRESS,
abi: vaultAbi,
functionName: "claimWithdrawal",
args: [], // no requestIndex — settles the queue head if it's yours
});
await publicClient.waitForTransactionReceipt({ hash });

claimWithdrawal() reverts with NotAtQueueHead if your request isn't at the front yet, or InsufficientLiquidity if the vault can't currently cover it — retry later or cancel.

Cancel a request

Cancel a queued request to get your shares back:

const hash = await walletClient.writeContract({
address: VAULT_ADDRESS, abi: vaultAbi, functionName: "cancelWithdrawal", args: [BigInt(requestIndex)],
});
caution

Cancellation is blocked once a NAV update has landed after your request (NAVUpdatedSinceRequest) — not after a fixed time. This closes a free-option MEV vector. After a NAV update you must exit through the normal payout path at the new share price.

Reading a request

const [user, shares, assetsOwed, timestamp, claimed, cancelled] =
await publicClient.readContract({
address: VAULT_ADDRESS, abi: vaultAbi, functionName: "getWithdrawalRequest", args: [BigInt(requestIndex)],
});

assetsOwed is computed live from the current share price (it is not stored) and returns 0 once the request is claimed or cancelled.

Events

EventWhen
WithdrawalInstant(user, shares, assetsOwed)Request settled immediately (no queue)
WithdrawalRequested(user, requestIndex, shares, assetsOwed)Request entered the FIFO queue
WithdrawalReleased(user, requestIndex, shares, assetsOwed)Queued request paid (admin release or self-claim)
WithdrawalCancelled(user, requestIndex, shares)Request cancelled
ReferralWithdrawal(user, requestIndex, shares, assetsEstimate, partnerId)Attributed to a partner

Error Cases

ErrorCause
ZeroAmountshares is 0, or computed assets round to 0
InsufficientSharesYou don't hold enough shares
InvalidRequestRequest index doesn't exist
NotRequestOwnerCaller isn't the request owner (cancel)
NotAtQueueHeadclaimWithdrawal() but your request isn't at the FIFO head
InsufficientLiquidityVault can't currently cover the payout
NAVUpdatedSinceRequestCancel attempted after a NAV update landed
WithdrawalAlreadyClaimed / WithdrawalAlreadyCancelledRequest already resolved
EnforcedPauseVault is paused (request only)

Using the REST API

Track withdrawal status off-chain:

# All withdrawals for a user
curl https://api.emeraldvaults.io/api/v1/users/{address}/withdrawals

# Filter by status: queued | claimed | cancelled
curl "https://api.emeraldvaults.io/api/v1/users/{address}/withdrawals?status=queued"

# Global withdrawal queue
curl https://api.emeraldvaults.io/api/v1/queues/withdrawals