> 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/deposit.md).

# Deposit into a Vault

Integrate the confidential deposit flow — shield USDC, join a deposit batch, and claim confidential shares.

This guide integrates the deposit flow into an existing app: the user starts with public USDC and ends with confidential vault shares (cShare). It assumes you know viem and wallets; the FHE-specific steps are explained as they appear. For the mental model behind the flow, read [Batch Lifecycle](/protocol/confidential-vault/concepts/batch-lifecycle.md) first.

The flow has three user-visible phases:

1. **Shield** — wrap public USDC into confidential cUSDC. One `approve` plus one `wrap`.
2. **Join** — send an encrypted amount of cUSDC to the deposit batcher.
3. **Claim** — after the batch settles (once a day on mainnet), claim the confidential shares.

{% hint style="warning" %}
**Settlement is not instant** Mainnet deposit batches are dispatched once a day, at about 15:00 UTC, and settle minutes later. A join sent after that time waits for the next day's dispatch. Render a pending state with the batch's readiness time. See [Track Batch State](/protocol/confidential-vault/guides/track-batches.md).
{% endhint %}

## Setup

Install the Zama SDK and viem:

{% tabs %}
{% tab title="pnpm" %}

```bash
pnpm add @zama-fhe/sdk viem
```

{% endtab %}

{% tab title="npm" %}

```bash
npm install @zama-fhe/sdk viem
```

{% endtab %}

{% tab title="bun" %}

```bash
bun add @zama-fhe/sdk viem
```

{% endtab %}
{% endtabs %}

Create the SDK once. The chain presets (`sepolia`, `mainnet` from `@zama-fhe/sdk/chains`) carry every FHE system-contract address — the ACL, the KMS, the input verifier, the relayer URL — so you configure only your RPC and clients:

{% tabs %}
{% tab title="sdk.ts (browser)" %}

```ts
import { ZamaSDK, indexedDBStorage } from "@zama-fhe/sdk";
import { sepolia as zamaSepolia } from "@zama-fhe/sdk/chains";
import { createConfig } from "@zama-fhe/sdk/viem";
import { web } from "@zama-fhe/sdk/web";
import { publicClient, walletClient } from "./clients";

const chain = { ...zamaSepolia, network: "https://ethereum-sepolia-rpc.publicnode.com" };

export const sdk = new ZamaSDK(
  createConfig({
    chains: [chain],
    relayers: { [chain.id]: web() },
    publicClient,
    walletClient,
    // Persist the decryption permit + transport keypair across reloads.
    storage: indexedDBStorage,
  }),
);
```

{% endtab %}

{% tab title="sdk.ts (Node / Bun)" %}

```ts
import { ZamaSDK, memoryStorage } from "@zama-fhe/sdk";
import { sepolia as zamaSepolia } from "@zama-fhe/sdk/chains";
import { createConfig } from "@zama-fhe/sdk/viem";
import { node } from "@zama-fhe/sdk/node";
import { publicClient, walletClient } from "./clients";

const chain = { ...zamaSepolia, network: process.env.RPC_URL! };

export const sdk = new ZamaSDK(
  createConfig({
    chains: [chain],
    relayers: { [chain.id]: node() },
    publicClient,
    walletClient,
    storage: memoryStorage,
  }),
);
```

{% endtab %}
{% endtabs %}

Set `network` to a fixed RPC URL for the target chain. A wallet on the wrong chain makes the SDK's system-contract reads fail with an opaque `CALL_EXCEPTION`.

React apps can use `@zama-fhe/react-sdk` instead: `createConfig` from `@zama-fhe/react-sdk/wagmi` plugs into an existing wagmi config and exposes hooks (`useEncrypt`, `useDecryptValues`, `useHasPermit`, `useGrantPermit`) over the same SDK.

{% hint style="info" %}
**Mainnet relayer key** Sepolia's relayer is open. Mainnet's requires an API key. To keep that key out of frontend code and requests, proxy the relayer through your backend (`relayerUrl` pointing at your server route) and inject the `x-api-key` header server-side.
{% endhint %}

Declare the ABI fragments the flow needs. Encrypted amounts (`euint64`, `externalEuint64`) are `bytes32` on the wire — handles pointing at ciphertexts held by the Zama Protocol:

{% code title="abis.ts" %}

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

export const WRAPPER_ABI = parseAbi([
  "function wrap(address to, uint256 amount) returns (bytes32)",
  "function rate() view returns (uint256)",
  "function confidentialBalanceOf(address account) view returns (bytes32)",
  "function confidentialTransferAndCall(address to, bytes32 amount, bytes inputProof, bytes data) returns (bytes32)",
]);

