> For the complete documentation index, see [llms.txt](https://docs.zama.org/protocol/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zama.org/protocol/confidential-vault/guides/withdraw.md).

# Withdraw from a Vault

Integrate both withdrawal paths — the confidential batched redemption and the instant public unshield.

There are two ways out of a confidential vault position, with opposite trade-offs:

|                 | Batched redemption                         | Instant withdrawal                  |
| --------------- | ------------------------------------------ | ----------------------------------- |
| Confidentiality | Amount stays hidden in the batch aggregate | Amount becomes public               |
| Speed           | Once a day on mainnet, about 11:00 UTC     | Minutes                             |
| Path            | cShare → redeem batcher → cUSDC            | cShare → public vault shares → USDC |

This guide covers both. It assumes the setup from [Deposit into a Vault](/protocol/confidential-vault/guides/deposit.md) — the SDK, the ABIs, and the addresses.

## Path A: batched redemption (confidential)

The redeem batcher mirrors the deposit batcher with the tokens reversed: users join with encrypted cShare, the batch redeems the aggregate from the vault, and participants claim confidential cUSDC.

{% hint style="warning" %}
**Mainnet redemptions settle once a day** Redeem batches are dispatched at about 11:00 UTC every day and settle minutes later. A join sent after that time waits for the next day's dispatch. Show the batch's readiness time, and offer `quit(batchId)` as a cancel action while the batch is still pending.
{% endhint %}

Deposit batches dispatch four hours later, at about 15:00 UTC. To move a user from one vault to another, claim the cUSDC from the redeem batch and join the target deposit batch before 15:00 UTC. The user is out of yield for four hours instead of a day.

### Size the withdrawal

The user's cShare balance is encrypted; read and decrypt it first — see [Read Confidential Balances](/protocol/confidential-vault/guides/read-balances.md). To let the user think in dollars ("withdraw $500"), convert through the vault's public pricing:

```ts
import { parseAbi } from "viem";

const VAULT_ABI = parseAbi([
  "function convertToShares(uint256 assets) view returns (uint256)",
  "function previewRedeem(uint256 shares) view returns (uint256)",
]);

const shares = await publicClient.readContract({
  address: VAULT,
  abi: VAULT_ABI,
  functionName: "convertToShares",
  args: [usdcAmount], // 6-decimal USDC units
});
```

Present conversions as estimates: the batch settles at the vault's share price at finalization time, not at quote time.

### Join, then claim

Encrypt the share amount bound to the **cShare token** and the user, then transfer-and-call to the redeem batcher — the same shape as a deposit join. No approval, no operator grant:

```ts
const encrypted = await sdk.encrypt({
  values: [{ type: "euint64", value: shareAmount }],
  contractAddress: CSHARE,
  userAddress: user,
});

const joinHash = await walletClient.writeContract({
  address: CSHARE,
  abi: WRAPPER_ABI,
  functionName: "confidentialTransferAndCall",
  args: [REDEEM_BATCHER, encrypted.encryptedValues[0]!, encrypted.inputProof, "0x"],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash: joinHash });
const batchId = joinedBatchId(receipt); // decode from the Joined event, as in the deposit guide
```

Track the batch as in [Track Batch State](/protocol/confidential-vault/guides/track-batches.md). Once `batchState(batchId)` returns `Finalized`, claim the cUSDC:

```ts
await walletClient.writeContract({
  address: REDEEM_BATCHER,
  abi: BATCHER_ABI,
  functionName: "claim",
  args: [batchId, user],
});
```

There is no slippage bound on redemptions — by design, so users can always exit even when the vault's share price has dropped. If the batch cancels instead, `quit(batchId)` returns the exact cShare amount.

To finish in public USDC, unshield the claimed cUSDC with the unwrap sequence below, applied to the cUSDC wrapper.

### Exit several vaults in one transaction

Each vault has its own cShare token, so a multi-vault exit uses the [router](/protocol/confidential-vault/concepts/multi-vault-router.md)'s pull path. Grant the router operator rights on each cShare, then call `join` with one leg per vault. Encrypt the leg amounts against the router:

```ts
const ROUTER_ABI = parseAbi([
  "struct Allocation { address batcher; address token; bytes32 amount; }",
  "function join(Allocation[] legs, bytes inputProof)",
]);
const OPERATOR_ABI = parseAbi([
  "function setOperator(address operator, uint48 until)",
  "function isOperator(address holder, address spender) view returns (bool)",
]);

const legs = [
  { batcher: REDEEM_BATCHER_A, token: CSHARE_A, amount: sharesA },
  { batcher: REDEEM_BATCHER_B, token: CSHARE_B, amount: sharesB },
];

// One-time per share token. The grant is safe to leave standing: the router can spend a
// balance only inside a transaction its holder sent.
for (const leg of legs) {
  const granted = await publicClient.readContract({
    address: leg.token,
    abi: OPERATOR_ABI,
    functionName: "isOperator",
    args: [user, ROUTER],
  });
  if (!granted) {
    await walletClient.writeContract({
      address: leg.token,
      abi: OPERATOR_ABI,
      functionName: "setOperator",
      args: [ROUTER, 2n ** 48n - 1n],
    });
  }
}

const allocation = await sdk.encrypt({
  values: legs.map((leg) => ({ type: "euint64", value: leg.amount })),
  contractAddress: ROUTER,
  userAddress: user,
});

await walletClient.writeContract({
  address: ROUTER,
  abi: ROUTER_ABI,
  functionName: "join",
  args: [
    legs.map((leg, i) => ({ ...leg, amount: allocation.encryptedValues[i]! })),
    allocation.inputProof,
  ],
});
```

Each leg credits the user in its redeem batcher, and each batcher is claimed separately once finalized. A pull fits about 10 legs. Exit through the same leg list the user entered with, decoys included, or the exit shows which vaults the deposit could not have gone to. A wallet that supports EIP-5792 can put the grants and the `join` in one confirmation.

## Path B: instant withdrawal (public)

The instant path skips the batcher entirely: unwrap cShare into **public** vault shares, then redeem them on the ERC-4626 vault like any other holder. It settles in minutes and reveals the amount — the unwrap publishes the burnt quantity on-chain.

The unwrap is asynchronous, in two transactions with a public decryption between them.

{% hint style="info" %}
**Exclusive vaults** Some vaults accept deposits only from their deposit batcher. Their public shares can still be redeemed on the vault, so this path works for every vault. The shares cannot be deposited again publicly. Gated vaults list a deposit gate in [Contract Addresses](/protocol/confidential-vault/reference/addresses.md).
{% endhint %}

### Step 1 — unwrap: burn confidential shares

Encrypt the amount bound to the cShare wrapper, then call `unwrap`. Unwrapping your own balance needs no operator grant:

```ts
const UNWRAP_ABI = parseAbi([
  "function unwrap(address from, address to, bytes32 amount, bytes inputProof) returns (bytes32)",
  "function finalizeUnwrap(bytes32 unwrapRequestId, uint64 cleartext, bytes decryptionProof)",
  "event UnwrapRequested(address indexed receiver, bytes32 indexed unwrapRequestId, bytes32 amount)",
]);

const encrypted = await sdk.encrypt({
  values: [{ type: "euint64", value: shareAmount }],
  contractAddress: CSHARE,
  userAddress: user,
});

const unwrapHash = await walletClient.writeContract({
  address: CSHARE,
  abi: UNWRAP_ABI,
  functionName: "unwrap",
  args: [user, user, encrypted.encryptedValues[0]!, encrypted.inputProof],
});
```

### Step 2 — decrypt the burnt amount publicly

Wait for the receipt and decode `unwrapRequestId` from the `UnwrapRequested` event. Then fetch the cleartext and its KMS proof. This is a **public** decryption — the unwrap marked the handle publicly decryptable so anyone can finalize — so it needs no wallet signature:

```ts
const receipt = await publicClient.waitForTransactionReceipt({ hash: unwrapHash });
const unwrapRequestId = parseUnwrapRequestedId(receipt.logs);

const { clearValues, decryptionProof } = await sdk.decryption.decryptPublicValues([
  unwrapRequestId,
]);
const cleartext = clearValues[unwrapRequestId];
if (typeof cleartext !== "bigint") throw new Error("public decrypt returned no cleartext yet");
```

### Step 3 — finalize, then redeem on the vault

```ts
await walletClient.writeContract({
  address: CSHARE,
  abi: UNWRAP_ABI,
  functionName: "finalizeUnwrap",
  args: [unwrapRequestId, cleartext, decryptionProof],
});
```

`finalizeUnwrap` mints public vault shares to the user. Redeem them for USDC — but **never assume the unwrapped amount equals the requested amount**. Snapshot the public share balance before the unwrap and diff it after finalization, and refuse to submit a zero redeem:

```ts
const clearShares = sharesAfter - sharesBefore; // balanceOf(user) around the sequence
if (clearShares === 0n) throw new Error("no unwrapped shares to redeem");

await walletClient.writeContract({
  address: VAULT,
  abi: parseAbi([
    "function redeem(uint256 shares, address receiver, address owner) returns (uint256)",
  ]),
  functionName: "redeem",
  args: [clearShares, user, user],
});
```

### Make the sequence resumable

Between the unwrap and the finalize, the user's funds are burnt but not yet minted. Treat "pending unshield" as a first-class state:

* Persist the unwrap transaction hash the moment it is sent.
* On reload, resume from the hash: wait for the receipt, re-decode the request id, re-run `decryptPublicValues`, and finalize.
* The sequence depends on the mined receipt, so it cannot be bundled into one wallet confirmation — design the UX around two.

## Choosing a default

Default to the batched path, since it keeps the amount confidential. Offer the instant path as an explicit "withdraw now, publicly" choice, with the consequence stated at the point of action. The same two-step unwrap also serves users who want their claimed cUSDC back as plain USDC.

## Next steps

* [Track Batch State](/protocol/confidential-vault/guides/track-batches.md) — batch progress, readiness, and events.
* [Confidentiality](/protocol/confidential-vault/concepts/confidentiality.md) — exactly what the instant path reveals.
* [Batch Lifecycle](/protocol/confidential-vault/concepts/batch-lifecycle.md) — why redemptions have no slippage bound, and the daily cadence.
* [Router Interface](/protocol/confidential-vault/reference/router-interface.md) — the pull entry point and its errors.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.zama.org/protocol/confidential-vault/guides/withdraw.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
