Skip to main content

Architecture

The system provides three progressive levels of financial privacy, referred to in the reference implementation as L1, L2, and L3 (see nomenclature flaw):

  • Public layer (L1) — Standard ERC-20 tokens. Balances and transfers are fully visible on-chain, as with any token contract.
  • Encrypted layer (L2) — Balances are stored as ElGamal ciphertexts on-chain. Transfer amounts are hidden, but the transaction graph (who sends to whom) remains visible.
  • Anonymous layer (L3) — Funds are deposited into a shared commitment pool and withdrawn via nullifiers. Both amounts and the transaction graph are hidden.

Each privacy layer is implemented across three tiers of software: on-chain smart contracts in Solidity that enforce state transitions and verify proofs, zero-knowledge circuits in Noir that prove correctness of private operations, and client-side libraries in TypeScript that construct transactions and manage local state.

A relayer service sits between users and the blockchain. It abstracts away gas fees and removes the need for users to submit transactions from their own addresses — which would otherwise leak their identity on-chain.

For app developers, the system ships an SDK compatible with web, iOS, and Android (via React Native). Any application wishing to integrate privacy can use the SDK directly — it exposes a high-level interface that hides the complexities of proof generation, encrypted state synchronization, and transaction construction.

┌───────────────────────────────────────────┐
│ Application │
│ (uses SDK) │
└───────────────────┬───────────────────────┘

┌────────▼────────┐
│ SDK │
│ (TypeScript) │
└────────┬────────┘

┌───────────▼───────────┐
│ Relayer │
│ (gas relay, witnesses)│
└───────────┬───────────┘

┌────────────────▼────────────────────┐
│ Smart Contracts │
│ (Solidity — proof verification, │
│ balance storage, state transitions)│
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Noir Circuits │
│ (transfer, shield/unshield, │
│ stake, unstake) │
└─────────────────────────────────────┘

Compliance

To comply with European regulations, the system includes a compliance component. Although technically intertwined with the L2 and L3 layers — the encryption is enforced inside their circuits — it can be thought of as conceptually independent: everything works without it, and removing it would not break any privacy or correctness guarantees for end users.

The compliance mechanism relies on an auditor party (in practice, a quorum of guardians) that holds a special decryption key. Whenever a user makes a transaction that hides information — an amount, the origin of funds, etc. — the transaction emits an on-chain event containing that data encrypted under the auditor's public key. The auditor can then decrypt and trace funds when required by law, without any cooperation from the user.

For details on how this is implemented and its known issues, see Compliance.

Component Summary

Encrypted Balances (L2)

Users deposit ERC-20 tokens into the contract, which stores their balance as an ElGamal ciphertext on the Grumpkin curve (encrypted via the user's keypair). Transfers update the sender's ciphertext (proved correct by a Noir circuit) and add the transfer ciphertext to the recipient's balance via on-chain point addition — no decryption needed, thanks to the homomorphic property. The recipient decrypts locally using their spending key, which requires solving a discrete log. Because the contract checks the sender's current ciphertext against the proof, only one operation per user can be in flight at a time (see flaw #5). For details see Encrypted Balances.

Anonymous Layer (L3)

The anonymous layer breaks the transaction graph. Users deposit from L2 into a shared Merkle tree of commitments ("stake"), then later withdraw to any L2 account ("unstake") by proving knowledge of a valid commitment and revealing a nullifier — without linking the withdrawal to the original deposit. Nullifiers are tracked in an Indexed Merkle Tree (IMT) to prevent double-spending. The IMT design creates a fundamental tension between privacy and permissionlessness (see flaw #4), and concurrent unstakes are serialized by the nullifier root (see flaw #6). For details see Staking (L3).

Relayer

The relayer serves two roles. First, it sponsors gas and submits transactions on behalf of users, so that users don't need an ETH-funded address — which would link their on-chain identity to their privacy operations. Second, for L3 unstakes, it maintains the nullifier IMT state and provides insertion witnesses that users need to generate their proofs. In the current design, this makes the relayer a privileged, single point of failure: if it goes down or loses state, L3 funds are stuck (see flaw #4). The relayer also has no gas protection against abuse (see flaw #3).

Addressing (BPK)

The system does not use EVM addresses for identifying users. Instead, each user has a Balance Public Key (BPK) — a point on the Grumpkin curve derived from their spending key. On-chain, balances are stored in a mapping keyed by the hash of the BPK, not by msg.sender. This means a user's privacy identity is entirely separate from their Ethereum account: knowing someone's EVM address reveals nothing about their encrypted balance, and vice versa. To send someone funds, you need their BPK. For details on how keys are derived, see Key Management.

SDK

The SDK is what app developers integrate. It handles key management, transaction construction, proof generation, balance decryption, and state synchronization behind a high-level TypeScript API. The two most platform-sensitive operations — circuit proving and discrete-log solving — are abstracted behind adapter interfaces, with native implementations for iOS (Rust via MoPro and Expo modules) and WASM fallbacks for web and Android. Key storage is similarly abstracted: MMKV on iOS, localStorage or IndexedDB in the browser, filesystem on Node.