export const BATCHER_ABI = parseAbi([
  "function currentBatchId() view returns (uint256)",
  "function batchState(uint256 batchId) view returns (uint8)",
  "function deposits(uint256 batchId, address account) view returns (bytes32)",
  "function claim(uint256 batchId, address account) returns (bytes32)",
  "function quit(uint256 batchId) returns (bytes32)",
  "event Joined(uint256 indexed batchId, address indexed account, bytes32 amount)",
  "event Claimed(uint256 indexed batchId, address indexed account, bytes32 amount)",
]);
```

{% endcode %}

Contract addresses per network are listed in [Contract Addresses](/protocol/confidential-vault/reference/addresses.md).

## Shield: wrap USDC into cUSDC

The wrapper pulls USDC with `transferFrom`, so approve it first, then call `wrap`. `wrap` takes a **cleartext** amount and mints the encrypted balance as a confidential token — client-side encryption only comes in at the join step. Amounts are in underlying units (USDC has 6 decimals and the wrapper's `rate()` is 1, so confidential units equal USDC units).

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

const approveHash = await walletClient.writeContract({
  address: USDC,
  abi: erc20Abi,
  functionName: "approve",
  args: [CUSDC, amount],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });

const wrapHash = await walletClient.writeContract({
  address: CUSDC,
  abi: WRAPPER_ABI,
  functionName: "wrap",
  args: [user, amount],
});
await publicClient.waitForTransactionReceipt({ hash: wrapHash });
```

{% hint style="info" %}
**Shielding is public** The wrap amount is visible on-chain. A user who wraps and immediately joins a batch has effectively published an upper bound on their deposit size. Users who want the amount to stay confidential can wrap ahead of time or hold a standing cUSDC balance. See [Confidentiality](/protocol/confidential-vault/concepts/confidentiality.md).
{% endhint %}

## Join: send an encrypted amount to the batcher

Joining is one call on the cUSDC token: `confidentialTransferAndCall` moves an encrypted amount to the batcher, and the batcher's receive hook records the position. The batcher itself has no join function.

First, encrypt the amount. The ciphertext is bound to `(contractAddress, userAddress)`: it can only be consumed by `confidentialTransferAndCall` on the **cUSDC contract**, called by **this user**. Encryption hits the relayer but needs no funds and no wallet signature:

```ts
const encrypted = await sdk.encrypt({
  values: [{ type: "euint64", value: amount }],
  contractAddress: CUSDC,
  userAddress: user,
});
const handle = encrypted.encryptedValues[0]!;
```

Then send the join. The trailing `0x` is optional callback data — empty for a plain deposit:

```ts
const joinHash = await walletClient.writeContract({
  address: CUSDC,
  abi: WRAPPER_ABI,
  functionName: "confidentialTransferAndCall",
  args: [DEPOSIT_BATCHER, handle, encrypted.inputProof, "0x"],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash: joinHash });
```

No `setOperator` or allowance is needed: the user signs the transfer themselves, so the token uses `msg.sender` directly.

Read the batch id from the `Joined` event in the receipt — do not read `currentBatchId()` before or after the transaction, because a dispatch can advance the counter between your calls:

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

function joinedBatchId(receipt: TransactionReceipt): bigint {
  for (const log of receipt.logs) {
    if (log.address.toLowerCase() !== DEPOSIT_BATCHER.toLowerCase()) continue;
    try {
      const decoded = decodeEventLog({ abi: BATCHER_ABI, data: log.data, topics: log.topics });
      if (decoded.eventName === "Joined") return decoded.args.batchId;
    } catch {
      // Unrelated log on the batcher address — skip.
    }
  }
  throw new Error("join transaction emitted no Joined event");
}
```

{% hint style="warning" %}
**A successful transaction does not prove a non-zero join** Two edge cases clamp a join to zero instead of reverting: a transfer larger than the user's cUSDC balance, and a join that would overflow the batch's `uint64` total. Verify the position by decrypting `deposits(batchId, user)` — see [Read Confidential Balances](/protocol/confidential-vault/guides/read-balances.md) — or treat a zero `Joined` amount handle as a failed join.
{% endhint %}

If the user changes their mind while the batch is still pending, `quit(batchId)` on the batcher refunds their exact deposit.

## Claim: receive the confidential shares

Once the batch state is `Finalized`, claim the shares. `claim` is ungated — any address can submit it, and the shares always go to `account`:

```ts
const state = await publicClient.readContract({
  address: DEPOSIT_BATCHER,
  abi: BATCHER_ABI,
  functionName: "batchState",
  args: [batchId],
});
// 0 = Pending, 1 = Dispatched, 2 = Finalized, 3 = Canceled

