V7 Shielded Pool
The V7 pool is the latest version of Kusama Shield's zero-knowledge privacy pool, deployed on both Paseo testnet and Polkadot mainnet.
Shield (Deposit)

Unshield (Withdraw)

What's New in V7
| Feature | Description |
|---|---|
| 8 Public Signals | Up from 7 in earlier versions — enables stronger privacy guarantees |
| Linkability Fix | No deposits[] mapping — deposits and withdrawals are fully unlinkable |
| Known-Roots Window | 16-slot recent-roots window prevents griefing attacks |
| Proxy Withdraw | Route withdrawals through a proxy contract for extra unlinkability |
| Leaner Events | Deposit(address,bytes32) — nullifierHash is never exposed at deposit time |
| Async Proofs | Background ZK proof generation with status polling |
Commitment Derivation (V7)
All hashing uses Poseidon over BN254, matching the ZK circuit exactly:
nullifier = poseidon2([secret, 1])
nullifierHash = poseidon1([nullifier])
precommitment = poseidon2([nullifier, secret])
valueAsset = poseidon2([amountWei, assetId])
commitment = poseidon2([valueAsset, precommitment])
poseidon1is a single-input Poseidon hash. V7 uses it for nullifierHash — NOTposeidon2(nullifier, 0).
Withdrawal: 8 Public Signals
When the ZK proof is verified on-chain, the contract receives 8 public signals:
| Index | Signal | Purpose |
|---|---|---|
[0] | newCommitmentHash | Change commitment inserted into tree |
[1] | existingNullifierHash | Marks the spent commitment (prevents double-spend) |
[2] | contextHash | Replay protection (binds to chain/transaction) |
[3] | withdrawnValue | Amount being withdrawn |
[4] | treeDepth | Merkle tree depth (fixed at 128) |
[5] | context | Chain-specific context binding |
[6] | root | Merkle tree root the proof was generated against |
[7] | asset | Asset identifier (precompile address for pallet assets, 0 for native) |
Contract ABI (V7)
function depositNative(bytes32 commitment) external payable
function depositAsset(uint256 assetId, uint256 amount, bytes32 commitment) external
function depositAssetDirect(uint256 assetId, uint256 amount, bytes32 commitment) external
function withdraw(uint256[2] pA, uint256[2][2] pB, uint256[2] pC, uint[8] pubSignals, address recipient) external
function proxy_withdraw(uint256[2] pA, uint256[2][2] pB, uint256[2] pC, uint[8] pubSignals, address recipient) external
function currentRoot() external view returns (uint256)
function treeSize() external view returns (uint256)
function getEscrowBalance(address) external view returns (uint256)
function isNullifierSpent(bytes32) external view returns (bool)
function isKnownRoot(uint256) external view returns (bool)
function verifier() external view returns (address)
Proxy Withdrawals
The proxy_withdraw function routes the withdrawal through a separate proxy contract, giving the recipient a unique sender address each time.
Standard: Pool ──────────────────▶ Recipient
(pool address as sender)
Proxy: Pool ──▶ Proxy Contract ──▶ Recipient
(fresh address as sender)
This adds a layer of unlinkability even if the recipient tries to trace the sender. Enable it via the Proxy Withdraw toggle on the Unshield tab.
Gas cost for proxy withdrawals is roughly 17x higher than standard withdrawals due to the extra contract deployment and forwarding logic.
Merkle Tree
V7 uses a LeanIMT (Lean Incremental Merkle Tree):
| Parameter | Value |
|---|---|
| Depth | 128 |
| Hash function | Poseidon (BN254) |
| Known-roots window | 16 slots |
| Event | Deposit(address,bytes32) |
| Unpaired nodes | Propagated unchanged (not hashed with self) |
The tree is synced from on-chain events by the backend proxy. A background monitor polls every 2 seconds for new deposits. The tree_update_lock ensures withdrawals wait for in-progress sync operations.
Root Mismatch Retry
If the local tree root doesn't match the on-chain currentRoot(), the proxy will:
- Rebuild the tree from the deployment block (not just recent blocks)
- Retry up to 5 times
- If still mismatched, return an error
SS58 Address Forwarding
When a withdrawal targets a native SS58 (Substrate) address, the proxy performs a two-step process:
Pool ── withdraw ──▶ Account 1 (H160: 0x74e539fc...) ── transfer_keep_alive ──▶ SS58 destination
This is necessary because the pool only supports EVM (H160) recipients, but users want funds at their Substrate addresses.
Proof Generation
| Backend | Time | Used By |
|---|---|---|
| rapidsnark (C++) | ~4.3 seconds | Backend proxy |
| snarkjs (WASM) | ~15 seconds | Browser UI (client-side) |
The backend uses rapidsnark for production withdrawals. The browser falls back to snarkjs when generating proofs client-side (e.g., for the Unshield tab).
Circuit artifacts:
- WASM:
withdraw_phase2_fixed_v7.wasm - Proving key:
withdraw_phase2_fixed_v7_0001.zkey
Gas Costs (pallet-revive)
| Operation | Gas | USD (approx) |
|---|---|---|
| Deposit | ~45,000 | ~$0.19 |
| Withdraw (standard) | ~7,000 | ~$0.03 |
| Withdraw (proxy) | ~120,000 | ~$0.50 |
Gas costs on pallet-revive are ~270x cheaper than equivalent operations on Ethereum mainnet.
Deployed Contracts
Polkadot AssetHub (Mainnet)
| Contract | Address |
|---|---|
| Pool V7 | 0x0D694Da746e73D1e255c1894F90e38170db45809 |
| Verifier | 0x6A13781E43AEA21918120CD0E7a2ed8614c01e14 |
| Poseidon | 0xB8F0C6679D6Cc56450470522Bd96573C3D615052 |
| Chain ID | 420420419 |
| Deployment Block | 18460000 |
Paseo AssetHub (Testnet)
| Contract | Address |
|---|---|
| Pool V7 | 0xbcE09D4De052b2816df1285663ac89528DF45380 |
| Verifier | 0xcA4cBc5d31eccd08d393C43aF492F729FF30b685 |
| Poseidon | 0x1d165f6fE5A30422E0E2140e91C8A9B800380637 |
| Chain ID | 420420417 |
| Deployment Block | 11273491 |
Event Format
event Deposit(address indexed asset, bytes32 commitment);
Only the asset address and commitment hash are emitted. The nullifierHash is never exposed during deposit — it is only revealed when the deposit is spent (in the ZK proof's public signals).
Context Hash
Every withdrawal binds to a context hash for replay protection:
contextHash = keccak256(abi.encodePacked(senderAddress)) % BN254_R
Where BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617.
This ensures a proof generated for one chain or sender cannot be replayed elsewhere.
Source Code
- Pool contract: FixedIlopPhase2Paseo_v7.sol
- Circuit:
withdraw_phase2_fixed_v7.circom - Verifier: VerifierPhase2_Fixed_v7.sol