Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/collinsville22/Sable/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Shielded Swap Pool enables private swaps from WBTC shielded pools to output tokens (ETH, USDC, STRK) without revealing:
  • Source WBTC amount
  • Swap timing
  • Output token type (until withdrawal)
  • User identity
Key Innovation: Reuses the same V4 Groth16 circuit — no new trusted setup.

Architecture

V4 Pool (WBTC) → [ZK Proof] → Swap Pool → AVNU → Output Token Pool

                            Merkle Tree (BN254)

                            Per-Leaf Storage

                        [ZK Proof] → Recipient

Flow

  1. Deposit Phase: User deposits WBTC into V4 shielded pool
  2. Swap Phase: Relayer calls private_swap() with:
    • V4 withdrawal proof (recipient = swap pool)
    • New commitment for output token
    • AVNU routes
  3. Withdrawal Phase: User proves ownership of output commitment, receives output token

Core Functions

private_swap

Execute a private swap: withdraw WBTC from V4 pool → swap to output token → store commitment.
fn private_swap(
    ref self: ContractState,
    wbtc_pool: ContractAddress,
    proof_calldata: Span<felt252>,
    new_commitment: u256,
    output_token: ContractAddress,
    min_amount_out: u256,
    routes: Array<Route>,
)
wbtc_pool
ContractAddress
required
V4 shielded pool contract address
proof_calldata
Span<felt252>
required
V4 withdrawal proof where recipient = this contract
new_commitment
u256
required
BN254 Poseidon hash for output token deposit
output_token
ContractAddress
required
Desired output token (ETH, USDC, STRK, etc.)
min_amount_out
u256
required
Minimum output amount (slippage protection)
routes
Array<Route>
required
AVNU swap routes (computed off-chain)
  1. Record WBTC balance before V4 withdrawal
  2. Call wbtc_pool.withdraw(proof_calldata) — WBTC arrives atomically
  3. Measure WBTC received
  4. Approve AVNU and swap WBTC → output_token
  5. Measure output token received
  6. Insert new_commitment into Merkle tree
  7. Store per-leaf amount and token address
  8. Emit SwapDepositEvent
Each swap deposit is treated as a “batch of 1” for V4 circuit compatibility.

withdraw

Withdraw output token using a Groth16 ZK proof.
fn withdraw(ref self: ContractState, proof_with_hints: Span<felt252>)
proof_with_hints
Span<felt252>
required
Garaga Groth16 proof blob (same 7 public inputs as V4)
Public Inputs (7):
IndexFieldTypeConstraint
0rootu256Must be in recent history (last 30)
1nullifierHashu256Must not be spent
2recipientu256Output token recipient
3relayeru256Fee recipient
4feeu256Relayer fee (≤ max_fee_bps)
5batchStartu256Leaf index (= leafIndex)
6batchSizeu256MUST be 1
  1. Verify Groth16 proof via Garaga
  2. Verify batchSize == 1 (single-leaf batch)
  3. Check nullifier hasn’t been spent
  4. Verify Merkle root is known
  5. Look up stored amount and token for leaf at batchStart
  6. Verify fee ≤ max_fee_bps of stored amount
  7. Mark nullifier as spent
  8. Send (amount - fee) to recipient, fee to relayer
batchSize MUST be 1. The circuit proves the leaf is at batchStart index with a single-element batch.

Storage

Merkle Tree (Same as V4)

next_index: u32
filled_subtrees: Map<u32, u256>
roots: Map<u32, u256>
current_root_index: u32
commitments: Map<u256, bool>
nullifier_hashes: Map<u256, bool>

Per-Leaf Token Tracking

leaf_amount: Map<u32, u256>              // Stored output token amount
leaf_token: Map<u32, ContractAddress>    // Stored output token address
Unlike V4’s batch/vault system, Swap Pool stores per-leaf amounts and token addresses directly.

View Functions

fn get_leaf_info(self: @ContractState, leaf_index: u32) -> (ContractAddress, u256)
fn get_next_index(self: @ContractState) -> u32
fn is_spent(self: @ContractState, nullifier_hash: u256) -> bool
fn get_last_root(self: @ContractState) -> u256
fn active_deposits(self: @ContractState) -> u32
fn total_swaps(self: @ContractState) -> u32

Events

