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
- blockchain
- compact
- css3
- dapp
- defi
- docker
- graphql
- html5
- javascript
- midnight
- node.js
- typescript
- web3
- zk
Log in or sign up for Devpost to join the conversation.