Latch

ZK-private batch auctions on Uniswap v4 — hidden orders, zero MEV, optional KYC compliance

Latch

Created At

HackMoney 2026

Project Description

Latch is a Uniswap v4 hook that makes trading private by design. Orders are submitted as blinded commitments, settled at a single ZK-verified clearing price, and never exposed to front-runners or sandwich bots. Optional compliance mode lets institutions participate through dual-root whitelist verification without sacrificing privacy guarantees.

The Privacy Problem: On-chain trading leaks information at every step. When you submit a swap, your intent is broadcast before execution — searchers exploit this asymmetry to extract value via front-running and sandwich attacks, costing traders over $1B annually. This isn't a bug in any protocol; it's a structural consequence of sequential, transparent order execution on public blockchains.

How Latch Eliminates Information Leakage: Commit Phase — Traders submit only a commitment hash. Order amount, price, and direction are completely hidden. All traders post identical token1 bonds, making buyers and sellers indistinguishable on-chain — even the trade direction is private. Reveal Phase — After the commit window closes, traders reveal order details. The contract verifies each reveal matches its commitment hash. No one can act on order information before all orders are collected. Settle Phase — An off-chain solver computes the uniform clearing price and generates a ZK proof (Noir circuit, UltraHonk proving system) that cryptographically guarantees: the clearing price maximizes matched volume, all fills respect limit prices, pro-rata allocation is correct, and every trader is whitelisted (in COMPLIANT mode). The contract verifies the proof on-chain — no trust in the solver required. Claim Phase — Traders withdraw their matched tokens and deposit refunds.

Key Privacy Innovations: Uniform bond deposits hide trade direction during commit (buyers and sellers look identical on-chain) ZK proof is the sole authority for settlement correctness — the contract only validates chain-state bindings Dual-root whitelist system: keccak Merkle proofs for on-chain commit access control, Poseidon Merkle proofs verified inside the ZK circuit for settlement — compliance without privacy leakage 25 public inputs (9 base + 16 fills) carefully partitioned between on-chain validated and proof-trusted values

Optional Compliance: Pools can operate in PERMISSIONLESS mode (open to all) or COMPLIANT mode with KYC whitelist verification. The dual-root design means compliance checks happen both on-chain (keccak proofs at commit) and inside the ZK circuit (Poseidon proofs at settlement), preserving privacy while satisfying regulatory requirements.

Technical Stack: Solidity 0.8.27 smart contracts (Uniswap v4 hooks, EIP-170 compliant), Noir ZK circuits (103 tests), UltraHonk proof system (no trusted setup), TypeScript solver with real-time ZK proof generation (~5s), Next.js frontend with privacy visualization. Deployed and E2E verified on Unichain Sepolia with real ZK proofs (2.98M gas settlement). 894 Solidity tests including fuzz and invariant testing. Latch proves that privacy and compliance can coexist — fair, private, verifiable trading for everyone.

How it's Made

Architecture Overview Latch is built as a Uniswap v4 hook that intercepts beforeSwap to redirect all trading through a batch auction system. The hook uses beforeInitialize to register pools and blocks direct swaps entirely — all trades flow through the 4-phase batch lifecycle. This is intentional: sequential swaps inherently leak information, so replacing them with batched settlement is the only way to eliminate MEV at the protocol level.

Smart Contract Stack (Solidity 0.8.27, via_ir, Foundry) The core is LatchHook.sol at 24,237 bytes — just 339 bytes under the EIP-170 limit. We built a modular architecture to stay within size constraints: LatchHook.sol — Core hook inheriting BaseHook + ReentrancyGuard + Ownable2Step, implementing commit/reveal/settle/claim BatchVerifier.sol — Wraps the auto-generated HonkVerifier with enable/disable and public input validation HonkVerifier.sol + RelationsLib.sol — ZK proof verifier split into two contracts (17.7KB + 10.3KB) via a post-compilation script (split_verifier.sh) to stay under EIP-170 EmergencyModule.sol — Batch start bonds, emergency timeouts, dual-token refunds for stuck batches SolverRewards.sol — Protocol fee distribution to solvers who submit valid proofs WhitelistRegistry.sol — Keccak Merkle proof verifier for on-chain commit access control LatchTimelock.sol — Time-delayed admin operations for module changes TransparencyReader.sol — Gas-intensive view functions kept separate to preserve LatchHook's size budget Storage is aggressively packed: PoolConfigPacked fits mode + 4 phase durations + feeRate into a single uint152. RevealSlot (1 storage slot: trader + isBuy) replaces the full Order struct (2 slots) during reveal — full order data is emitted via events and stored in a parallel _revealedAmountData mapping for solver reads. RevealDeposit packs depositAmount (uint128) + isToken0 (bool) into 1 slot.

Privacy-Preserving Commit-Reveal Orders are committed as: keccak256(abi.encodePacked(DOMAIN, trader, amount, limitPrice, isBuy, salt)) The critical privacy innovation is the dual-token deposit model. During commit, ALL traders (buyers and sellers) post identical token1 bonds — making them indistinguishable on-chain. You cannot tell if a commitment is a buy or sell order by looking at the deposit. During reveal, the trade direction is disclosed and the actual trade deposit is collected (buyers deposit token1, sellers deposit token0). This two-phase deposit design is what makes the privacy property meaningful — most commit-reveal schemes leak trade direction via which token is deposited.

