🎓 National University of Singapore · School of Computing

Design and Evaluation of a Privacy‑Preserving Decentralised Exchange Using Zero‑Knowledge Proofs

Poh Say Keong

Zero-Knowledge ProofsSP1 zkVMSolidity / RustDeFi
Overview

Agenda

01

Problem & Motivation

Why DEX privacy matters

02

Research Objectives

5 key questions addressed

03

System Design

Architecture & protocol

04

ZK Proof Architecture

Three SP1 programs

05

Evaluation & Results

Benchmarks vs Uniswap V3

06

Conclusion

Limitations & future work

Background

What is a Decentralised Exchange?

TRADITIONAL EXCHANGE (CEX)

  • Central operator holds custody of funds
  • Order book managed off-chain
  • Requires KYC and trust in operator

DECENTRALISED EXCHANGE (DEX)

  • Smart contract holds custody and no operator is required
  • Trades settled on-chain, trustlessly
  • Uses AMM: liquidity pools replace order books

AMM price formula

(Automated Market Maker)

x · y = k

Constant product maintained as tokens are swapped between two reserves

Uniswap V3

Dominant DEX · $2.7T cumulative volume

Background

AMM Example: x · y = k

INITIAL POOL STATE

Reserve X (ETH)

100

Reserve Y (USDC)

200,000

k = x · y

20,000,000

User swaps in

+2,000 USDC

AFTER SWAP

New Reserve Y

200,000 + 2,000 = 202,000

New Reserve X

20M ÷ 202,000 = 99.01

ETH received

100 − 99.01 = 0.99 ETH

Effective price

$2,020 / ETH

Larger trades move further along the curve, paying a higher price. This is price impact.

SLIPPAGE

The difference between the expected price and the actual price paid. In this example: expected $2,000/ETH, paid $2,020/ETH — 1% slippage.

Problem

Every DEX Trade is
Permanently Public

👁️

Order Size

Visible in mempool before confirmation

📊

Execution Price

Slippage tolerance exposed on-chain

🔗

Identity

Wallet addresses linkable over time

Enables front-running, MEV extraction, and statistical deanonymisation

Problem

Front-Running & MEV

SANDWICH ATTACK

  • Monitor mempool for large pending trades
  • Insert buy with higher gas (execute first)
  • Victim executes at worse price
  • Attacker sells for guaranteed profit

BLOCK TRANSACTION ORDER

1

Attacker BUY

high gas price ↑

2

Victim SWAP

pays inflated price

3

Attacker SELL

guaranteed profit ✓

$1B+

Extracted from retail users

$21B → $1.55T

DeFi market 2024 → 2034

Research

Research Objectives

Q1

Hide individual trade details while maintaining CFMM correctness

Q2

Achieve user anonymity and unlinkability across all protocol phases

Q3

Verify state transitions on-chain without revealing private inputs

Q4

Can privacy be achieved at lower cost than Uniswap V3?

Q5

Characterise the latency/throughput trade-off for batch settlement

Solution

Sealed-Bid Batch DEX

🔒

Sealed Orders

Encrypted under committee key until batch closes

⚖️

Unified Price

All orders clear at the same price, no MEV

ZK Settlement

On-chain proof verifies correctness, hides data

Key principle: The prover is a convenience, not an authority. A dishonest prover can only delay; it cannot forge.

Cryptographic Foundation

Zero‑Knowledge Proofs & SP1

What is Zero-Knowledge?

A prover convinces a verifier that a statement is true without revealing why it is true.

Analogy

Proving you know a password without typing it, the verifier learns only "correct" or "incorrect".

  • Completeness: honest proofs always verify
  • Soundness: false proofs cannot verify
  • Zero-knowledge: verifier learns nothing else
Succinct Labs

How SP1 Works

SP1 is a zkVM (zero-knowledge virtual machine) by Succinct Labs. Write normal Rust; SP1 generates a ZK proof of correct execution.

