Withdrawal Flow

Withdrawals use zero-knowledge proofs to privately transfer funds without revealing the original depositor. The proof demonstrates that the user knows the secret behind a valid commitment in the pool, while a nullifier prevents double-spending.

Sequence Overview

Step-by-Step Walkthrough

1

Load Account

User enters their 12-word recovery mnemonic. The SDK reconstructs the secret seed and derives all deposit precommitments from it.

2

Fetch Merkle Data

The SDK contacts the ASP (Association Set Provider) service to retrieve the current state tree and ASP tree data, including the Merkle proofs needed for proof generation.

3

Generate ZK Proof

A Groth16 ZK-SNARK proof is generated entirely client-side using snarkjs. The proof demonstrates knowledge of the deposit secret and valid tree membership without revealing the original depositor.

4

Submit to Relayer

The proof and withdrawal parameters are submitted to a relayer service. The relayer pays the gas cost and broadcasts the transaction, preventing the recipient address from being linked to the depositor.

5

On-Chain Verification

The Entrypoint contract forwards the proof to the on-chain Groth16 verifier. The verifier checks all public signals against the proof and confirms validity.

6

Execute Withdrawal

The Privacy Pool marks the nullifier as spent (preventing double-spending), inserts a new commitment for any remaining change, and transfers the withdrawn amount minus the relay fee to the recipient.

Client-Side Proving

Proofs are generated entirely client-side in the browser using snarkjs. No private data leaves the user's device. The proving process typically takes 10-30 seconds depending on the device.

ZK Proof Public Signals

The Groth16 proof exposes the following public signals that are verified on-chain. These signals constrain the proof without revealing any private inputs (deposit secret, Merkle paths, or precommitment).

SignalDescription
newCommitmentHashHash of the new "change" commitment created for any unspent balance remaining after the withdrawal.
existingNullifierHashDerived from the deposit secret. Published on-chain and recorded to prevent the same deposit from being spent twice.
withdrawnValueThe amount being withdrawn from the pool, denominated in the asset's smallest unit.
stateRootThe root of the state Merkle tree at proof generation time. Must match one of the recent on-chain roots to be accepted.
stateTreeDepthDepth of the state Merkle tree. Used by the circuit to verify the inclusion proof path length.
ASPRootMust match the latest on-chain ASP (Association Set Provider) root. Proves the deposit is part of the compliant set.
ASPTreeDepthDepth of the ASP Merkle tree. Used to verify the ASP inclusion proof within the circuit.
contextHash of the withdrawal parameters (recipient, relayer fee, etc.) combined with the pool scope. Binds the proof to a specific withdrawal request.

Fee Structure

Two fees are applied across the deposit-withdrawal lifecycle. Both are denominated in basis points (BPS), where 1 BPS = 0.01%.

Vetting Fee

baseFeeBPS

Deducted at deposit time by the Entrypoint contract. Covers the cost of ASP compliance vetting. The remaining amount after deduction is forwarded to the Privacy Pool.

Relay Fee

relayFeeBPS

Deducted at withdrawal time. Compensates the relayer for gas costs and transaction submission. The relayer broadcasts the withdrawal transaction so the recipient address is never linked to the depositor.

Total Fee Calculation

netReceived = depositAmount * (1 - baseFeeBPS / 10000) - relayFee

The vetting fee is percentage-based and deducted from the deposit. The relay fee is deducted from the withdrawal amount. The user receives the net amount after both deductions. Exact fee values are configured per pool and can be queried from the Entrypoint contract.

Stale State Roots

The stateRoot used in proof generation must match a recent on-chain root. If too much time passes between proof generation and submission, the root may expire and the transaction will revert. Re-generate the proof with fresh Merkle data if this occurs.

SDK Integration

The SDK handles account loading, Merkle data fetching, proof generation, and relayer submission in a streamlined API:

withdraw.ts
import { ShieldFlow } from '@shieldflow/sdk';

const sf = new ShieldFlow({ chainId: 1, rpcUrl: process.env.RPC_URL });

// Load account from recovery phrase
const account = await sf.loadAccount({
  mnemonic: 'word1 word2 word3 ... word12',
});

// List available deposits
const deposits = await account.getDeposits();
console.log('Available balance:', deposits.totalBalance);

// Withdraw to a recipient address
const withdrawal = await sf.withdraw({
  account,
  depositIndex: 0,
  recipient: '0xRecipientAddress...',
  amount: '0.5',
  relayerUrl: 'https://relay.shieldflow.xyz',
});

console.log('Withdrawal tx:', withdrawal.txHash);
console.log('Nullifier:', withdrawal.nullifierHash);

Partial Withdrawals

You can withdraw a portion of a deposit. The circuit creates a new "change" commitment for the remaining balance, which can be withdrawn later with the same recovery phrase.