ShieldPass is a private cross-border remittance corridor. It lets anyone swap crypto for Nigerian naira instantly, paid straight to their bank account — without ever exposing their identity or banking data on-chain — and send funds privately to other users, where the amount, sender, and receiver stay hidden.
Your crypto lives in a Shielded Pool, not a standard escrow. You prove ownership and compliance with zero-knowledge proofs generated locally on your device, and the smart contract verifies every proof on-chain using Stellar's native BN254 pairing functions. The chain only ever sees mathematics — never your BVN, name, or bank details.
Built for the Stellar Hacks: ZK hackathon. The ZK proof is load-bearing: no valid proof ⇒ no swap, no transfer, no payout. Verification happens on Soroban itself, not off-chain.
Shielded Pool • On-chain Groth16 (BN254) • Private P2P Transfers • Multi-Asset (XLM + USDC) • Zero-Storage Backend • Passkey Smart Wallets • Gasless
shieldpass.demo.mp4
Traditional off-ramps force you to upload your identity, trust a custodian with your funds, and publicly broadcast your transactions. ShieldPass eliminates all three — and adds fully private peer-to-peer payments on top.
- Old Way: When you sent money, everyone could see it sitting in an escrow account.
- New Way: All user funds of a given asset are mixed in one pool. Each balance is a secret note (
Poseidon(amount, owner, randomness, …)) owned by your shielded key. Nobody scanning the chain can tell which note is yours. ShieldPass currently runs two live pools — XLM and USDC — each a separate contract instance (same audited wasm/circuits, bound to a different token atinit).
- Old Way: Proofs were checked off-chain by a backend you had to trust.
- New Way: The Soroban contract verifies every Groth16 proof itself, on-chain, via the native
bn254pairing-check host function (~37.5M instructions — well within budget). The math is the only authority.
- Old Way: The pool would have to trust a relayer to publish the correct Merkle root.
- New Way: A dedicated
merkle_insertZK circuit proves everyold_root → new_roottransition. The contract verifies each append, so the commitment tree advances trustlessly — no party can forge a root.
4. Private Payments (Hidden Amounts)
- Old Way: Sending money revealed who, to whom, and how much.
- New Way: A
shielded_transferproof spends your note and mints one note for the recipient + a change note for you — entirely inside the pool. The amount, sender, and receiver are all hidden; only the recipient learns the amount (from an encrypted blob).
- Old Way: We saved your bank account in our database. A breach leaked your banking info.
- New Way: We don't save your bank account at all. It lives in your browser; at swap time your phone flashes the details to the backend, we pay the bank, and the details are dropped from memory.
- Old Way: If the off-ramp took your crypto but never paid the fiat, your funds were gone.
- New Way: A withdrawal pre-commits a refund note on-chain. If the naira never settles, after a 1-hour time-lock you reclaim your value trustlessly — the platform can never simply keep the crypto. (A future zkTLS
fiat_payoutproof will make the claim itself fully trustless; today the claim is admin-gated and the refund is your guarantee.)
Meet Tobi, who wants to use ShieldPass:
1. Onboard (invisible funding). Tobi signs up with a Passkey (Face ID). His shielded identity (spending + encryption keys) is derived from the passkey itself. ShieldPass mints him a secret note worth 500 XLM — no public transaction happens. To the outside world, his wallet holds $0.
2. Shield / Unshield. Tobi can move his own crypto — XLM or USDC — into the private pool (Shield) or pull it back out to his wallet (Unshield) at any time.
3. Withdraw to Naira. Tobi cashes out 100 XLM. On his phone a ZK proof spends his note, mints a 400 XLM change note, and authorizes the contract. The backend pays his bank via Paystack/Lenco; the change stays private.
4. Send privately. Tobi sends 50 XLM to a friend by email or shp_ address. A shielded_transfer proof moves the value inside the pool — fully private. His friend's app scans, decrypts the note, and their shielded balance just goes up. A 🔔 notification fires.
5. Stay informed. Every action — faucet, shield, unshield, withdraw, send, and received payments — lands in an in-app Activity feed with an unread badge.
Three Circom/Groth16 circuits over BN254, all verified on-chain:
confidential_swap(browser): proves you own a valid note, derives the spendnullifier, and computes the change. Used by Withdraw and Unshield.swap_amountis public here (real value leaves the pool).shielded_transfer(browser): spends one note → two output notes (recipient + change). Amounts are private; the recipient's owner tag is bound into the proof. Powers private P2P payments.merkle_insert(backend): proves eachold_root → new_roottree append so the pool's commitment tree advances trustlessly — no on-chain hashing needed.
flowchart LR
subgraph Device["📱 Your Device"]
SK["Shielded key (from passkey PRF)"]
PROOF["confidential_swap / shielded_transfer proof"]
ENC["x25519 note encryption"]
end
subgraph Chain["⛓️ Soroban Shielded Pool"]
VERIFY["bn254 pairing_check (on-chain Groth16)"]
TREE["Poseidon commitment tree"]
NULL["Spent nullifiers"]
end
subgraph Backend["⚙️ ShieldPass Relayer"]
IDX["Tree indexer + merkle_insert prover"]
PAY["Paystack / Lenco"]
BLOB["Encrypted note blobs"]
end
SK -->|"prove locally"| PROOF
PROOF -->|"verified on-chain"| VERIFY
VERIFY --> TREE
VERIFY --> NULL
IDX -->|"advance root trustlessly"| TREE
ENC -->|"deliver to recipient"| BLOB
Backend -->|"naira"| PAY
Sending to another ShieldPass user keeps everything hidden, like a locked mailbox with no names:
- You encrypt the note details to the recipient's published encryption key (x25519 ECDH + XChaCha20-Poly1305) and submit the
shielded_transfer. - The funds never leave the pool — your note is nullified; the recipient gets a new note; nothing public is revealed.
- The recipient's app scans new encrypted blobs, trial-decrypts with their key, finds the ones meant for them, and updates their shielded balance — automatically, with a notification.
Sending to an external
G…/C…wallet instead unshields (the funds become public on arrival). Sending to a ShieldPass user stays fully shielded.
A tiered compliance model so small swaps stay frictionless while large ones stay regulated:
| Tier | Gate | Unlocks |
|---|---|---|
| Tier 1 | Passkey hardware attestation (hardware_attested) |
Everyday swaps |
| Tier 2 | BVN verification (bvn_verified) |
High-value swaps (> ₦1,000,000) |
The same circuit enforces both. A public input require_bvn is set by the backend from the swap's naira value; the contract rejects a high-value withdrawal that didn't use a Tier-2 proof. Compliance attributes are bound into the note.
No seed phrases, no extensions, no XLM required. Each user gets an OpenZeppelin Smart Account secured by a WebAuthn passkey; transactions are submitted gaslessly via the relayer. Your shielded key (which owns your private notes) is derived from the passkey's PRF — so resetting your login PIN never touches your private funds.
| Layer | Tech |
|---|---|
| ZK Circuits | Circom + snarkjs Groth16, Poseidon (BN254) |
| On-chain verification | Soroban native bn254 pairing-check (Rust contract) |
| Smart Contract | Rust / Soroban — Shielded Pool (deposit · insert · confidential_swap · unshield · shielded_transfer · claim · refund), one instance per supported asset |
| Note encryption | x25519 + XChaCha20-Poly1305 (@noble) |
| Wallets | OpenZeppelin Smart Accounts (WebAuthn passkeys / secp256r1) |
| Gasless Relay | OpenZeppelin Channels |
| Backend | Node, Express, Prisma 7 + Neon Postgres |
| Frontend | React, Vite, Tailwind, Framer Motion |
| Fiat Payouts | Paystack + Lenco (stateless / zero-storage) |
- Network: Stellar / Soroban testnet
- XLM Shielded Pool:
CBHGVQ7UH4C47LFUYFFKZMRI7AYUQ4TGYNIKX37KFKGBFGECKQM7BEG5— token: native XLM SAC - USDC Shielded Pool:
CCRQD3ALTYX5GMSQA5GDN65CF3DPLLHPWOOKHQOKRDO3JNK3FVYDM7ZP— token: testnet USDC SAC - Wallets: OpenZeppelin smart-account-kit, shared pre-deployed account wasm + WebAuthn verifier on testnet (gasless submission via OpenZeppelin Channels)
Prerequisites: Node 20+ (Prisma 7 requires ≥ 20.19), Rust + stellar CLI (for the contract), circom (only to regenerate circuits), a backend .env, and a frontend .env.
# Backend — prisma generate is required (the v7 client is gitignored)
cd backend && npm install && npx prisma generate && npm run dev # http://localhost:3001
# Frontend
cd frontend && npm install && npm run dev # http://localhost:5173Build & test the Soroban Shielded Pool:
cd SDK/contracts/shielded_pool && cargo test # contract tests (real proofs)
stellar contract build # wasm for deployRegenerate circuits + proving keys (only if you change a circuit):
cd SDK/circom && npm install
bash scripts/build.sh confidential_swap
bash scripts/build.sh merkle_insert
bash scripts/build.sh shielded_transferKey environment variables:
| File | Var | Purpose |
|---|---|---|
backend/.env |
NEON_CONNECTION_STRING |
Neon Postgres direct URL (?sslmode=require, no channel_binding) |
backend/.env |
STELLAR_CONTRACT_ID / USDC_POOL_CONTRACT_ID |
XLM / USDC Shielded Pool contract ids |
backend/.env |
XLM_SAC_ADDRESS / USDC_SAC_ADDRESS |
Stellar Asset Contract addresses backing each pool |
backend/.env |
STELLAR_RELAYER_SECRET |
Admin/relayer keypair (pool init, faucet seeding) |
backend/.env |
CHANNELS_URL / CHANNELS_API_KEY |
OpenZeppelin Channels — required for gasless smart-account submission |
backend/.env |
CORS_ORIGIN |
Comma-separated allowed frontend origin(s); unset = allow all (dev only) |
backend/.env |
PAYSTACK_SECRET_KEY / LENCO_* |
Naira payout providers |
backend/.env |
FAUCET_NOTE_AMOUNT / FAUCET_NOTE_ASSET |
Onboarding seed note (defaults 500 / XLM) |
backend/.env |
SEED_TOKENS |
Fallback public-funding for brand-new smart wallets (comma-separated contractId:amount) |
frontend/.env |
VITE_API_URL |
Backend URL (powers swaps, transfers, scanning, notifications) |
frontend/.env |
VITE_ACCOUNT_WASM_HASH / VITE_WEBAUTHN_VERIFIER_ADDRESS |
smart-account-kit account wasm + WebAuthn verifier contract |
frontend/.env |
VITE_XLM_POOL_CONTRACT_ID / VITE_USDC_POOL_CONTRACT_ID |
Per-asset Shielded Pool contract ids |
frontend/.env |
VITE_XLM_SAC / VITE_USDC_SAC |
Per-asset SAC addresses (drives which assets show up in the UI) |
See LICENSE.