ZK Circuit (Noir 1.0.0-beta + Barretenberg UltraHonk) The batch verifier circuit proves 9 properties in a single proof: Clearing price correctly maximizes matched volume (supply/demand intersection) Price optimality — no alternative price yields higher matched volume All fills respect limit prices (buyers pay ≤ limit, sellers receive ≥ limit) Pro-rata fill allocation is correct (constrained side gets proportional fills, unconstrained side gets full fills) Orders Merkle root matches committed set (Poseidon hashing for ZK efficiency) Whitelist Merkle root matches (COMPLIANT mode, also Poseidon) Fee rate is within bounds (≤ 10%) Protocol fee is correctly computed from matched volume All financial values are within u128 range (defense-in-depth) 25 public inputs (9 base + 16 per-order fills) are split between on-chain validated (batchId, orderCount, ordersRoot, whitelistRoot, feeRate, protocolFee) and proof-trusted (clearingPrice, buyVolume, sellVolume, fills). The contract validates chain-state bindings; the proof is the sole authority for settlement correctness. The circuit compiles to an UltraHonk proof (no trusted setup, 128-bit security). Proof generation takes ~5 seconds locally (nargo execute ~2s, bb prove ~3s). Proof size is 10,176 bytes with 800 bytes of public inputs.

Dual-Root Whitelist System (Hackiest Part) This was the trickiest engineering challenge. COMPLIANT mode needs whitelist verification both on-chain (at commit time) and inside the ZK circuit (at settlement). But Poseidon hashing is 8x cheaper than keccak inside ZK circuits, while keccak is native on the EVM. Our solution: two separate Merkle trees over the same address set. Keccak root — Stored in WhitelistRegistry, used for gas-efficient on-chain commit access control Poseidon root — Stored per-pool in LatchHook, snapshotted at batch start, passed as PI[6] to the ZK circuit The solver builds the Poseidon whitelist tree at settlement time using circomlibjs (compatible with Noir's bn254 Poseidon), generates per-trader Merkle proofs, and includes them as private inputs to the circuit. The circuit verifies each trader's inclusion proof against the Poseidon root. This gives us compliance verification inside the ZK proof without forcing expensive Poseidon precompiles at commit time.

TypeScript Solver The off-chain solver is a TypeScript service that watches for settleable batches and auto-generates ZK proofs: Polls chain state via eth_call only (no eth_getLogs) — this is critical for OP Stack L2 compatibility where the safe head lags thousands of blocks behind the chain tip Reads revealed orders via getRevealedOrderAt(i) view function (added specifically for L2 solver access) Computes clearing price via supply/demand intersection Computes pro-rata fills for imbalanced batches Builds Poseidon Merkle tree for ordersRoot (circomlibjs, padded to BATCH_SIZE=16 leaves) Generates Prover.toml, executes nargo execute + bb prove, parses proof artifacts Approves net token0 liquidity (only the gap between buy and sell fills) and submits settleBatch() The solver is the only component that needs to run off-chain. It's permissionless — anyone can run a solver and earn protocol fees via SolverRewards.

EIP-170 and Poseidon Size Challenges Poseidon precompile contracts (PoseidonT4 at 32KB, PoseidonT6 at 115KB) far exceed the 24KB EIP-170 limit. We handle this with --code-size-limit 200000 on both Anvil and forge broadcast. For the HonkVerifier (auto-generated from the circuit), we wrote split_verifier.sh to extract RelationsLib into a separate library contract — bringing both pieces under 24KB. Every time the proof is regenerated, this script must be re-run. We also added assembly ("memory-safe") annotations to the generated verifier for via_ir compatibility. The ordersRootValidationEnabled toggle lets us disable on-chain Poseidon cross-checking on chains where the precompiles can't be deployed. The ZK proof still verifies ordersRoot internally — the contract just doesn't double-check it.

Settlement Liveness What if no solver submits a proof? The EmergencyModule handles this: Batch starters post a bond in token1 when calling startBatch() If the batch settles successfully, the bond is refundable If no settlement occurs within EMERGENCY_TIMEOUT (1800 blocks, ~6 hours), anyone can activate emergency mode Emergency activation enables dual-token refunds: non-revealers get bond back (token1), revealers get bond + trade deposit back (correct token) A 1% penalty rate on emergency claims prevents frivolous activation forceUnpause() ensures pauses can't permanently lock funds (MAX_PAUSE_DURATION = 14,400 blocks, ~48 hours)

Phase Transitions All phases are block-based (block.number), not time-based. This is critical for L2s where block.timestamp can be unreliable. Current phase is computed from block.number relative to batch.startBlock and configured durations. On Unichain Sepolia we use 100-block phases (~100s each); on local Anvil, 5-block phases for fast iteration.

Frontend Next.js 14 dashboard with wagmi v2 and RainbowKit. The signature feature is a frosted-glass overlay on the "What Chain Sees" panel that dissolves as the batch progresses — backdrop-blur(12px) during COMMIT, decreasing through REVEAL and SETTLE, then fully transparent with a "ZK VERIFIED" badge after settlement. Real-time chain polling via useBlockNumber({ watch: true }) with multicall batching.

Testing 894 Solidity tests across 41 files (unit, integration, fuzz, invariant) + 103 Noir circuit tests + 11 E2E proof verification tests using the real HonkVerifier. Invariant tests cover fund conservation (deposits = claimables), phase monotonicity, and price protection. Cross-system hash compatibility tests verify Poseidon outputs match between Solidity, Noir, and TypeScript. Settlement gas: 2.93M (PERMISSIONLESS) to 2.98M (COMPLIANT) on Unichain Sepolia.

Deployed and E2E verified on Unichain Sepolia — both PERMISSIONLESS and COMPLIANT modes, with real ZK proofs, real token transfers, and full commit-reveal-settle-claim lifecycle.

background image mobile

Join the mailing list

Get the latest news and updates

Latch | ETHGlobal