Private Deposits into Public DeFi: Zama's First Confidential Vault Design

Confidential tokens and onchain yield have, until now, been mutually exclusive. You could hold an ERC-7984 confidential token with an encrypted balance, or you could earn yield in an ERC-4626 vault, but not both. The moment you unwrapped your confidential USDC (cUSDC) to deposit into Morpho, your exact deposit amount was written in plaintext onchain, and the whole point of holding a confidential token was gone.

Zama’s first version of the confidential vault stack closes that gap. It lets confidential token holders deposit into public ERC-4626 vaults without revealing the individual deposit amount. It does so by reusing a generic OpenZeppelin primitive. Further versions of DeFi offerings for confidential token holders will focus on tailor-made solutions to expand on this initial version.

This post explains what is actually in this first version of the confidential vault stack: the building block we reuse, the exact transaction flow, the privacy model and its honest limits, and where the design goes next.

The generic building block

The core of the system is OpenZeppelin's BatcherConfidential. It is worth being precise about what this contract is, because it is the part most people get wrong: [.c-inline-code]BatcherConfidential[.c-inline-code] is neither a DeFi contract, nor a vault contract.

It is a general-purpose primitive for pooling users who share the same intent. Any number of users submit an encrypted amount of some [.c-inline-code]fromToken[.c-inline-code]; the batcher aggregates those amounts homomorphically; at dispatch it decrypts only the aggregate total, performs one route from [.c-inline-code]fromToken[.c-inline-code] to [.c-inline-code]toToken[.c-inline-code] on behalf of the whole pool, wraps the resulting [.c-inline-code]toToken[.c-inline-code] into their corresponding confidential token and then distributes back to each participant pro-rata in the encrypted domain. The route in the middle is an abstract method. It could be a swap, a bridge, a lending deposit, anything with a fromToken → toToken shape.

The privacy comes from the pooling. Individual amounts go in encrypted and never come out; the only number ever revealed is the sum across everyone in the batch. An observer learns that Alice put something into batch N and learns the batch total, but not Alice's share of it.

Our contribution in this first version of the confidential vault stack is to implement that one abstract route for each ERC-4626 route (deposit and redeem):

function _executeVaultRoute(uint256, uint256 amount)
    internal override returns (ExecuteOutcome)
{
    uint256 underlyingAmount = amount * fromToken().rate();
    try _vault().deposit(underlyingAmount, address(this)) {
        return ExecuteOutcome.Complete;
    } catch {
        // A vault failure is caught and the batch is recovered,
        // not locked; see "When things go wrong"
        return ExecuteOutcome.Cancel;
    }
}

That is essentially the whole DeFi-specific surface. Deposit aggregation, FHE arithmetic, unwrap/wrap orchestration, exchange-rate computation, claim distribution, and failure recovery are all inherited from the base contract. We add the vault call, a callback deadline, slippage protection, and a minimum batch age.

Reusing a generic, audited primitive instead of writing complex, bespoke privacy code is a deliberate v1 choice: get production-grade confidential infrastructure first, keep it simple, and expand on it later.

What we deploy

A vault is served by a pair of batchers plus one share wrapper:

an [.c-inline-code]ERC7984ERC20Wrapper[.c-inline-code] over the vault's plaintext share token, so vault shares can themselves be held confidentially.

a deposit batcher — [.c-inline-code]fromToken[.c-inline-code] = confidential USDC, [.c-inline-code]toToken[.c-inline-code] = confidential vault shares;

a withdrawal batcher — [.c-inline-code]fromToken[.c-inline-code] = confidential vault shares, [.c-inline-code]toToken[.c-inline-code] = confidential USDC;

Each batcher is scoped to a single vault. Supporting several vaults means deploying as many pairs. That is an intrinsic limitation of the underlying primitive: one batcher per route, two routes per vault, no netting across routes.

The deposit flow

Here is what a confidential deposit actually looks like onchain with two users, Alice and Bob, depositing into a Morpho vault.

1. Join the batch ([.c-inline-code]join[.c-inline-code]). Alice submits an encrypted amount and a proof. The batcher pulls her confidential tokens via [.c-inline-code]confidentialTransferFrom[.c-inline-code] and adds her encrypted amount to the running batcher’s [.c-inline-code]totalDeposits[.c-inline-code]. Bob does the same. No amounts are visible; the only public fact is that Alice's and Bob's addresses joined batch [.c-inline-code]N[.c-inline-code].

Users can change their mind. [.c-inline-code]quit(batchId)[.c-inline-code] reclaims an encrypted deposit from a pending batch at any time before dispatch.

2. Dispatch the batch ([.c-inline-code]dispatchBatch[.c-inline-code]). Once the batch is old enough (more on [.c-inline-code]minBatchAge[.c-inline-code] below), anyone can dispatch it. Dispatch pins the current vault rate (for slippage protection), then calls unwrap on the confidential token for the aggregate [.c-inline-code]totalDeposits[.c-inline-code]. This kicks off the asynchronous decryption of the total on Zama's network. In the meantime, the next batch ([.c-inline-code]N+1[.c-inline-code]) opens for deposits immediately.

