Skip to main content

Privacy / Shielded Vaults

Zero-knowledge shielded deposits that break the on-chain link between depositor and withdrawer using Groth16 BN254 proofs verified on StarkNet via Garaga.

Overview

Sable’s privacy layer allows users to deposit into yield vaults without revealing their identity on-chain. Withdrawals can be made to any address with no traceable connection to the original deposit.
Deposit Address A  ──┐
                     ├──► Shielded Pool ──► Withdraw to Address B
Deposit Address C  ──┘        (ZK proof)      (no on-chain link)

Pool Types

TabPool TypeTokenDescription
LendingSentinel VaultWBTCPrivate deposits into Sable Sentinel (lending yield)
YieldDelta Neutral VaultWBTCPrivate deposits into Sable Delta Neutral (spread yield)
StablesStablecoin VaultUSDCPrivate USDC deposits into Vesu RE7 USDC Core
SwapSwap PoolWBTC→ETH/USDC/STRKPrivate token swaps via pool-to-pool transfers

How Shielded Deposits Work

Deposit Flow

1

Generate Note Locally

User generates a random note in the browser:
const note = {
  secret:    randomBytes(31),
  nullifier: randomBytes(31),
  commitment: Poseidon(secret, nullifier)
}
The note is saved to browser localStorage. Losing the note = losing your funds permanently.
2

Submit Deposit

User approves WBTC and submits deposit transaction:
WBTC.approve(pool_address, denomination)
pool.deposit(commitment)
Fixed denominations ensure uniform anonymity sets:
  • Sentinel: 0.0002, 0.0004, 0.0006, 0.0008 BTC
  • Delta Neutral: 0.00036, 0.00072, 0.00108, 0.00144 BTC
  • Stablecoin: 10, 25, 50, 100 USDC
3

Batch Processing

Relayer batches 3 deposits together:
pool.process_batch([
  commitment_1,
  commitment_2,
  commitment_3
])
All 3 commitments are inserted into the Merkle tree.
Batching improves anonymity — withdrawals cannot be linked to specific deposits within a batch.
4

Note Saved

Note is saved to browser localStorage with:
  • Commitment
  • Nullifier
  • Secret
  • Leaf index (after batch confirmation)
  • Merkle path

Withdrawal Flow

1

Generate ZK Proof

User generates a Groth16 proof locally in the browser:
const proof = await generateProof({
  // Private inputs (not revealed on-chain)
  secret,
  nullifier,
  pathElements,
  pathIndices,
  
  // Public inputs (verified on-chain)
  root,
  nullifierHash,
  recipient,
  relayer,
  fee,
  batchStart,
  batchSize
})
Proof generation takes ~5-10 seconds in browser.
2

Submit to Relayer

Proof is submitted to the relayer API:
POST /api/relayer/withdraw
{
  "proof": "0x...",
  "publicInputs": [...],
  "recipient": "0x...",
  "fee": "0.0001"
}
3

On-Chain Verification

Relayer calls the pool contract:
pool.withdraw(
  proof,
  root,
  nullifierHash,
  recipient,
  relayer,
  fee
)
Garaga verifier contract validates the Groth16 proof on-chain.
4

Receive Funds

On successful verification:
  • Nullifier hash is recorded (prevents double-spend)
  • Funds sent to recipient address
  • Relayer fee deducted
Recipient has no on-chain link to the original depositor.

Merkle Tree Structure

                     ROOT
                    /    \
                   /      \
               H(0,1)    H(2,3)
               /    \    /    \
             L0    L1  L2    L3     ◄── Leaf = Poseidon(commitment)
             │     │   │     │
           Note0 Note1 Note2 Note3

Tree Parameters

  • Depth: 20 levels (supports ~1M deposits)
  • Hash function: Poseidon (StarkNet-native, efficient in-circuit)
  • Batch size: 3 deposits per batch
  • Proof size: ~1KB (Groth16 BN254)

Fixed Denomination Pools

Sentinel (Lending)

Pool KeyDenominationContract Address
sentinel_1x0.0002 BTC0x002bdb9769851d0307e812351cc1eb31b617951fba786cfd5d58baff36589a33
sentinel_2x0.0004 BTC0x02a94630f46bcf7362c12ed5b0163b4dd7644eb923aaada61ffb858d7912e03d
sentinel_3x0.0006 BTC0x038224d3966b850913cfc4dd610032d8082e14c90fce91819a0fb994b1cc63f3
sentinel_4x0.0008 BTC0x06de0d6c46431628f0cd257aa4384125b2380a7c5362aab2b146283181c2dff3