1

Write program logic in Rust (no circuit DSL)

2

SP1 compiles to a RISC-V trace and proves it with Groth16

3

On-chain verifier checks a 260-byte proof in constant gas

Result: complex off-chain computation with on-chain trust

Architecture

Three-Layer Architecture

⛓️

On-Chain · PrivacyDEX.sol

ERC-20 vault · Merkle root storage · Spent nullifiers · SP1 Groth16 verifier

SolidityFoundry
🖥️

Off-Chain · Prover

Aggregates orders · Runs CFMM batch · Generates SP1 ZK proofs

RustSP1 zkVMCUDA
👤

User Layer

Deposits/withdraws on-chain · Submits encrypted orders off-chain · Local note storage

TypeScriptethers.js
Cryptography

Cryptographic Foundation

Two-Hop Key Derivation

// Identity chain
ownerPublic = H(ownerSecret)
recipientPattern = H(ownerPublic || salt)
nullifier = H(ownerSecret || salt)

// Token-bound note commitment
noteCommitment = H(value || token || recipientPattern)

Without ownerSecret and salt, observers cannot link any deposit to its output — forward unlinkability.

Incremental Merkle Tree

Tree Depth

20

Max Notes

2²⁰ ≈ 1M

On-chain storage

Root only

Hash function

Keccak256

Protocol

End-to-End Protocol Flow

1

Deposit

Token transferred to vault; note commitment stored (pending state)

2

Order Submission

Encrypted order + nullifier ZK proof sent to prover queue

3

Batch Matching (off-chain)

Prover computes CFMM batch; generates Groth16 proof

4

Batch Settlement (on-chain)

Contract verifies proof; updates Merkle root & reserve commitment

5

Withdrawal

User generates proof locally; redeems tokens without prover involvement

Stage 1

Deposit Phase

  1. User calls deposit(amount, token, recipientPattern) on the vault contract
  2. Contract takes custody of tokens
  3. Note commitment computed and placed in pending pool
  4. Contract emits Deposit(noteCommitment, token, amount)
// Two-hop key derivation
ownerPublic = H(ownerSecret)
recipientPattern = H(ownerPublic || salt)

// Token-bound note commitment
noteCommitment = H(value || token || recipientPattern)

Note sits in pending pool; Merkle root unchanged until batch settles.

Stage 1 — Deep Dive

Why the Contract Generates the Commitment

If the user computed it

deposit(noteCommitment)

Alice deposits 1 ETH but submits a commitment for 100 ETH:

// Alice deposits 1 ETH but lies
noteCommitment = H(100 || ETH || recipientPattern)

The ZK circuit only sees the commitment; it cannot know the real deposit was 1 ETH. Alice withdraws 100 ETH.

Contract computes it

deposit(amount, token, recipientPattern)

The contract receives 1 ETH and commits to exactly that:

// Contract commits to the real amount
noteCommitment = H(1 || ETH || recipientPattern)

The circuit asserts the withdrawal amount matches the committed value. Alice can only withdraw 1 ETH.

Stage 1 — Deep Dive

Why a Pending Pool?

Deposits arrive continuously. The Merkle root is a single on-chain value. Two concurrent insertions would race to update it and one would overwrite the other.

Without pending pool

  1. Alice deposits → tries to update root
  2. Bob deposits → also tries to update root
  3. One transaction overwrites the other
  4. One note is lost

With pending pool

  1. Alice and Bob both deposit into the pool
  2. Root stays unchanged
  3. Batch settlement inserts all pending notes atomically
  4. No race condition

The ZK proof for batch settlement commits to the full set of pending insertions, so the Merkle root transition is verified in one atomic on-chain step.

Stage 2

Order Submission

What the user submits

Encrypted order (under committee key)

Order = {
  token_in, amount_in, inputNotes[],
  token_out, min_output, recipientPattern
}

