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.
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 release —
releaseWithdrawals()processes the queue in strict FIFO order, paying each request while liquidity lasts. - Self-claim —
claimWithdrawal()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)],
});
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
| Event | When |
|---|---|
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
| Error | Cause |
|---|---|
ZeroAmount | shares is 0, or computed assets round to 0 |
InsufficientShares | You don't hold enough shares |
InvalidRequest | Request index doesn't exist |
NotRequestOwner | Caller isn't the request owner (cancel) |
NotAtQueueHead | claimWithdrawal() but your request isn't at the FIFO head |
InsufficientLiquidity | Vault can't currently cover the payout |
NAVUpdatedSinceRequest | Cancel attempted after a NAV update landed |
WithdrawalAlreadyClaimed / WithdrawalAlreadyCancelled | Request already resolved |
EnforcedPause | Vault 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