3. Settle the batch (dispatchBatchCallback). When the decryption proof is ready (a few hundreds of milliseconds later), anyone can submit it. The batcher finalizes the unwrap, receiving the plaintext aggregate of underlying USDC, and runs the route:

[.c-inline-code]vault.deposit(totalUnderlying)[.c-inline-code] → the vault returns shares;

the base contract wraps those shares into the confidential share token;

it computes a single public [.c-inline-code]exchangeRate = wrappedShares · 1e6 / totalDeposits[.c-inline-code].

Only the aggregate total was ever decrypted. The exchange rate is public, which is actually the batch-wide conversion ratio.

4. Claim shares ([.c-inline-code]claim[.c-inline-code]). Alice calls [.c-inline-code]claim(N)[.c-inline-code]. The batcher computes her output in the encrypted domain:

aliceShares = FHE.div(FHE.mul(aliceDeposit_encrypted, exchangeRate), 1e6)

and transfers her the confidential vault shares. Bob does the same. Each user's deposit and each user's share balance stay encrypted end to end.

Withdrawal is the mirror image through the second batcher: encrypted shares in, [.c-inline-code]vault.redeem[.c-inline-code] on the aggregate, confidential USDC (cUSDC) back out.

What happens when things go wrong

v1 is built so a batch can never get permanently stuck with users' funds inside it. The route returns an [.c-inline-code]ExecuteOutcome[.c-inline-code], and the base callback acts on it:

Outcome Effect
Complete Batch finalizes; shares are wrapped, exchange rate set, users claim.
Cancel Inputs are re-wrapped into fromToken; users quit to recover.

If the vault reverts (paused, capped), the try-catch returns [.c-inline-code]Cancel[.c-inline-code] on the first callback. If nobody can settle the batch and a [.c-inline-code]callbackDeadline[.c-inline-code] passes, the route is force-cancelled. If every depositor quit before dispatch and the total decrypts to zero, the base contract auto-cancels instead of dividing by zero. In all of these, deposits come back.

Privacy depends on the size of the pool

We want to be direct about this, because it is the defining tradeoff of Zama’s first version of the confidential vault stack.

The anonymity set is exactly the set of independent depositors in a batch. With one depositor, the aggregate total is that person's deposit. With five depositors of similar size, recovering any individual amount is statistically harder. The privacy scales with participation.

It is not unconditional. A motivated adversary who controls N−1 of the addresses in a batch can deposit known amounts, wait for a victim, dispatch, and subtract: [.c-inline-code]victimDeposit = batchTotal − Σ(attackerDeposits)[.c-inline-code]. This is an inherent property of any batch-aggregate privacy model.

Zama’s first version of the confidential vault stack mitigates this by enforcing a minimal batch age before dispatch: a batch cannot be dispatched until it has had time to accumulate organic depositors, raising the cost and lowering the odds of isolating a victim. Longer window = stronger anonymity set but slower settlement; it is an owner-tunable knob.

Protocol-injected entropy can be added as well. It doesn’t require any update to the onchain protocol but is an off-chain service that can be run in parallel to bootstrap anonymity.

So the honest statement is: Zama’s first version of the confidential vault stack provides meaningful privacy against passive observers and materially raises the cost of active deanonymization. It does not claim perfect privacy against a well-funded adversary willing to dominate a batch.

What this first version of Zama’s Confidential Vault Stack unlocks today

With this, confidential token holders can earn ERC-4626 yield without actively deanonymizing their position. We are launching with top-tier vault partners so that the very first batches have real, organic depositors, which is precisely what makes the anonymity set meaningful from day one.

The tradeoffs above are v1 tradeoffs by design. The architecture is deliberately thin: a generic batcher, a five-line route, and a share wrapper. That thinness is what let us build on audited foundations and ship a privacy-preserving DeFi gateway now, rather than a bespoke system later.

Where this goes next

The first version of the confidential vault stack cost model is dominated by crossing the confidential/plaintext boundary: every batch unwraps confidential tokens to plaintext to enter the vault, then wraps the result back. The user experience stays the same as we attack that boundary, but the machinery underneath gets sharper:

A native confidential ERC-4626 that performs netting internally, matching deposits against withdrawals so only the net flow ever has to cross the unwrap/wrap boundary. Two users moving in opposite directions can settle against each other with no plaintext round-trip at all.

A single confidential gateway that nets across all operations at the one shield/unshield boundary, so the entire protocol exposes one aggregate flow to the public chain instead of one per batch per vault, shrinking both the cost and the information surface.

Same UX, same [.c-inline-code]join → claim[.c-inline-code] flow for the user. Under the hood, fewer boundary crossings, larger effective anonymity sets, and lower cost. v1 is the foundation that makes that evolution only a swap of the route and settlement layer away.

News, research and product releases

Latest Blog Posts