SwapDepositEvent

pub struct SwapDepositEvent {
    pub commitment: u256,          // [key]
    pub leaf_index: u32,
    pub output_token: ContractAddress,
    pub output_amount: u256,
    pub timestamp: u64,
}

SwapWithdrawEvent

pub struct SwapWithdrawEvent {
    pub nullifier_hash: u256,      // [key]
    pub recipient: ContractAddress,
    pub output_token: ContractAddress,
    pub payout: u256,
    pub fee: u256,
}

Configuration

Constructor

fn constructor(
    ref self: ContractState,
    wbtc: ContractAddress,
    verifier: ContractAddress,     // Garaga Groth16 BN254 verifier
    owner: ContractAddress,
)

Parameters

fn set_verifier(ref self: ContractState, verifier: ContractAddress)
fn set_max_fee_bps(ref self: ContractState, bps: u32)  // Hard cap: 1000 (10%)

Curator Functions

fn pause(ref self: ContractState)
fn unpause(ref self: ContractState)
fn upgrade(ref self: ContractState, new_class_hash: ClassHash)
fn emergency_withdraw(
    ref self: ContractState,
    token: ContractAddress,
    recipient: ContractAddress
)

Privacy Model

Hidden from V4 Pool

  • Withdrawal destination (swap pool)
  • Intent to swap
  • Output token choice

Hidden from AVNU

  • Original depositor
  • Final recipient
  • Source pool
  • WBTC withdrawal uses V4 anonymity set (3-7 users)
  • Swap timing decoupled from deposit timing
  • Output token type hidden until final withdrawal
  • Final recipient unlinked from WBTC depositor
  • On-chain swap event reveals output_token and output_amount
  • AVNU swap is visible (but relayer-submitted)
  • Final withdrawal reveals recipient address

Integration Example

use btcvault::shielded_swap_pool::{IShieldedSwapPoolDispatcher, IShieldedSwapPoolDispatcherTrait};
use btcvault::interfaces::Route;

// Step 1: User deposits WBTC into V4 pool (off-chain proof generation)
let wbtc_pool = IShieldedPoolV4Dispatcher { contract_address: wbtc_pool_addr };
wbtc_pool.deposit(wbtc_commitment, user_addr);

// Step 2: Relayer executes private swap
let swap_pool = IShieldedSwapPoolDispatcher { contract_address: swap_pool_addr };
let v4_proof = generate_v4_proof(
    secret: secret,
    nullifier: wbtc_nullifier,
    recipient: swap_pool_addr,  // KEY: recipient = swap pool
    relayer: relayer_addr,
    fee: 0,
    batchStart: batch_start,
    batchSize: batch_size,
);

let avnu_routes = compute_routes_offchain(WBTC, USDC, amount);
let output_commitment = poseidon_hash_2(output_secret, output_nullifier);

swap_pool.private_swap(
    wbtc_pool: wbtc_pool_addr,
    proof_calldata: v4_proof,
    new_commitment: output_commitment,
    output_token: USDC_ADDRESS,
    min_amount_out: min_usdc,
    routes: avnu_routes,
);

// Step 3: User withdraws output token (generates new proof)
let output_proof = generate_v4_proof(
    secret: output_secret,
    nullifier: output_nullifier,
    recipient: final_recipient,
    relayer: relayer_addr,
    fee: relayer_fee,
    batchStart: leaf_index,      // Same as leaf_index
    batchSize: 1,                 // MUST be 1
);
swap_pool.withdraw(output_proof);

Security Considerations

Circuit Reuse: The V4 circuit was designed for fixed-denomination batches. Swap Pool adapts it by treating each swap as a “batch of 1” — this works but reduces anonymity set to 1 per swap.
AVNU Integration: Swap execution is permissionless and atomic. Slippage protection via min_amount_out.
Fee Model: Relayer earns fees in output token, NOT WBTC. This decouples relayer incentives from V4 pool.

AVNU Route Format

pub struct Route {
    pub token_from: ContractAddress,
    pub token_to: ContractAddress,
    pub exchange_address: ContractAddress,
    pub percent: u128,  // Basis points (10000 = 100%)
    pub additional_swap_params: Array<felt252>,
}
Use AVNU API to compute optimal routes off-chain. Pass the result directly to private_swap().

See Also