The Inspiration

DeFi today has a transparency paradox. Protocols like Aave and Compound let anyone see your deposits, withdrawals, and balance — down to the dollar. For institutions and privacy-conscious users, this is a dealbreaker.

Midnight solves this with zero-knowledge proofs, but the question was: can you build a real DeFi vault that keeps individual balances hidden while still being verifiable?

That's what ShadowVault is — a proof that privacy-first DeFi is possible today.

What It Does

ShadowVault is a token vault on the Midnight blockchain where:

  • The TVL is public — everyone sees how much is in the vault
  • Individual balances are private — Alice and Bob use the same vault but can't see each other's balance
  • Withdrawals use real ZK proofs — funds move on-chain, but the caller's identity stays hidden
  • Deposits mint tokens to the vault — increasing the total supply on-chain

Every transaction returns a verifiable on-chain hash. The privacy isn't a promise — it's provable.

How We Built It

Smart Contract (Compact): The vault has four circuits — initialize (mints 10K SVT), fundUser (sends tokens to wallets), deposit (mints more tokens to the vault), and withdraw (sends tokens via ZK proof). Compact is Midnight's ZK-native language — TypeScript-like syntax that compiles into zero-knowledge circuits.

Backend (TypeScript/Node.js): A single deploy-and-serve.ts file deploys the contract, funds test personas, and serves the dashboard. The wallet provider wraps Midnight's SDK for transaction submission and state syncing.

Frontend (vanilla HTML/CSS/JS): A live dashboard at localhost:3030 pulls real-time contract state from the Midnight indexer via GraphQL. Persona switching demonstrates privacy — connect as Alice, see 200 SVT, switch to Bob, see 100 SVT. No traces left behind.

Local Devnet: All testing runs on Midnight's official midnight-local-dev — Docker containers running a full node, indexer, and proof server.

Challenges We Faced

1. Type system in Compact — The language is new and strict. Mismatched Uint widths (Uint<64> vs Uint<128>) caused compilation errors that took hours to debug. We learned to match every circuit parameter precisely with the standard library's type signatures.

2. Wallet-to-contract transfers — The wallet SDK's transferTransaction only supports user addresses (UnshieldedAddress), not contract addresses. We couldn't send tokens from a user's wallet to the vault. Our workaround: the deposit circuit uses mintUnshieldedToken with the same domain as initialization, creating more of the same token type on-chain. It's a novel approach the Compact standard library supports.

3. ZK proof generation time — Zero-knowledge proofs take ~30-60 seconds to generate on the proof server. We had to design the UI to handle this gracefully — showing a processing state and disabling buttons during proof generation.

4. DUST registration — Midnight's fee model requires a DUST coin in the wallet before any transaction. Starting fresh required resetting the devnet and LevelDB state. The error InvalidDustSpendProof (code 170) became a familiar sight.

What We Learned

  • Compact is genuinely ZK-native — privacy isn't bolted on. The language forces you to think about what's public (disclose()) and what's private (everything else).
  • Midnight's devnet is production-grade — full node, indexer, proof server, all in Docker. It's not a toy.
  • Privacy UX is different — every action needs to convey "your data is hidden" without confusing users. The terminal log clearing on persona switch was a design decision, not a technical one.

What's Next

  • Real deposits — wallet-to-contract transfers once the SDK supports it
  • Yield generation — deposited tokens earning yield through lending
  • Testnet deployment — moving from local devnet to public testnet

Built With

Share this project:

Updates