Architecture
Contract relationships, data flow diagrams, and deployment order for the privacy system.
Contract Relationships
flowchart TB
subgraph privacy["Privacy Package"]
CL["ConfidentialLedger<br/>(UUPS Upgradeable)"]
MT["IncrementalMerkleTree<br/>(depth 32, Poseidon)"]
NS["Nullifier Set<br/>(mapping uint256 → bool)"]
VF["Halo2KZGVerifier<br/>(BN254 pairing verifier)"]
PH["PoseidonBN254<br/>(P128Pow5T3)"]
end
subgraph wallets["Wallets Package"]
SF["ShieldedTransferFacet<br/>(Diamond facet)"]
end
subgraph external["External"]
EP["EntryPoint (ERC-4337)"]
PM["AxiomaticPaymaster"]
BN["Bundler"]
end
SF -->|"shield / transfer / unshield"| CL
CL --> MT
CL --> NS
CL -->|"verifyProof"| VF
CL -->|"hash"| PH
MT -->|"hash"| PH
BN -->|"UserOp"| EP
EP -->|"execute()"| SF
PM -->|"sponsor gas"| EPData Flow — Shield Operation
sequenceDiagram
participant User
participant API as /api/privacy/shield
participant Wallet as Smart Account
participant Facet as ShieldedTransferFacet
participant CL as ConfidentialLedger
participant MT as MerkleTree
User->>API: POST {entityId, token, amount}
API->>API: Derive key set, compute commitment
API->>API: Encrypt note under IVK
API->>Wallet: UserOp (shieldETH or shieldTokens)
Wallet->>Facet: delegatecall
Facet->>CL: shield(token, amount, commitment, scanTag, ephemeralPubKey, encryptedNote)
CL->>MT: insertLeaf(commitment)
MT-->>CL: leafIndex
CL-->>CL: emit NoteCreated(commitment, leafIndex, scanTag, ephemeralPubKey, encryptedNote)
CL-->>Facet: success
Facet-->>Wallet: success
API-->>User: {userOpHash, noteCommitment}Data Flow — Private Transfer
sequenceDiagram
participant User
participant API as /api/privacy/prove-transfer
participant Prover as Native Prover (Halo2-KZG)
participant Relay as /api/relay/transfer
participant Bundler
participant Wallet as Relay Smart Account
participant CL as ConfidentialLedger
participant VF as Halo2KZGVerifier
User->>API: POST {entityId, recipient, token, amount}
API->>API: Select input notes, fetch Merkle paths
API->>Prover: Generate JoinSplit proof (witness)
Prover-->>API: proof, public inputs
API-->>User: proof bundle
User->>Relay: POST {proof, root, nullifiers, commitments, ...}
Relay->>Bundler: Submit UserOp (anonymous relay account)
Bundler->>Wallet: execute()
Wallet->>CL: transfer(proof, root, nullifiers, commitments, ...)
CL->>VF: verifyProof(proof, instances)
VF-->>CL: valid
CL->>CL: Record nullifiers, insert new commitments
CL-->>Wallet: successData Flow — Private DeFi Call (Swap / Earn)
sequenceDiagram
participant User
participant WalletUI as Wallet App
participant Prover as Browser Prover (private_call)
participant Bundler
participant SA as Smart Account
participant CL as ConfidentialLedger
participant Adapter as Private Adapter (UniswapV4 / ERC4626)
User->>WalletUI: Submit private swap/deposit/withdraw
WalletUI->>Prover: Build witness + prove(private_call)
Prover-->>WalletUI: proof + public inputs
WalletUI->>Bundler: UserOp(privateContractCall)
Bundler->>SA: execute()
SA->>CL: privateContractCall(...)
CL->>Adapter: external call(adapterCalldata)
Adapter-->>CL: token output
CL-->>SA: successPrivate DeFi calls use protocol adapters:
PrivateUniswapV4Adapterfor swapsPrivateERC4626Adapterfor vault deposit/withdraw
If adapter execution fails, ConfidentialLedger now bubbles adapter revert bytes through a typed error so wallet decoding can show actionable diagnostics instead of a generic revert.
ECDH Key Agreement Model
Note encryption uses X25519 ECDH (Elliptic Curve Diffie-Hellman) for per-note key agreement:
- The recipient's X25519 public key is derived from their IVK and published in the stealth meta-address
- Each sender generates a fresh ephemeral X25519 keypair
(r, R)per note - Shared secret:
x25519(r, recipientX25519Pub)— only the recipient (with IVK-derived private key) can compute it - Note key:
HKDF-SHA256(sharedSecret, "axiomatic.note.v2")
This ensures inter-sender privacy: even if Sender A is compromised, they cannot decrypt Sender B's notes to the same recipient.
Server-Side IVK for Cron Scanning
The IVK (Incoming Viewing Key) is stored server-side to enable cron-based scanning of incoming notes:
- Rationale: Journal entries and note data must be available in the web app for reconciliation and reporting. Client-only scanning would require the wallet to be open for events to be processed.
- Security: The IVK is read-only — it cannot spend notes or derive the spending key. It is encrypted at rest under the entity's data encryption key (DEK).
- Scope: The scanning cron (
/api/cron/scan-incoming-notes) uses the IVK to trial-decrypt notes and classify transfer types (deposits, transfers in/out, withdrawals).
Wallet Receive UX Model
Receive now uses a segmented three-mode layout:
- Crypto: stealth meta-address (
st:), QR, and sharing actions - Bank: KYC/Plaid + ACH deposit details
- Request: contact-first private payment request generation
The same axiomatic://send?... request format is used for both QR and deep-link prefills.
Scheduled Payment Execution Semantics
Bills scheduling now enforces destination-aware behavior:
- Bank contacts:
Auto (bank)execution path remains active - Wallet contacts:
Reminder (wallet)only in this release (no unattended execution)
This avoids silent/unsafe execution for private wallet-to-wallet flows before policy-constrained delegation is fully enabled.
Scoped Session-Key Scaffold (Phase 2)
A feature-flagged policy scaffold is staged in wallet settings for:
- recurring bank payouts,
- recurring private wallet transfers,
- auto-shield policy presets.
Policies encode strict constraints (method allowlist, token caps, amount ceilings, time windows, and recipient limits) before autonomous execution is enabled by default.
Scan Tag Protocol
Scan tags are 4-byte fingerprints that enable efficient server-side filtering without trial decryption:
- Derivation:
keccak256(recipientX25519Pub)[0:4] - On-chain: The
NoteCreatedevent includesscanTagas an indexed parameter, allowing the Goldsky indexer and scanning cron to filter events with a simpleWHERE scan_tag = $1query - Privacy trade-off: All notes to the same recipient share the same scan tag (linkability), but the tag does not reveal the recipient's identity or public key. Collision probability is ~1 in 4 billion.
See Scan Tags for full details.
Key Types
| Type | Solidity | Description |
|---|---|---|
| Commitment | uint256 | Poseidon(ownerPubkey, Poseidon(tokenHash, amount, salt)) |
| Nullifier | uint256 | Poseidon(spendingKey, commitment) |
| Merkle Root | uint256 | Root of the depth-32 Poseidon Merkle tree |
| Token | address | address(0) for native ETH, ERC-20 address otherwise |
Deployment Order
flowchart LR
P["1. PoseidonBN254"] --> V["2. Halo2KZGVerifier"]
V --> CL["3. ConfidentialLedger(verifier, hasher)"]
CL --> SF["4. Add ShieldedTransferFacet to Diamonds"]
SF --> INIT["5. ShieldedTransfer_init(admin, ledgerAddress)"]Environment Variables
After deployment, set these in the platform .env:
| Variable | Description |
|---|---|
CONFIDENTIAL_LEDGER | ConfidentialLedger address (shared across Base Sepolia and Base mainnet) |
WALLET_ADDRESS_BASE_SEPOLIA | Relay smart account address (Base Sepolia) |
WALLET_ADDRESS_BASE | Relay smart account address (Base mainnet) |
BUNDLER_URL_BASE_SEPOLIA | ERC-4337 bundler RPC URL (Base Sepolia) |
BUNDLER_URL_BASE | ERC-4337 bundler RPC URL (Base mainnet) |
ENTRYPOINT_ADDRESS | ERC-4337 EntryPoint contract address |