Each inputNote

InputNote = {
  value, token, leafIndex,
  merklePath[], nullifier
}

Nullifier ZK proof (SP1 Nullifier Program)

Proves ownership of a committed note without revealing which one

// private inputs
IN: ownerSecret, salt

// SP1 program checks
nullifier = H(ownerSecret || salt)
ownerPublic = H(ownerSecret)
recipientPattern = H(ownerPublic || salt)

// public outputs
OUT: nullifier, recipientPattern
Stage 2 — Deep Dive

Validating the Input Note

The prover receives the recipientPattern from the nullifier proof. It uses this to regenerate the note commitment and check it against the Merkle tree.

// 1. Regenerate commitment from order fields
expected = H(value || token || recipientPattern)

// 2. Verify Merkle inclusion
assert( MerkleVerify(expected, leafIndex, merklePath[], root) )

Both recipientPattern and nullifier are public outputs of the SP1 nullifier proof. The prover trusts them without ever seeing ownerSecret.

Stage 3

Batch Matching (Off-chain)

k = R_X · R_Y

// Loop until stable
loop {
  // recompute ΔX, ΔY from remaining orders
  R'_X = R_X + ΔX
  R'_Y = R_Y + ΔY
  // enforce invariant: pair each reserve with its minimum counterpart
  // option A: keep R'_X, set R'_Y = k / R'_X
  // option B: keep R'_Y, set R'_X = k / R'_Y
  // pick the option that leaves more leftover in the pool
  p* = R'_Y / R'_X
  // remove orders where slippage > tolerance; break if unchanged
}

All traders in the batch receive the same unified clearing price p*. Order sequencing cannot affect outcomes.

Stage 3 — Deep Dive

Slippage Filtering

Sort orders by slippage tolerance

O(n log n)

Binary search for the cut-off price

O(log n)

Orders are sorted so the binary search can find the cut-off in O(log n). Once p* is known, every order below the cut-off is excluded in one pass.

Example: USDC → ETH, original price 2000, p* = 2010

Order Max slippage Max price Filled?
Alice 5% 2100
Bob 2% 2040
Carol 0.5% 2010
Dave 0.1% 2002
Eve 0.05% 2001

Output: valid order set + Groth16 proof

SP1 Batch Settlement Program

Stage 4

Batch Settlement (On-chain)

Checks

assert caller is authorised prover
assert oldRoot == current merkleRoot
assert ZK proof is valid
assert each depositCommitment is in pending pool
assert each nullifier has not been seen before

State transitions

remove each depositCommitment from pending pool
mark each nullifier as spent
merkleRoot ← newRoot
reserveCommitment ← newReserveCommitment
Stage 5

Withdrawal Phase

What happens

  1. User generates withdrawal proof locally (no prover involved)
  2. Circuit re-derives recipientPattern and recomputes noteCommitment; asserts Merkle inclusion
  3. Circuit derives nullifier from ownerSecret and salt
  4. User submits proof to contract; contract checks nullifier not spent, then transfers tokens to recipient

Withdrawal program I/O

IN: ownerSecret, salt, value, token, leafIndex, merklePath[], merkleRoot, recipient

// SP1 program checks
assert MerkleVerify(noteCommitment, leafIndex, merklePath[], merkleRoot)
nullifier = H(ownerSecret || salt)

// public outputs
OUT: merkleRoot, nullifier, recipient, amount, token

Withdrawal is unlinkable from deposit: recipient address is only revealed at this step and is not tied to any on-chain history.

ZK Proofs

Three Independent SP1 Programs

① Nullifier Program

Proves correct key derivation. Generated by each user in parallel.

MODE

Compressed (recursive)
② Batch Settlement Program

Verifies CFMM, Merkle insertions, deposits. Recursively verifies n nullifier proofs.

MODE

Groth16 · ~260 bytes on-chain

ON-CHAIN COST