Delta Neutral (Yield)

Pool KeyDenominationContract Address
dn_1x0.00036 BTC0x07298d2765e1dc61dae0f5d8c70b86e1857b038ab7a1f7c473111321aaac51aa
dn_2x0.00072 BTC0x059511116f7e1877fc9e3d26a2b9165d02cc367414c009c94b7b76f6d1e4c929
dn_3x0.00108 BTC0x01191727f6135bb878e9771066b0c6bcc18faed9146eb0a04eabb83190b90ce3
dn_4x0.00144 BTC0x040babfa49b967c7873e3b275b7f8d6bea88028854f5b5923c2de5af76d78c56

Stablecoin (Private USDC)

Pool KeyDenominationContract Address
stable_1010 USDC0x04f4af2cf01a1cf28f424ce2ce3d7fed7f11792c88e5ce7a3eedd21cea24a5eb
stable_2525 USDC0x05aa66a4541caf4d43a526edd89c0285a671e0be0024ae5f8ca9f6734f4b7c89
stable_5050 USDC0x03226cbb8976f41eddf88c2871cf4d04653a1b6415dee7037a1ceaf641e977a9
stable_100100 USDC0x06348e9e2db703841bed848146ef895c8aed4f3c143e76bd145d9b3c544cea68

Private Swap Flow

Private swaps break the link between input token depositor and output token recipient:
1. Deposit WBTC into swap input pool (shielded)
2. Generate withdrawal proof targeting swap pool contract
3. Swap pool receives WBTC, swaps via AVNU DEX
4. Output token sent to recipient address
5. No on-chain link between depositor and recipient

ZK Circuit Versions

VersionProof SystemVerifierStatus
V1UltraHonk (Noir)@aztec/bb.jsLegacy
V2Groth16 BN254snarkjs + GaragaLegacy
V3Groth16 BN254 (6 public inputs)Garaga 0x0410...Legacy
V4Groth16 BN254 (7 public inputs)Garaga 0x0332...Current

Current Verifier

Contract: 0x03329c4d5c2e37dfd20d46c3c20be9230b2152c71947ead441c342d989d52ffa Public Inputs (7):
  1. Merkle root
  2. Nullifier hash
  3. Recipient address
  4. Relayer address
  5. Fee amount
  6. Batch start index
  7. Batch size

Security Considerations

Critical: Notes are stored in browser localStorage. If you lose your note, you lose your funds permanently. Always export and backup your notes.

Best Practices

  1. Export Notes Immediately: After each deposit, export your notes to a secure location
  2. Multiple Backups: Store encrypted note backups in 2+ locations
  3. Use Different Withdrawal Address: Withdraw to a fresh address for maximum privacy
  4. Wait for Larger Anonymity Set: More deposits in your denomination = stronger privacy
  5. Use Relayer: Let the relayer submit your withdrawal transaction to hide your IP

Privacy Limitations

Privacy depends on the number of deposits in your denomination pool:
  • < 10 deposits: Weak anonymity
  • 10-100 deposits: Moderate anonymity
  • > 100 deposits: Strong anonymity
Check pool size before depositing.

Relayer Flow

1

Fee Estimation

GET /api/relayer/fee-estimate?pool=sentinel_1x
Returns estimated relayer fee (typically 0.5-1% of denomination).
2

Generate Proof

User generates proof locally with relayer address and fee.
3

Submit to Relayer

POST /api/relayer/withdraw
{
  "proof": "0x...",
  "publicInputs": [...],
  "poolKey": "sentinel_1x"
}
4

Relayer Execution

Relayer submits transaction on-chain, pays gas, receives fee.

Technical Files

FilePurpose
src/lib/privacy/note.tsNote generation, Poseidon hashing, nullifiers
src/lib/privacy/prover.tsGroth16 proof generation (snarkjs WASM)
src/lib/privacy/merkle.tsMerkle tree management
src/lib/privacy/calldata.tsProof calldata serialization
contracts/src/shielded_pool_v4.cairoCurrent shielded pool contract
contracts/src/shielded_swap_pool.cairoPrivate swap pool contract

Next Steps

Make Your First Shielded Deposit

Step-by-step guide to private deposits

Withdraw Privately

Generate ZK proofs and withdraw to a new address

Export & Backup Notes

Secure your notes to prevent fund loss

Understanding ZK Proofs

Learn how Groth16 proofs work

Build docs developers (and LLMs) love