DarkBook is a privacy-preserving on-chain orderbook DEX on Monad. Traders submit encrypted orders (price, size, direction) backed by ZK proofs that attest to order validity and balance sufficiency — without revealing order parameters. Orders are matched in a shared commitment pool, and settlement occurs via a reveal-and-verify mechanism. All ZK circuits are written in Noir (Aztec's DSL), verified on-chain via Solidity UltraPlonk verifiers, with a Next.js frontend.
- 10k+ TPS and 1s finality makes on-chain proof verification economically viable
- Parallel execution allows concurrent proof verification across multiple order settlements
- Sub-cent verification costs vs $5–15 on Ethereum L1
- EVM-compatible — deploy existing Solidity verifier infrastructure directly
┌─────────────────────────────────────────────────────────┐
│ Next.js Frontend │
│ Wallet Connect · Order Entry · Book Visualization │
└──────────────┬──────────────────────┬───────────────────┘
│ submit commitment │ query state
▼ ▼
┌──────────────────────┐ ┌─────────────────────────────┐
│ Off-Chain Prover │ │ Indexer / Event Listener │
│ (Noir + nargo) │ │ (WebSocket + Subgraph) │
└──────────┬───────────┘ └─────────────────────────────┘
│ proof + commitment
▼
┌─────────────────────────────────────────────────────────┐
│ Monad (EVM) │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ DarkBook │ │ Settlement │ │ Vault / │ │
│ │ Engine │ │ Verifier │ │ Escrow │ │
│ │ (commitments │ │ (UltraPlonk) │ │ (balance mgmt) │ │
│ │ + matching) │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
All privacy logic lives in Noir circuits compiled to UltraPlonk proofs.
Proves an order is valid and funded without revealing parameters.
Private Inputs:
- price: Field // limit price (scaled integer)
- amount: Field // order size
- side: Field // 0 = buy, 1 = sell
- balance: Field // user's deposited balance in vault
- salt: Field // randomness for commitment
- balance_proof: Field // Merkle path into balance tree
Public Inputs:
- commitment: Field // Pedersen(price, amount, side, salt)
- balance_root: Field // current vault Merkle root
- nullifier: Field // Hash(salt, user_address) — prevents double-submit
- token_pair_id: Field // which market this order targets
Constraints:
1. commitment == pedersen([price, amount, side, salt])
2. nullifier == pedersen([salt, sender_secret])
3. balance >= amount * price (if buy)
balance >= amount (if sell)
4. verify_merkle(balance, balance_proof, balance_root)
5. side ∈ {0, 1}
6. price > 0, amount > 0
Proves two commitments form a valid match (executed by the matcher/relayer).
Private Inputs:
- price_a, amount_a, side_a, salt_a // order A
- price_b, amount_b, side_b, salt_b // order B
- fill_amount: Field // how much is filled
Public Inputs:
- commitment_a: Field
- commitment_b: Field
- fill_amount_hash: Field
- settlement_price: Field
Constraints:
1. Recompute and verify both commitments
2. side_a != side_b (one buy, one sell)
3. price_a >= settlement_price >= price_b (if A=buy, B=sell)
OR price_b >= settlement_price >= price_a (if A=sell, B=buy)
4. fill_amount <= min(amount_a, amount_b)
5. fill_amount > 0
6. settlement_price == (price_a + price_b) / 2 // midpoint execution
Proves post-settlement balances are correct without revealing individual balances.
Private Inputs:
- old_balance_a, old_balance_b: Field
- fill_amount, settlement_price: Field
- side_a: Field
- merkle_path_a, merkle_path_b: Field[]
Public Inputs:
- old_root: Field
- new_root: Field
- commitment_a, commitment_b: Field
Constraints:
1. Verify old balances in old_root
2. Compute new balances based on fill
3. Verify new balances produce new_root
4. No underflow (balances >= 0)
Core orderbook logic — stores commitments, manages the order lifecycle.
State:
- mapping(bytes32 => OrderCommitment) public commitments;
- mapping(bytes32 => bool) public nullifiers;
- bytes32 public balanceRoot; // Merkle root of all vault balances
- uint256 public epochCounter;
Structs:
- OrderCommitment { bytes32 commitment, address owner, uint256 tokenPairId, uint256 epoch, Status status }
Functions:
- submitOrder(bytes32 commitment, bytes32 nullifier, uint256 tokenPairId, bytes proof)
→ verify order_commitment proof against on-chain balance root
→ store commitment, mark nullifier used
→ emit OrderSubmitted(commitment, tokenPairId, epoch)
- cancelOrder(bytes32 commitment, bytes32 salt, bytes proof)
→ prove ownership via salt knowledge (small circuit or signature)
→ mark commitment as cancelled, free nullifier
- settleMatch(bytes32 commitA, bytes32 commitB, uint256 fillAmount, uint256 price, bytes matchProof, bytes balanceProof)
→ verify match_proof
→ verify balance_update proof
→ update balanceRoot to new_root
→ emit MatchSettled(commitA, commitB, fillAmount, price)
Handles deposits/withdrawals with Merkle-tree balance tracking.
State:
- Incremental Merkle tree of balances (leaf = hash(address, token, amount))
- mapping(address => mapping(address => uint256)) public pendingDeposits;
Functions:
- deposit(address token, uint256 amount)
→ transferFrom user, update Merkle leaf, emit DepositProcessed
- withdraw(uint256 amount, address token, bytes32[] merkleProof, bytes zkProof)
→ verify user has balance via ZK proof (don't reveal full balance)
→ update Merkle tree, transfer tokens
- getBalanceRoot() → bytes32
Auto-generated from Noir circuit compilation via nargo codegen-verifier. One verifier per circuit (or a dispatcher pattern).
Functions:
- verifyOrderCommitment(bytes proof, bytes32[] publicInputs) → bool
- verifyMatch(bytes proof, bytes32[] publicInputs) → bool
- verifyBalanceUpdate(bytes proof, bytes32[] publicInputs) → bool
A permissioned (initially) or decentralized (later) off-chain service that:
- Listens for
OrderSubmittedevents - Has access to the encrypted order details (shared via encrypted channel or TEE)
- Runs matching algorithm (price-time priority)
- Generates
match_proofandbalance_updateproofs - Submits
settleMatchtransactions to Monad
Key design decision: The matcher sees plaintext orders to perform matching. Privacy is from other traders and on-chain observers, not from the matcher. Future upgrade path → MPC-based matching or FHE.
Architecture:
- Node.js/Rust service
- Encrypted order submission via ECDH shared secret (user ↔ matcher)
- In-memory orderbook (price-time priority, per pair)
- Proof generation via nargo CLI or Noir.js WASM prover
- Batched settlement every N seconds or M matches
- Listens to all DarkBook contract events
- Maintains a shadow state of:
- Active commitments (by token pair)
- Settlement history
- Vault balance roots
- Serves API to frontend via REST/WebSocket
- Can use a subgraph (The Graph) or custom indexer
| Layer | Tech |
|---|---|
| Framework | Next.js 14+ (App Router) |
| Styling | Tailwind CSS + shadcn/ui |
| Wallet | wagmi v2 + viem + RainbowKit |
| State | Zustand (orderbook state, user state) |
| ZK Prover | Noir.js (WASM in-browser proving) |
| Data | WebSocket for live orderbook, REST for history |
| Chain | Monad RPC via viem custom chain config |
/app
/page.tsx — Landing / hero
/trade/[pair]/page.tsx — Main trading interface
/vault/page.tsx — Deposit / withdraw
/history/page.tsx — User's trade history (decrypted locally)
/stats/page.tsx — Protocol stats (TVL, volume, matches)
/components
/orderbook
CommitmentBook.tsx — Shows hashed commitments (no price/size visible)
DepthVisualization.tsx — Aggregated depth hints (optional, partial reveal)
OrderEntryForm.tsx — Price, amount, side → generates proof client-side
ActiveOrders.tsx — User's own orders (decrypted locally)
/vault
DepositForm.tsx
WithdrawForm.tsx
BalanceDisplay.tsx — Shows user's private balance (local decryption)
/shared
ProofStatusIndicator.tsx — Proving... / Verified / Failed
TransactionTracker.tsx — Tracks on-chain tx status
WalletButton.tsx
/layout
Navbar.tsx
Footer.tsx
User enters order (price, amount, side)
→ Frontend generates random salt
→ Computes commitment = Pedersen(price, amount, side, salt)
→ Computes nullifier = Pedersen(salt, user_secret)
→ Fetches current balance_root from Vault contract
→ Fetches user's Merkle proof from indexer API
→ Invokes Noir.js WASM prover with all private/public inputs
→ Receives proof (UltraPlonk, ~2-5s in browser)
→ Submits (commitment, nullifier, tokenPairId, proof) to DarkBookEngine
→ Simultaneously sends encrypted order details to Matcher via API
→ UI shows "Order Committed" with proof status
1. Matcher publishes an ephemeral ECDH public key
2. User derives shared secret: ECDH(user_privkey, matcher_pubkey)
3. User encrypts {price, amount, side, salt} with AES-256-GCM(shared_secret)
4. Sends ciphertext to Matcher API alongside on-chain commitment tx hash
5. Matcher decrypts, verifies commitment matches, adds to internal book
darkbook-circuits/
├── Nargo.toml
├── src/
│ ├── main.nr — entry point (selector for circuit type)
│ ├── order_commitment.nr — order validity circuit
│ ├── match_proof.nr — match verification circuit
│ ├── balance_update.nr — post-settlement balance circuit
│ └── lib/
│ ├── merkle.nr — Merkle tree verification helpers
│ ├── pedersen.nr — commitment scheme wrappers
│ └── utils.nr — field arithmetic, range checks
├── provers/
│ └── Prover.toml
└── verifiers/ — auto-generated Solidity verifiers
use dep::std;
fn main(
// Private
price: Field,
amount: Field,
side: Field,
balance: Field,
salt: Field,
balance_leaf_index: Field,
balance_merkle_path: [Field; TREE_DEPTH],
// Public
commitment: pub Field,
balance_root: pub Field,
nullifier: pub Field,
token_pair_id: pub Field,
) {
// 1. Verify commitment
let computed_commitment = std::hash::pedersen([price, amount, side, salt]);
assert(computed_commitment[0] == commitment);
// 2. Verify nullifier
let computed_nullifier = std::hash::pedersen([salt, balance_leaf_index]);
assert(computed_nullifier[0] == nullifier);
// 3. Side must be 0 or 1
assert((side == 0) | (side == 1));
// 4. Price and amount must be positive
// (range check: ensure < 2^126 to prevent overflow)
assert(price as u126 > 0);
assert(amount as u126 > 0);
// 5. Balance sufficiency
if side == 0 {
// Buy: need balance >= amount * price
let required = amount * price;
assert(balance as u126 >= required as u126);
} else {
// Sell: need balance >= amount
assert(balance as u126 >= amount as u126);
}
// 6. Verify balance in Merkle tree
let leaf = std::hash::pedersen([balance_leaf_index, balance]);
let computed_root = compute_merkle_root(leaf[0], balance_leaf_index, balance_merkle_path);
assert(computed_root == balance_root);
}# Compile circuit
nargo compile
# Generate Solidity verifier
nargo codegen-verifier
# Run prover (for testing)
nargo prove
# Verify proof locally
nargo verifyPrice-Time Priority (continuous matching):
For each token pair:
- Maintain sorted buy orders (descending by price, then ascending by timestamp)
- Maintain sorted sell orders (ascending by price, then ascending by timestamp)
On new order arrival:
1. Check if crossing orders exist on the opposite side
2. If buy_price >= best_ask OR sell_price <= best_bid → match
3. Settlement price = midpoint of the two limit prices
4. Fill amount = min(buy_amount, sell_amount)
5. Generate match_proof + balance_update proofs
6. Submit settlement batch to Monad
7. Update remaining quantities, re-insert partial fills
- Batch settlements every 2 seconds (aligns with Monad block time)
- Max 16 matches per batch (gas optimization)
- Priority: largest fill amounts first (maximize volume)
- Each batch = 1 transaction with multiple proof verifications
- Monad's parallel execution handles concurrent batch verification
Goal: Noir circuits compile, Solidity verifiers deploy on Monad testnet, basic vault works.
- Initialize Noir project, implement
order_commitmentcircuit - Implement Pedersen commitment and Merkle tree helpers in Noir
- Compile circuit, generate UltraPlonk Solidity verifier
- Write
Vault.solwith deposit/withdraw and Merkle root tracking - Write
DarkBookEngine.solwithsubmitOrder(verify proof on-chain) - Deploy to Monad devnet/testnet
- Unit tests: circuit constraints, contract logic, proof verification gas costs
- Benchmark: proof verification gas on Monad vs Ethereum estimates
Goal: Off-chain matcher runs, matches settle on-chain with ZK proofs.
- Implement
match_proofcircuit in Noir - Implement
balance_updatecircuit in Noir - Build matcher service (Node.js or Rust)
- ECDH key exchange for encrypted order relay
- In-memory orderbook with price-time priority
- Proof generation pipeline (nargo CLI or Noir.js)
- Implement
settleMatchinDarkBookEngine.sol - Batched settlement transactions
- Integration tests: full flow from order submission → match → settlement
- Event indexer (basic) for tracking commitments and settlements
Goal: Functional Next.js trading interface with client-side proving.
- Scaffold Next.js app with wagmi, viem, RainbowKit
- Monad chain configuration in viem
- Vault pages: deposit, withdraw, balance display
- Order entry form → client-side Noir.js WASM proof generation
- Commitment book display (hashed commitments, no plaintext)
- Active orders panel (user's own, decrypted locally)
- Proof status indicator (generating / submitted / verified)
- WebSocket connection to indexer for live updates
- ECDH encrypted order relay to matcher API
- Trade history page
Goal: Production-grade reliability, gas optimization, polished UX.
- Gas optimization: batched verifier, calldata compression
- Order cancellation flow (with ZK proof of ownership)
- Partial fill handling in UI and matcher
- Error handling: failed proofs, reverted txs, matcher downtime
- Rate limiting and anti-spam (commitment bonds)
- Loading states, animations, dark-mode polish
- Mobile responsive layout
- Proof generation progress bar (WASM proving takes 2-5s)
Goal: Competitive feature set, decentralization path.
- Multiple trading pairs
- Market orders (taker orders that match immediately at best price)
- Aggregated depth visualization (partial privacy: reveal aggregate volume at price bands without revealing individual orders)
- MEV protection: encrypted mempool via threshold decryption
- Matcher decentralization: multiple matchers with stake-based selection
- Governance token and fee distribution
- Audit preparation (circuits + contracts)
| Risk | Mitigation |
|---|---|
| Soundness bugs in Noir circuits | Formal verification where possible, extensive fuzzing, audit |
| Commitment malleability | Use Pedersen with domain separation tags |
| Nullifier reuse / replay | On-chain nullifier set, per-epoch scoping |
| Balance proof staleness | Enforce balance_root freshness (must match latest on-chain root) |
| Prover-side info leaks (timing) | Constant-time Noir operations, pad proof generation time |
- Current: Trusted matcher sees plaintext orders. Trust assumption is no front-running.
- Mitigation v1: Matcher posts a bond; slashable if proven to front-run (via fraud proofs).
- Mitigation v2: Threshold decryption — order encrypted to N-of-M committee, matcher only sees plaintext after K confirmations.
- Mitigation v3: MPC-based matching (long-term) — no single party sees plaintext.
- Re-entrancy on settlement → use checks-effects-interactions + ReentrancyGuard
- Merkle root manipulation → only updateable via verified proofs
- Front-running of settlement txs → Monad's parallel execution + matcher-submitted txs with flashbots-style private submission if available
Unit Tests:
- Noir circuits: nargo test (constraint satisfaction, edge cases, overflow)
- Solidity contracts: Foundry (forge test)
- Matcher: Jest/Vitest (matching logic, batching, edge cases)
Integration Tests:
- Full flow on local Monad fork (Anvil-compatible)
- Submit order → verify on-chain → match → settle → check balances
- Concurrent order submission stress test
- Proof verification gas benchmarks
E2E Tests:
- Playwright/Cypress for frontend flows
- Client-side proving → on-chain submission → UI update
- Wallet connection, deposit, trade, withdraw cycle
Fuzzing:
- Noir circuit fuzzing with random inputs
- Foundry fuzz tests for contract edge cases
- Matcher: random order streams, adversarial pricing
darkbook/
├── circuits/ — Noir ZK circuits
│ ├── Nargo.toml
│ ├── src/
│ │ ├── order_commitment.nr
│ │ ├── match_proof.nr
│ │ ├── balance_update.nr
│ │ └── lib/
│ └── tests/
├── contracts/ — Solidity smart contracts
│ ├── foundry.toml
│ ├── src/
│ │ ├── DarkBookEngine.sol
│ │ ├── Vault.sol
│ │ ├── verifiers/ — auto-generated from Noir
│ │ └── libraries/
│ ├── test/
│ └── script/ — deployment scripts
├── matcher/ — Off-chain matching engine
│ ├── src/
│ │ ├── orderbook.ts
│ │ ├── prover.ts
│ │ ├── settler.ts
│ │ ├── encryption.ts — ECDH + AES-GCM
│ │ └── indexer.ts
│ └── tests/
├── frontend/ — Next.js application
│ ├── app/
│ ├── components/
│ ├── lib/
│ │ ├── noir/ — WASM prover integration
│ │ ├── contracts/ — ABIs + addresses
│ │ ├── encryption/ — Client-side ECDH
│ │ └── stores/ — Zustand stores
│ └── public/
│ └── circuits/ — Compiled circuit artifacts for WASM prover
├── docs/
│ ├── architecture.md
│ ├── circuits.md
│ └── api.md
└── README.md
| Component | Package | Version |
|---|---|---|
| ZK Circuits | Noir (nargo) | 0.30+ |
| Browser Prover | @noir-lang/noir_js | latest |
| WASM Backend | @noir-lang/backend_barretenberg | latest |
| Contracts | Foundry (forge) | latest |
| Frontend | Next.js | 14+ |
| Wallet | wagmi + viem | v2 |
| Wallet UI | RainbowKit | latest |
| Styling | Tailwind + shadcn/ui | latest |
| State | Zustand | 4+ |
| Matcher | Node.js / Bun | 20+ / 1+ |
| Encryption | @noble/ciphers, @noble/curves | latest |
| Indexer | ethers.js or viem | latest |
- Order submission: < 5s total (prove + submit + confirm on Monad)
- Proof verification gas: < 300k gas per order proof on Monad
- Match-to-settlement latency: < 4s (matcher detects cross → settles on-chain)
- Concurrent throughput: 50+ orders/minute without degradation
- Zero information leakage: On-chain observer cannot determine price, size, or direction of any uncommitted order
- Clean demo flow: Deposit → Submit private order → See commitment appear → Counter-order matches → Settlement verified → Balances updated — all without revealing order details publicly