~270,000 gas (constant regardless of batch size)
③ Withdrawal Program

Proves note ownership & Merkle inclusion. No prover involvement required.

MODE

Groth16
Privacy

Reserve Privacy

UNISWAP V3

  • R_X and R_Y always public
  • Pool size and implied price visible to all

THIS SYSTEM

  • Only H(R_XR_Y) stored on-chain
  • ZK circuit verifies CFMM invariant holds
  • Actual reserves never revealed publicly
// On-chain: only hash stored
C_reserves = H(R_X || R_Y)

// ZK circuit asserts:
assert(H(R_X||R_Y) == C_old)
assert(R''_X * R''_Y == R_X * R_Y)
Security

What Does a BatchSettled Event Reveal?

❌ Visible to Observers

public

New Merkle root

public

Count of notes spent

public

Which deposits confirmed

public

Reserve commitment hash

✓ Hidden from Observers

hidden

Individual commitment values

hidden

Which notes were spent

hidden

Trade amounts per trader

hidden

Actual reserve values R_X, R_Y

Evaluation

Gas Cost per Order vs Batch Size

$0.074

per order (N=128)

$0.375

Uniswap V3 per trade

80%

gas reduction at N=128

N≈3

break-even point

1 gwei gas · ETH @ $2,500 · Fixed Groth16 verification (~270k gas) amortised across batch

Evaluation

This System vs Uniswap V3

MetricThis System (N=128)Uniswap V3
Gas cost per trade $0.074  (29,413 gas) ~$0.375  (150,000 gas)
Settlement latency ~53.8s + 1 block * ~12s (1 block)
Trade privacy ✓ Full ✗ None
MEV protection ✓ Structural ✗ Exposed
Front-running protection ✓ Sealed-bid ✗ Mempool visible

Trade-off: higher latency is acceptable for privacy-sensitive applications where minutes-scale finality is tolerable.

* Measured on the SP1 Prover Network

Evaluation

Proof Time: Sublinear Scaling

32× orders

→ only 5.4× more proof time

WHY SUBLINEAR

  • SP1 segment-level GPU parallelism
  • Fixed setup costs amortised

GPU cost

~$1/hour

NVIDIA L4 · <1% of per-order cost

Scalability

Layer 2 & Multi-Prover

Layer 2 Deployment (0.01 gwei)

N=128 cost/order

$0.00074

N=2 cost/order

$0.004

Gas is no longer the bottleneck as proof generation time becomes the binding constraint.

Pipelined Multi-Prover

Prover A
Collect
Prove
Settle
Prover B
Collect
Prove
Settle
Prover C
Collect
Prove
Settle
Time →

3× throughput with no additional trust assumptions

Limitations

Open Problems

🛡️

Operator Trust

Malicious prover can censor or delay, but cannot forge valid settlements

💰

Fee Transparency

Fees deducted out-of-circuit; in-circuit deduction needed for full verifiability

📈

Price Discovery

Hidden reserves create post-settlement arbitrage opportunity

Future Work

Research Roadmap

🌐

Permissionless Proving

Any party submits valid proofs, eliminates trusted prover assumption

🔧

In-Circuit Fees

Deduct fees inside ZK circuit for full verifiability

Multi-Prover Protocol

Formal pipelining protocol; continuous batches

🌳

Larger Merkle Trees

Beyond depth-20 for production scale

Conclusion

Privacy-Preserving DeFi is Economically Viable

80%

gas reduction vs Uniswap V3 at N=128

cheaper per trade with full privacy

0

order-sequencing MEV (structural)

  • Sealed-bid batch DEX with unified clearing price means that front-running is structurally impossible
  • Prover-as-convenience: dishonest prover can only delay, never forge
  • Three-program SP1 design with recursive nullifier verification
  • $0.074/trade at N=128 with full cryptographic privacy guarantees

Thank you — Questions?

Poh Say Keong  ·  NUS School of Computing