Zama FHE Implementation
How Complyr uses Zama's Fully Homomorphic Encryption to power private audit logic on encrypted payment data.
Overview
Conventional encryption methods such as AES-GCM or ECIES protect data at rest and in transit. To compute anything on the data, you have to decrypt it first. For an auditor, that means the only way to check whether payments meet their criteria is to receive the decryption key and read the entire payment ledger in plaintext. Privacy and oversight become mutually exclusive: you either encrypt and lose the ability to audit, or you decrypt and lose the privacy the encryption was meant to provide.
Zama's Fully Homomorphic Encryption resolves this directly. FHE allows a program to perform calculations on encrypted values and produce an encrypted result, without the inputs ever leaving ciphertext. The contract can compare two encrypted numbers, add them together, or branch on their relationship, all without knowing what those numbers are.
Complyr uses this as the foundation for its entire audit layer.
What gets encrypted
Every payment made through Complyr is accompanied by an encrypted audit record. For each recipient in that payment, the following values are encrypted in the business browser before the transaction is submitted:
- the expense category
- the jurisdiction
Encryption happens client-side using the Zama relayer SDK. The plaintext values never reach the chain. What is stored onchain are FHE ciphertexts that authorised wallets can decrypt, and that the contract can compute over directly.
How audit tests work
When a business grants an auditor access, the auditor can create private audit tests from their portal. The available test types are:
- flag any single payment above a set amount
- flag when total payments to a specific recipient cross a set amount
- flag when total spending in a category crosses a set amount
- flag when total payments tied to a jurisdiction cross a set amount
For each test, the auditor sets a limit. That limit is encrypted in the auditor's browser before submission. Once stored onchain, it is unreadable by the business.
This has a consequence worth stating plainly. If the business could see an auditor's test limits, it could structure payments to stay just below them, splitting a large payment into smaller ones to avoid triggering a flag, for example. Because the limits are encrypted and the business has no access to them, that kind of evasion is not possible. The audit criteria are as private as the payment data itself.
What happens when a payment is recorded
Every time a payment is made, the contract automatically evaluates all active audit tests against that payment's encrypted data using FHE comparison and arithmetic operations.
The contract never decrypts any value during this process. It produces an encrypted result indicating whether the test's limit was triggered and stores it in the auditor's findings list. The list accumulates silently as payments come in. Nothing is revealed to the auditor until they choose to decrypt.
What the auditor actually sees
When the auditor reviews their findings, they sign a decryption request with their wallet. The Zama KMS validates that signature against the contract's access control list and returns only the values that wallet has been explicitly permitted to read.
For each finding, the auditor sees whether their test was triggered and, if it was, the associated payment that caused the flag. The auditor learns nothing about business payments that fell below their criteria.
Access levels determine how much an auditor can see beyond their findings. Auditors with standard access can decrypt findings and aggregate report totals. Auditors granted full access can also decrypt individual payment records from the business ledger. Both levels are enforced at the FHE layer by the contract's permission system, not by the application.
A note on what Complyr encrypts and why. Payment amounts and recipient addresses remain publicly visible on the blockchain, as with any onchain transaction. What Complyr encrypts is the audit context attached to each payment: the expense category and the jurisdiction. An encrypted copy of the amount is also stored so the contract can run private calculations, such as totalling spend by category or checking whether a limit was crossed. It is a computation input, not a privacy feature. Reference IDs remain plaintext by design, as a usability tradeoff. Complyr is audit infrastructure leveraging Zama's FHE at its core for private computations, not a payment privacy tool.
Technical detail
Client-side encryption
Encryption is handled in apps/web/src/lib/fhe-audit.ts using the Zama relayer SDK, loaded in the browser via a <Script> tag.
When a business submits a payment, encryptAuditInput() is called. For each recipient it encrypts three values in a single input object:
- the payment amount as
euint128(add128) - the expense category as
euint8(add8) - the jurisdiction as
euint8(add8)
The SDK produces a set of ciphertext handles and a zero-knowledge input proof. These are what gets submitted to the contract. The plaintext values are discarded in the browser and never sent anywhere.
When an auditor creates a test, encryptThresholdInput() follows the same pattern, encrypting a single euint128 threshold bound to the auditor's address and the registry contract. The business cannot read it because it is never granted FHE.allow.
Onchain storage and permissions
The contract (AuditRegistry.sol) accepts the ciphertext handles and materialises them into internal encrypted types using FHE.fromExternal(handle, proof). It then immediately assigns access using two primitives:
FHE.allowThis(value)— permits the contract itself to read the value in future operationsFHE.allow(value, address)— permits a specific wallet to decrypt the value via the KMS
For each audit record, the business owner (masterEOA) is always granted FHE.allow. Auditors with full ledger access are also granted FHE.allow at record time. Auditors with findings-only access are not — they can only decrypt the outputs of their own tests.
Audit test thresholds are permitted only to the contract and the auditor who created the test. The business is never in the allow list.
The Auditor Portal
The Auditor Portal is a dedicated interface for compliance teams, external auditors, and regulators. Once a business grants an auditor access, they use this portal to interact with the encrypted ledger. It serves as the control center where auditors can deploy active audit tests, view triggered findings, and securely decrypt the specific payment records that require investigation—all without ever exposing the company's full plaintext transaction history.
Active Audit Tests
At the core of the auditor portal are active compliance rules, known internally in the contract as ReviewTests. For clarity, we will refer to them as Audit Tests. An auditor can define limits against encrypted financial activity, allowing the smart contract to act as an automated compliance engine.
1. Test Creation and Privacy
An auditor creates a test by setting an exposure limit (for example, a maximum spend amount) via the portal. Before this limit ever touches the blockchain, the Zama relayer SDK encrypts it directly in the auditor's browser.
The privacy model here is strict:
- The Business cannot see the auditor's limits, meaning they cannot split payments to intentionally evade detection.
- The Public sees only encrypted activity and cannot determine payment amounts, categories, or the auditor's rules.
- The Auditor does not see the plaintext ledger unless explicitly granted full access. If they only have signaling access, they simply receive an encrypted "Yes/No" flag when a rule is broken.
2. The Four Types of Audit Tests
The contract supports four types of tests to evaluate risk and exposure.
Large Payment Tests Flags any single transaction that strictly exceeds a defined limit.
// LargePayment evaluation
FHE.gt(amount, test.threshold) // compares the encrypted amount to the encrypted limitRecipient Exposure Tests Monitors the cumulative total paid to a specific recipient address. The contract aggregates payments over time and triggers a finding if the running total crosses the auditor's limit.
// RecipientExposure evaluation
FHE.gt(_recipientTotals[proxyAccount][recipient], test.threshold)Category Exposure Tests Tracks overall spending across specific business categories (e.g., Contractor payouts or Bonuses). Example: "Alert me if the total combined spending for bonuses ever crosses $20,000."
- Payment 1: $4,000 to bonus -> (Running Total: $4,000) -> No alert.
- Payment 2: $8,000 to bonus -> (Running Total: $12,000) -> No alert.
- Payment 3: $12,000 to bonus -> (Running Total: $24,000) -> ALERT!
// CategoryExposure evaluation
ebool isCategory = FHE.eq(category, test.numericScope);
ebool triggered = FHE.and(
isCategory,
FHE.gt(_categoryTotals[proxyAccount][test.numericScope], test.threshold)
);Jurisdiction Exposure Tests Functions exactly like the category test, but tracks aggregate spending within a specific jurisdiction.
// JurisdictionExposure evaluation
ebool isJurisdiction = FHE.eq(jurisdiction, test.numericScope);
ebool triggered = FHE.and(
isJurisdiction,
FHE.gt(_jurisdictionTotals[proxyAccount][test.numericScope], test.threshold)
);3. Execution Workflow: The "Blind Accumulation" Problem
Tracking cumulative exposure on a public blockchain usually destroys privacy. If a business makes a payment and the public sees the "Bonus" running total increase, they instantly know the payment was a bonus.
To solve this, Complyr updates every single category and jurisdiction total at the exact same time. When a new payment comes in, the contract uses FHE.select to add the real encrypted amount to the correct category, and a completely encrypted $0 to all the other categories. Observers see every total update simultaneously with ciphertext, making it mathematically impossible to tell which category actually received the funds.
Tests evaluate in two modes:
- Real-time: The contract runs active tests automatically against every new payment.
- Historical Backtesting: Auditors can deploy a new limit today and instruct the contract to evaluate it against the entire historical ledger.
Appendix: Data Mappings
For reference, the system maps plaintext UI categories to numeric IDs onchain.
Categories:
1: Payroll (W2) | 2: Payroll (1099) | 3: Contractor | 4: Bonus | 5: Invoice | 6: Vendor | 7: Grant | 8: Dividend | 9: Reimbursement | 10: Other
Jurisdictions:
1: US-CA | 2: US-NY | 3: US-TX | 4: US-FL | 5: US-OTHER | 6: UK | 7: EU-DE | 8: EU-FR | 9: EU-OTHER | 10: NG | 11: SG | 12: AE | 13: OTHER
Finding storage and masking
When a test runs, _storeReviewResult() stores four encrypted values in the auditor's queue:
result: aneuint8that is1if triggered,0if notmaskedAmount: the payment amount if triggered,0if notmaskedCategory: the category if triggered,0if notmaskedJurisdiction: the jurisdiction if triggered,0if not
The masking is done with FHE.select(triggered, value, asEuint(0)) before the values are stored. An auditor who decrypts a non-triggered finding receives only zeros. They learn nothing about payments that did not meet their criteria.
Decryption flow
Decryption is handled in the browser by userDecryptAuditHandles(). The flow is:
- A temporary keypair is generated client-side.
- An EIP-712 typed data payload is constructed, scoped to the registry contract with a time-limited validity window.
- The connected wallet signs the payload.
- The signature and public key are sent to the Zama KMS.
- The KMS checks the contract's permission list for each requested handle. It returns decrypted values only for handles that have
FHE.allowset for the requesting address. - Decrypted values are returned to the browser. Nothing is decrypted server-side or by the application backend.