if (state === 2) {
  const claimHash = await walletClient.writeContract({
    address: DEPOSIT_BATCHER,
    abi: BATCHER_ABI,
    functionName: "claim",
    args: [batchId, user],
  });
  await publicClient.waitForTransactionReceipt({ hash: claimHash });
}
```

If the batch state is `Canceled` instead — the batch was empty, paused, or missed its callback deadline — call `quit(batchId)` to recover the original cUSDC in full.

An operator currently delivers claims as a convenience, so a position can become claimed without the user doing anything — treat the `Claimed(batchId, account)` event as the "funds arrived" signal. Implement self-claim anyway; it is the guaranteed path, and claims never expire.

## Deposit into several vaults in one transaction

One cUSDC transfer to the [router](/protocol/confidential-vault/concepts/multi-vault-router.md) can fund several USDC vaults, and legs with an encrypted zero hide which vaults the user picked. The user gets an ordinary position in each deposit batcher.

The call is the same `confidentialTransferAndCall`, sent to the router with the per-vault split encoded in `data`. Encrypt twice: the transfer amount is bound to the cUSDC contract, the leg amounts to the router.

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

const legs = [
  { batcher: DEPOSIT_BATCHER_A, amount: 600_000_000n },
  { batcher: DEPOSIT_BATCHER_B, amount: 400_000_000n },
  { batcher: DEPOSIT_BATCHER_C, amount: 0n }, // decoy leg
];
const total = legs.reduce((sum, leg) => sum + leg.amount, 0n);

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

const data = encodeAbiParameters(
  [
    {
      type: "tuple[]",
      components: [
        { name: "batcher", type: "address" },
        { name: "token", type: "address" },
        { name: "amount", type: "bytes32" },
      ],
    },
    { type: "bytes" },
  ],
  [
    legs.map((leg, i) => ({
      batcher: leg.batcher,
      token: CUSDC, // ignored on the push path, but must be present
      amount: allocation.encryptedValues[i]!,
    })),
    allocation.inputProof,
  ],
);

const hash = await walletClient.writeContract({
  address: CUSDC,
  abi: WRAPPER_ABI,
  functionName: "confidentialTransferAndCall",
  args: [ROUTER, transfer.encryptedValues[0]!, transfer.inputProof, data],
});
```

Each leg emits a `Joined` event on its batcher. Decode the batch id per batcher from the receipt, as above, and track each position on its batcher. The router returns any unused balance to the user in the same transaction.

Three rules for the leg list:

* **Cap it at 10 legs.** A push fits about 18, but an exit fits about 10, and a user must be able to exit every vault they entered in one transaction.
* **Send the same list for every user.** The batchers a call names are public. The decoys work only when everyone sends the same list.
* **Round down when splitting by percentage.** A leg larger than the remaining balance joins with zero.

## Handle the possible reverts

Include these errors in your ABI so viem decodes them by name:

```ts
export const ERROR_ABI = parseAbi([
  "error BatchUnexpectedState(uint256 batchId, uint8 current, bytes32 expectedStates)",
  "error ZeroDeposits(uint256 batchId, address account)",
  "error EnforcedPause()",
  "error ERC7984UnauthorizedUseOfEncryptedAmount(bytes32 amount, address user)",
  "error SenderNotAllowedToUseHandle(bytes32 handle, address account)",
]);
```

| Revert                                    | Cause                                                                                                                                                                    |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `BatchUnexpectedState`                    | Claiming a non-finalized batch, or quitting a dispatched one. Re-read `batchState`.                                                                                      |
| `ZeroDeposits`                            | Claiming or quitting a batch this account never joined.                                                                                                                  |
| `EnforcedPause`                           | The batcher is paused; joins are rejected. Gate your deposit button on `paused()`.                                                                                       |
| `ERC7984UnauthorizedUseOfEncryptedAmount` | The encrypted input was bound to a different contract or user. Re-encrypt with the right pair. On a router call, leg amounts must be bound to the router, not the token. |
| `UnlistedConfidentialToken`               | A router push from a token the wrapper registry does not list.                                                                                                           |

Encryption and decryption calls are relayer round-trips — wrap them in a short retry so a transient relayer error does not surface as a failed deposit.

## Next steps

* [Track Batch State](/protocol/confidential-vault/guides/track-batches.md) — show batch progress and readiness in your UI.
* [Read Confidential Balances](/protocol/confidential-vault/guides/read-balances.md) — display the user's cShare balance.
* [Withdraw from a Vault](/protocol/confidential-vault/guides/withdraw.md) — the reverse flow.
* [Router Interface](/protocol/confidential-vault/reference/router-interface.md) — the leg format and 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/deposit.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.
