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
Deposit Phase : User deposits WBTC into V4 shielded pool
Swap Phase : Relayer calls private_swap() with:
V4 withdrawal proof (recipient = swap pool)
New commitment for output token
AVNU routes
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>,
)
V4 shielded pool contract address
V4 withdrawal proof where recipient = this contract
BN254 Poseidon hash for output token deposit
Desired output token (ETH, USDC, STRK, etc.)
Minimum output amount (slippage protection)
AVNU swap routes (computed off-chain)
Record WBTC balance before V4 withdrawal
Call wbtc_pool.withdraw(proof_calldata) — WBTC arrives atomically
Measure WBTC received
Approve AVNU and swap WBTC → output_token
Measure output token received
Insert new_commitment into Merkle tree
Store per-leaf amount and token address
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>)
Garaga Groth16 proof blob (same 7 public inputs as V4)
Public Inputs (7) :
Index Field Type Constraint 0 root u256 Must be in recent history (last 30) 1 nullifierHash u256 Must not be spent 2 recipient u256 Output token recipient 3 relayer u256 Fee recipient 4 fee u256 Relayer fee (≤ max_fee_bps) 5 batchStart u256 Leaf index (= leafIndex) 6 batchSize u256 MUST be 1
Verify Groth16 proof via Garaga
Verify batchSize == 1 (single-leaf batch)
Check nullifier hasn’t been spent
Verify Merkle root is known
Look up stored amount and token for leaf at batchStart
Verify fee ≤ max_fee_bps of stored amount
Mark nullifier as spent
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.
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