π Overall Winner β MLH Midnight Hackathon, July 2026
NightPool lets big traders place orders nobody can see β then prove any trade to a regulator on demand. Private to the market, provable to an auditor.
Trade in the dark. Settle in the light. Two venues on Midnight: a dark RFQ desk and a sealed-bid batch auction.
"Given recent events, now may be a good time for someone to launch a dark-pool perpetuals DEX." β CZ, June 2025, after a trader's billion-dollar positions were liquidation-hunted on Hyperliquid
βΆ Live demo: https://nightpool.vercel.app β fully interactive (mock data, no wallet needed) Β· πΉ Video: https://youtu.be/QMrUhcGt4uc (2-min walkthrough)
The hosted demo runs on mock data so anyone can click through instantly. The contracts are real and verified on a local Midnight devnet β see Run it to drive the UI against live ZK proofs through the bridge.
- On the Maker tab, deposit and rest a hidden order β watch the public chain view on the right show only an opaque commitment, and the resting book below it: your row readable, every other order sealed. The count is all the chain reveals.
- Copy the RFQ ticket, switch to the Taker tab, paste it, and fill β the explorer still shows nothing.
- Open the Auditor tab, paste the disclosed record, and hit Verify β green β against the on-chain fingerprint. Tamper one digit β NO MATCH.
- Or try the Auction Pool: submit a sealed bid as both personas and watch the round clear at one uniform price.
NightPool is a hidden-order trading venue built for the MLH Midnight Hackathon (July 2026, DeFi track). Order size, price, and counterparties never touch the public ledger β only opaque cryptographic commitments do β yet every trade is mathematically verified by zero-knowledge proofs, and every fill leaves an on-chain audit fingerprint: voluntarily disclose the trade record to your auditor and they can verify it against the chain; disclose nothing and it stays dark forever. That trade-off is the point: this is privacy a regulator can live with, which is exactly the thesis of the Midnight network.
On every public DEX, your trading is a broadcast:
- Whales get hunted. Position sizes and entry prices are public, so liquidation levels become targets (the James Wynn/Hyperliquid saga).
- Strategies get copied. Nansen/Arkham-style trackers reverse-engineer proprietary strategies from on-chain history in real time.
- Blocks get front-run. Telegraphing a large order moves the price before it fills.
Traditional finance solved this decades ago with dark pools β but crypto's versions either centralize custody or leak metadata. Renegade and GSR's confidential OTC desk prove institutional demand is real; Midnight's own ecosystem wishlist requests exactly this (contributor-hub issue #92: Hidden Order Dark Pool DEX).
1. The RFQ desk β bilateral hidden-order block trading (below).
2. The sealed-bid batch auction β the anonymous venue. Traders submit sealed bids: side, size, and limit price live inside hiding commitments and β unlike classic commit-reveal auctions β are never revealed on-chain, not even after clearing. Every ~45s the operator clears the round at one uniform price; the public ledger learns that price and aggregate counts, nothing else. Each trader then claims with a ZK proof of dual Merkle membership (my bid was in the round β§ marked filled/refunded) plus a division-free check that their settlement equals amount Γ clearingPrice exactly β the operator can decide who matches, but can never bend the price or amounts. Front-running and copy-trading aren't discouraged β there is nothing to see and no ordering to game. (The Penumbra batch-auction insight, built natively on Midnight's Kachina model.)
Three personas on the RFQ desk, one contract per venue:
| Persona | What they do | What they see |
|---|---|---|
| Maker | Shields funds into the pool, rests a hidden order, shares an RFQ ticket off-chain, claims payout after a fill | Their own notes and orders |
| Taker | Pastes the RFQ ticket, verifies terms, fills β a ZK proof settles both legs atomically | The quoted terms, nothing else |
| Auditor | Receives a voluntarily disclosed trade record and verifies it against the on-chain audit fingerprint | Exactly what was disclosed, cryptographically bound to the chain |
The public ledger sees: an opaque commitment appear, a nullifier appear, a trade counter increment. No size. No price. No parties.
- Kachina dual state. Every circuit call proves "my private state change justifies this public state change" β order terms live client-side and are proven, never revealed.
- Hiding commitments + ZK membership proofs. Orders and balance notes go on-chain only as
persistentCommitvalues (hiding + binding, 32-byte randomness) appended to Merkle trees. The commitments themselves are public β what's protected is their contents, and spending proves membership against a (historic) root without revealing which leaf, so consumption is unlinkable to insertion. HistoricMerkleTreeaccepts proofs against past roots, so a fill doesn't race deposits landing after the order was placed.- Selective disclosure by construction. Every fill writes a salted fingerprint
H(TradeRecord β₯ salt)on-chain (salt derived from the order randomness). The salt travels inside the disclosed trade record, so an auditor you hand the record to can recompute and verify the fingerprint against the chain β while an outside observer, without the record, cannot. Disclose the record and it's verifiable; disclose nothing and it stays dark β and because each fingerprint is independently blinded, disclosing one trade reveals nothing about any other. Both sides of "rational privacy" in one primitive. - Shielded gas (DUST) hides fee amounts (though not submission timing or the submitting wallet β see limitations).
sequenceDiagram
participant M as Maker
participant C as NightPool contract (public ledger)
participant T as Taker
participant A as Auditor
M->>C: deposit(token, amt) β note commitment (tree insert, hidden)
M->>C: placeOrder() β ZK: spends note, escrows funds, inserts orderCommit
Note over C: chain shows: 1 nullifier, opaque commits, openOrders+1
M-->>T: RFQ ticket (order terms + commitment opening) β off-chain
T->>C: fillOrder() β ZK: proves terms match commitment, nullifies order,<br/>mints payout notes for both sides + audit fingerprint
Note over C: chain shows: nullifiers, opaque commits, settledTrades+1
M->>C: claimFills() β detects order nullifier, derives payout note locally
T-->>A: (optional) disclose TradeRecord
A->>C: verify H(TradeRecord) β auditFingerprints β
The RFQ handshake is the honest design: real OTC desks exchange quotes bilaterally. The public never sees terms; the counterparties obviously must. Possession of the commitment opening is the maker's authorization to settle β the circuit enforces settlement at exactly the committed terms, so a taker can't alter them. Blind order-crossing without even counterparty visibility requires MPC matching (Renegade's design) β that's on the roadmap, not fake-demoed here.
| Data | Public ledger | Counterparty | Auditor (after disclosure) |
|---|---|---|---|
| Order size / price | β (commitment) | β (RFQ quote) | β |
| Trader identity | β (per-persona trading pseudonym β a hash of a local secret, not a wallet address; per-trade rotation is roadmap) | pseudonym only | pseudonym only |
| That an event happened (circuit id, counters) | β (fills vs cancels are distinguishable; open-order and trade counts are exact public numbers β by design, they're the venue's public stats) | β | β |
| Pool entry/exit amounts | β (by design β like any shielded pool boundary) | β | β |
| That some trade settled | β (counter + fingerprint) | β | β |
| Full trade record | β | β | β if and only if disclosed |
contracts/nightpool.compact RFQ desk β 5 ZK circuits + 7 pure helpers
contracts/nightauction.compact sealed-bid batch auction β 9 ZK circuits + 5 pure helpers
(two contracts by necessity: each circuit ships a verifier
key in the deploy tx, and 14 keys exhausted the block limit.
the note/key helper circuits are duplicated across both files
on purpose β Compact contracts can't share code across
deploys, so each venue carries its own copy)
src/witnesses.ts private inputs: staged per-call, JSON-safe private state
src/nightpool-api.ts persona client: notes/orders wallet, RFQ tickets, path proofs
src/server.ts demo bridge (HTTP) β wallet + proving stay server-side/local
src/test/nightpool.test.ts full E2E: deposit β hidden order β RFQ fill β claim β audit β withdraw
ui/ React dark-desk UI (maker / taker / auditor + live public chain view)
Concurrency model: users never read-then-write a shared cell β all shared state is touched through commutative ledger ops (MerkleTree appends, Set inserts, Counter bumps), which Kachina merges without conflict; the one order-sensitive structure (the audit-fingerprint list) only ever appends. Stack: midnight-js 4.1.1, local proof server 8.1.0, midnight-node 1.0.0 + indexer 4.3.3 devnet, wallet-sdk 1.2.0.
Just want to click it? Open the live demo, or run the UI locally on mock data β no toolchain, no Docker:
cd ui && npm install && npm run dev # desk UI on :5173 (mock mode)Full stack (real ZK proofs on a local devnet):
# prerequisites: Node 22+, Docker, Compact toolchain
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
compact update
yarn install
yarn compile # REQUIRED before tsc/tests β builds circuits + ZK keys
# into the (gitignored) contracts/managed/ directory
yarn env:up # local devnet: node + indexer + proof server (docker)
yarn test:local # full E2E: RFQ desk 8/8, auction 6 (+1 documented skip),
# plus 6 pure-circuit unit tests β 20 passed | 1 skipped
# live demo (drive the UI against real proofs through the bridge)
MIDNIGHT_NETWORK=local npx vite-node src/server.ts # bridge on :8787
cd ui && VITE_BRIDGE_URL=http://localhost:8787 npm run dev # UI on :5173- Pool balances are demo accounting, not custodied value.
depositmints an internal note; it does not yet take custody of real shielded coins (ZswapreceiveShielded/sendShieldedescrow is the next milestone). Inside the pool every trade conserves value atomically; the pool boundary is where real custody would attach. - RFQ, not blind matching β counterparties see each other's quote (like real OTC), and an RFQ ticket is bearer authorization: whoever holds it can settle it (counterparty binding is roadmap). The taker also inherently knows the payout note they mint for the maker β the same way any payment sender knows what they sent; maker sweep-to-fresh-note is roadmap.
- The auction operator is trusted for matching, not for terms. It picks which crossing bids fill (exactly like a real dark-pool operator) and could decline to match; it cannot alter the clearing price, amounts, or steal escrow, and every claim is verified against the public price in-circuit. Proving clearing optimality in-circuit is the research roadmap.
- Auction sell-side settlement β known bug. The auction's full lifecycle is verified live on devnet β sealed bids, hidden uniform-price clearing, and buy-side claim settlement with exact refund math (see
src/test/auction.test.ts, 6 passing). The sell-side claim currently hits a ZKIR alignment error in the proof server (Inputs did not match alignment, a 31-vs-32-byte marshaling mismatch on the sell mint path) β deterministic and specific to that branch, not timing or contention. It's isolated and tracked; the sell settlement is the only auction path not yet green. The auction UI (fully working in mock) demonstrates the intended end-to-end flow. - Pseudonyms are per-persona, not per-trade β a trader's fills cluster under one trading key (rotation is a straightforward roadmap item). And in this demo one funding wallet submits all personas' transactions; in production each user's own wallet submits, which is what makes the walletβpseudonym unlink real.
- Event shapes are visible. Which circuit ran is public, so fills and cancels are distinguishable and the open-order/settled-trade counters are exact β we treat those as the venue's public stats, not a leak, but a maximally-private design would pad or batch them.
- The HTTP bridge is a demo shim β personas live server-side without auth; in production the persona is your wallet session.
- Pool boundary amounts are public β deposits/withdrawals show amounts (standard shielded-pool trade-off). Next: Zswap-native shielded escrow so even entry amounts are hidden.
- Reference pricing β midpoint-pegged execution via a witness-verified signed price feed (DIA) is designed but not wired.
- Wallet UX β a deliberate choice, not a limitation of the chain. Lace can target a local node via its Undeployed network, but cold-starting a browser wallet on a fresh devnet (genesis funding β DUST registration β indexer sync) is too fragile for a one-take live demo, so the desk signs through a server-side operator wallet against that same local chain β the wallet the live E2E proofs use. Lace via the
mnLaceDApp connector on Preview/Mainnet is the productionization path. - Compliance gating β OpenZeppelin Compact
Allowlist(ERC-3643-style) drops intodepositfor KYC-gated institutional pools. - Cross-chain settlement β effectstream orchestration to settle NightPool trades against assets on other networks.
- Timing correlation between RFQ sharing and fills is out of scope for a 48h build; batch windows mitigate it.
Built during the MLH Midnight Hackathon, July 17β19 2026, on Midnight devnet tooling. Market: tNIGHT/dUSD (mock USD, 6 decimals).
Built by Karan Singh Bisht (@KaranSinghBisht).
MIT β see LICENSE.
NightPool is a hackathon prototype. The contracts are unaudited; do not use with real funds.