A Security-Focused Guide to Fully Homomorphic Encryption for Web3 Protocols
Aligned with Zama fhEVM, Fhenix CoFHE, and Web3 Security Best Practices
- Chapter 1: Introduction
- Chapter 2: Cryptographic Foundations
- Chapter 3: Zama fhEVM
- Chapter 4: Fhenix CoFHE
- Chapter 5: Common Vulnerabilities
- Chapter 6: Security Best Practices
- Chapter 7: Auditing FHE Protocols
- Chapter 8: Ecosystem & Research
- Chapter 9: Open Research Problems
- Appendices
📋 FHE Protocol Security Audit Checklist - Comprehensive security audit checklist covering IND-CPA/CCA/CPAD, lattice attacks, side-channels, key recovery, and implementation security.
Fully Homomorphic Encryption (FHE) is a cryptographic technique that allows computations to be performed directly on encrypted data without ever decrypting it. The result of any computation remains encrypted and can only be decrypted by the holder of the secret key.
┌─────────────────────────────────────────────────────────────┐
│ Traditional Encryption │
├─────────────────────────────────────────────────────────────┤
│ Encrypt(data) → ciphertext │
│ Decrypt(ciphertext) → data │
│ Compute(data) → result ← Data exposed during computation! │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Homomorphic Encryption │
├─────────────────────────────────────────────────────────────┤
│ Encrypt(data) → ciphertext │
│ Compute(ciphertext) → encrypted_result │
│ Decrypt(encrypted_result) → result │
│ ↑ Data NEVER exposed during computation! │
└─────────────────────────────────────────────────────────────┘
Blockchains are inherently transparent - all transaction data, smart contract state, and computations are publicly visible. This creates fundamental privacy challenges:
| Problem | Traditional Blockchain | With FHE |
|---|---|---|
| Account balances | Publicly visible | Encrypted |
| Transaction amounts | Publicly visible | Encrypted |
| Smart contract logic | Executes on plaintext | Executes on ciphertext |
| MEV exploitation | Easy to front-run | Data hidden from validators |
| Regulatory compliance | Privacy violations | GDPR-compatible |
| Year | Milestone |
|---|---|
| 1978 | Rivest, Adleman, Dertouzos propose the concept |
| 2009 | Craig Gentry achieves first FHE construction (impractical) |
| 2011-2014 | BGV, BFV, CKKS schemes developed |
| 2016 | TFHE enables fast bootstrapping |
| 2022+ | Zama, Fhenix bring FHE to production Web3 |
graph TB
subgraph Privacy Technologies
FHE["FHE<br/>Compute on encrypted data"]
ZK["Zero-Knowledge Proofs<br/>Prove without revealing"]
MPC["Multi-Party Computation<br/>Distribute trust"]
TEE["Trusted Execution Environments<br/>Hardware isolation"]
end
subgraph Security Properties
FHE --> |"Data stays encrypted"| P1["Full Computation Privacy"]
ZK --> |"Verify statements"| P2["Verification Without Disclosure"]
MPC --> |"No single party sees all"| P3["Distributed Trust"]
TEE --> |"Hardware boundary"| P4["Isolated Execution"]
end
| Technology | Data Privacy | Computation Privacy | Trust Model | Performance |
|---|---|---|---|---|
| FHE | ✅ Full | ✅ Full | Cryptographic | Slow (improving) |
| ZK Proofs | ✅ Full | ❌ Prover sees data | Cryptographic | Medium |
| MPC | N-of-M threshold | Medium | ||
| TEE | Hardware vendor | Fast |
Important
Key Insight for Security Engineers: FHE and ZK are complementary. ZK proves correctness without revealing data; FHE allows computation on hidden data. Many production systems combine both.
Traditional smart contract audits focus on:
- Reentrancy attacks
- Integer overflow/underflow
- Access control bypass
- Logic errors
FHE introduces entirely new attack surfaces:
- Encrypted arithmetic behaves differently - No reverts on overflow (would leak information)
- Access control on ciphertexts - Who can decrypt what, and when?
- Asynchronous decryption - Callback patterns introduce race conditions
- Information leakage - Timing, access patterns, and metadata can leak data
| Traditional Assumption | FHE Reality |
|---|---|
| Overflow/underflow reverts | Silently wraps to prevent info leakage |
| View functions are safe | Decryption can leak state |
| Gas costs are predictable | FHE operations 100-10,000x more expensive (addition: ~100-200x, multiplication: ~1,000-5,000x, comparisons: ~500-2,000x) |
| All state is public | Encrypted state requires permission to read |
flowchart LR
subgraph "New FHE Attack Surfaces"
A[Ciphertext Handle Leakage] --> B[Unauthorized Decryption]
C[Callback Replay] --> D[Double-Spend Equivalent]
E[ACL Misconfiguration] --> F[Data Disclosure]
G[Overflow Wrapping] --> H[Economic Exploits]
end
-
Confidential DeFi
- Private lending with encrypted credit scores
- Hidden order books and MEV protection
- Encrypted liquidity positions
-
Private Governance
- Anonymous voting with verifiable tallying
- Encrypted DAO proposals until execution
-
Confidential Tokens (FHERC20)
- Hidden balances and transfer amounts
- Compliant with regulatory requirements
- Note: FHERC20 is a proposed naming convention for confidential tokens on FHE-enabled chains, analogous to ERC20 but with encrypted balances. No formal EIP standard exists yet.
-
Sealed-Bid Auctions
- Bids remain encrypted until reveal
- Fair price discovery
Actors:
- Validators/Sequencers: Can see transaction ordering, gas usage, access patterns
- Other Users: Cannot see encrypted values but can observe behavior
- Protocol Operators: May control threshold decryption keys
- External Attackers: Side-channel attacks, replay attacks
Trust Assumptions:
- FHE coprocessors are trusted for computation
- Threshold decryption network is Byzantine fault tolerant
- Client-side encryption is performed correctly
Understanding FHE security requires precise definitions. These game-based formulations are directly relevant to auditing Zama, Fhenix, and Web3 protocols.
The baseline security guarantee claimed by all FHE schemes (Zama TFHE-rs, Fhenix dBFV/TFHE).
Security Game
1. Challenger generates (pk, sk) ← KeyGen(λ)
2. Adversary A receives pk
3. A submits two messages m₀, m₁ of equal length
4. Challenger computes c* ← Encrypt(pk, m_b) for random bit b
5. A outputs guess b'
6. A wins if b' = b
Advantage:
Zama/Fhenix Relevance:
- ✅ Encrypted balances are indistinguishable (attacker can't tell Encrypt(1000) from Encrypt(5000))
- ✅ Sealed bids in auctions remain private
- ✅ DAO votes cannot be distinguished
Note
A scheme is IND-CPA secure if
IND-CCA1 (non-adaptive) and IND-CCA2 (adaptive) provide decryption oracle access:
Additional Phase (CCA):
- A can submit ciphertexts c ≠ c* to decryption oracle
- Receives Decrypt(sk, c) for each query
Why FHE is Inherently CCA-Insecure:
FHE's homomorphic property creates an unavoidable attack:
Attack:
1. Receive challenge ciphertext c* = Encrypt(m)
2. Compute c' = FHE.add(c*, Encrypt(1)) = Encrypt(m + 1)
3. Query decryption oracle: Decrypt(c') = m + 1
4. Recover m = (m + 1) - 1 ✓
Web3 Implication: This is why ACL (Access Control Lists) are critical in fhEVM and CoFHE—the cryptography alone cannot prevent unauthorized computation on ciphertexts.
IND-CPAD (Chosen Plaintext Attack with Decryption) is directly relevant to threshold decryption networks used by Zama KMS and Fhenix TDN.
Security Game
Standard IND-CPA game, but:
- A receives decryption OUTPUTS (not oracle access)
- Decrypted values may contain residual noise information
- This noise can leak the secret key!
Attack Mechanics (Li-Micciancio 2021, Guo et al. 2024):
For approximate schemes (CKKS) and threshold decryption:
- Observe decryption output
$m' = m + e$ where$e$ is residual noise - Collect multiple
$(c_i, m'_i)$ pairs - Solve system of equations to recover secret key
$s$ - 2024 USENIX Result: Key recovery from single decryption output possible!
Concrete Attack Complexity:
| Scheme | Decryptions Needed | Time | Reference |
|---|---|---|---|
| CKKS (no flooding) | 1 | seconds | Guo et al. 2024 |
| BFV/BGV (imperfect) | ~100-1000 | < 1 hour | ePrint 2024/127 |
| TFHE (error injection) | varies | varies | ePrint 2022/1563 |
Mitigation (Required for Production):
Noise Flooding: Before revealing decrypted value, add noise σ_flood where:
σ_flood >> B_max × 2^λ
Where:
B_max = maximum noise bound after computation
λ = security parameter (e.g., 128)
Caution
Audit Requirement: Verify Zama/Fhenix implementations use worst-case noise estimation for flooding. Average-case estimation was bypassed in the 2024 USENIX attack.
FHE security reduces to the hardness of lattice problems. Understanding these is essential for parameter validation.
Learning With Errors (LWE) Problem:
Given
with:
-
$a_i \leftarrow \mathbb{Z}_q^n$ uniformly random -
$s \leftarrow \chi_s$ (secret distribution, typically Gaussian or uniform small) -
$e_i \leftarrow \chi_e$ (error distribution, typically discrete Gaussian with std dev$\sigma$ )
Decision-LWE: Distinguish
Search-LWE: Recover
Reduction: Decision-LWE
Ring-LWE (RLWE):
More efficient variant operating over polynomial rings:
where
Samples:
Efficiency Gain: Polynomial multiplication via NTT in
Security is measured in bits of security based on the best known attacks.
Lattice Estimator Formula:
For LWE with parameters
where
-
$\delta \approx 1.004$ for 128-bit security -
$\delta \approx 1.003$ for 192-bit security -
$\delta \approx 1.002$ for 256-bit security
Production Parameter Sets:
| Implementation | Ring Dim (n) | log₂(q) | σ | Security (bits) | Source |
|---|---|---|---|---|---|
| TFHE-rs (Zama) | 2048 | 64 | 3.19 | ~128 | TFHE-rs docs |
| Fhenix CoFHE | 4096 | 109 | 3.2 | ~128 | Fhenix docs |
| OpenFHE default | 4096 | 54 | 3.19 | ~128 | OpenFHE docs |
| HE Standard | 2048+ | varies | 3.2 | 128+ | HomomorphicEncryption.org |
NIST PQC Alignment (2024):
NIST's post-quantum standards (FIPS 203/204/205) use similar lattice assumptions:
- FHE parameters generally exceed NIST Category 1 (128-bit) requirements
- Same resistance to Grover's algorithm (quadratic speedup mitigated by parameter sizing)
Important
Audit Verification: Use the Lattice Estimator to independently verify claimed security levels. Do not trust library documentation alone.
| Assumption | Formal Definition | Quantum Secure? | Used By |
|---|---|---|---|
| LWE | Decision-LWE hard | ✅ Yes | BGV, BFV, CKKS |
| RLWE | Ring variant of LWE | ✅ Yes | All efficient schemes |
| NTRU | Short vector in NTRU lattice | Some older schemes | |
| AGCD | Approximate GCD | ✅ Believed | DGHV (historical) |
Note
For Security Engineers: Always verify the specific lattice parameters used. Older schemes used "overstretched" NTRU parameters vulnerable to subfield attacks (broken 2015-2016).
| Scheme | Data Type | Exact? | Speed | Use Case | Original Paper |
|---|---|---|---|---|---|
| BGV | Integers | ✅ | Medium | General computation | ePrint 2011/277 |
| BFV | Integers | ✅ | Medium | Smart contracts | ePrint 2012/144 |
| CKKS | Approx. floats | ❌ (~10-20 bits) | Fast | ML/Analytics | ASIACRYPT 2017 |
| TFHE | Bits/small ints | ✅ | Fast bootstrap | Fhenix, Zama | J. Cryptol. 2019 |
| dBFV | Integers | ✅ Exact | Medium | High-precision exact FHE | Research papers |
Used by both Zama and Fhenix. Key properties:
- Fast bootstrapping: ~10ms per operation (vs seconds for others)
- Programmable bootstrapping: Evaluate lookup tables during noise refresh
- Bit-level operations: Native AND, OR, XOR gates
A technique for high-precision exact FHE:
- High precision: Exact arithmetic without approximation errors
- Decomposed operations: Better noise management through digit decomposition
- Optimized for smart contracts: Improved error rates compared to standard BFV
Note
For Fhenix's specific implementation details and their Threshold FHE Decryption protocol, see their official documentation.
Note
For comprehensive attack research, see awesome-fhe-attacks - a curated repository of FHE security research maintained by Hexens.
Subfield Lattice Attack:
- Affects NTRU-based schemes with poor parameter choices
- Historical Note: LTV (Lopez-Alt et al., 2012) and BLLN (Brakerski-Langlois-Lepoint-Naehrig, 2013) were early FHE schemes broken by subfield lattice attacks in 2015-2016
- Modern schemes (TFHE, BGV, BFV, CKKS) use different algebraic structures and parameter regimes
- Audit check: Verify scheme is not using deprecated constructions (LTV, BLLN, overstretched NTRU)
- Research: A Successful Subfield Lattice Attack on FHE | Revisiting Lattice Attacks on Overstretched NTRU
Dual Lattice Attack:
- Exploits weak modulus-to-dimension ratios
- Audit check: Verify security level claims (128-bit minimum recommended)
- Research: On Dual Lattice Attacks Against Small-Secret LWE | Hybrid Dual and Meet-LWE Attack
Recent research (2024) has demonstrated practical key recovery against major FHE libraries:
| Library | Attack Type | Reference | Status |
|---|---|---|---|
| SEAL | IND-CPAD/Chosen ciphertext | ePrint 2024/127 | Patched |
| OpenFHE | IND-CPAD exploitation | ePrint 2024/116 | Patched |
| TFHE-rs | Decryption error induction | ePrint 2022/1563 | Mitigated |
| Lattigo | Parameter weakness | ePrint 2024/116 | Patched |
Key Research Papers:
- On the Practical CPAD Security of "Exact" and Threshold FHE Schemes - CRYPTO 2024
- Attacks Against the IND-CPAD Security of Exact FHE Schemes - Demonstrates key recovery in <1 hour on BFV/BGV
- A Practical Full Key Recovery Attack on TFHE and FHEW - Via induced decryption errors
- Key Recovery Attacks on Approximate HE with Non-Worst-Case Noise Flooding - USENIX Security 2024
Caution
Security Engineers: Always verify the library version. Many attacks have been patched in recent releases. Check CVE databases and library changelogs. See Zellic's Key Recovery Analysis for practical implications.
FHE ciphertexts contain "noise"
Ciphertext Structure:
For LWE-based schemes, a ciphertext is
with:
-
$s$ = secret key -
$\Delta = \lfloor q/t \rfloor$ = scaling factor (for plaintext modulus$t$ ) -
$m$ = plaintext message -
$e$ = noise term (must satisfy$|e| < \Delta/2$ for correct decryption)
Noise Growth Formulas:
| Operation | Noise After Operation | Growth Rate |
|---|---|---|
| Encryption | Initial (small) | |
| Addition | Linear | |
| Scalar Mult | Linear in scalar | |
| Ciphertext Mult | Multiplicative | |
| Relinearization | Adds |
Fixed cost |
| Bootstrapping | Resets to |
Constant (refresh) |
Noise Budget:
The noise budget
-
Decryption succeeds when
$\beta > 0$ -
Decryption fails when
$\beta \leq 0$ (noise exceeds threshold) - Multiplication roughly halves the noise budget
TFHE Bootstrapping (Unique Property):
TFHE's programmable bootstrapping resets noise while evaluating a lookup table:
This enables unlimited depth computation (at cost of ~10ms per bootstrap).
Security Implications:
| Failure Mode | Consequence | Detection |
|---|---|---|
| Noise overflow (undetected) | Silent wrong answer | None—catastrophic |
| Noise overflow (detected) | Decryption refuses | Explicit error |
| Near-threshold noise | Probabilistic decryption errors | Intermittent failures |
Warning
Audit Critical: Some FHE implementations return incorrect results on noise overflow without any error. This is the FHE equivalent of integer overflow—but cannot be fixed with require statements (would leak information).
Formal Parameter Relationships:
| Parameter | Symbol | Security Impact | Performance Impact |
|---|---|---|---|
| Ring dimension | ↑ increases security | ↓ decreases speed | |
| Ciphertext modulus | ↓ too large weakens security | ↑ increases noise budget | |
| Error std dev | ↑ increases security | ↓ decreases noise budget | |
| Plaintext modulus | — | ↑ increases message space, ↓ decreases |
Critical Ratios:
-
Security:
$n \cdot \log_2(q/\sigma)$ must exceed$\lambda \cdot \log_2(\delta^{-1})$ -
Correctness:
$q/t$ must be large enough for noise headroom -
Overstretching danger:
$\log_2(q) / n > 1$ may enable subfield attacks
Common Mistakes:
| Mistake | Consequence | How to Detect |
|---|---|---|
| Copying parameters between schemes | Security/correctness fail | Verify with lattice estimator |
| Sparse/ternary secrets with standard params | Reduced security by 20-40 bits | Check secret distribution |
| Ignoring type width in smart contracts | Overflow wrapping | Static analysis of euint types |
FHE provides IND-CPA security (formal definitions in §2.1) with these practical implications:
| Security Aspect | Status | Notes |
|---|---|---|
| Ciphertext indistinguishability | ✅ Guaranteed | Attacker cannot distinguish encryptions |
| Quantum resistance | ✅ Yes | Based on lattice hardness (LWE/RLWE) |
| CCA security | ❌ Not achievable | FHE is inherently malleable by design |
| IND-CPAD (threshold decryption) | Noise flooding mandatory |
Warning
Fundamental Limitation: FHE is inherently malleable—attackers can compute on ciphertexts without knowing plaintexts. This is by design (enabling computation), but requires ACL enforcement at the application layer.
Zama provides a complete FHE stack for Web3:
graph TB
subgraph "Zama FHE Stack"
A[Concrete<br/>FHE Compiler] --> B[TFHE-rs<br/>Rust Library]
B --> C[fhEVM<br/>Encrypted EVM]
C --> D[Blockchain Integration]
end
Concrete: Zama's domain-specific language (DSL) and compiler for FHE. It allows developers to write high-level FHE programs that compile to optimized TFHE operations. Concrete provides:
- High-level API for expressing encrypted computations
- Automatic parameter selection and optimization
- Noise budget tracking and management
- Compilation to efficient TFHE-rs operations
TFHE-rs: High-performance Rust implementation of TFHE primitives, providing the cryptographic foundation.
fhEVM: Modified Ethereum Virtual Machine supporting FHE operations via precompiles.
Zama's protocol has undergone security audits. For the most current and verified audit information, refer to Zama's official documentation and their GitHub repositories.
Note
Verification Required: When auditing FHE implementations, always request official audit reports directly from the protocol team. Third-party claims about audit coverage should be independently verified.
| Component | Known Auditors | Focus |
|---|---|---|
| TFHE-rs core | Trail of Bits | Cryptographic correctness |
| fhEVM contracts | OpenZeppelin | Smart contract security |
| Other components | Various (verify with team) | Key management, gateway security |
- Memory Safety: Rust's ownership model prevents common C/C++ vulnerabilities
- Constant-time operations: Critical paths are timing-attack resistant
- Parameter validation: Invalid parameters rejected at compile time where possible
| Attack Vector | TFHE-rs Status | Notes |
|---|---|---|
| Timing attacks | Partial mitigation | Core operations constant-time; bootstrapping reveals circuit depth |
| Power analysis | Hardware-dependent | Standard DPA countermeasures applied at hardware level |
| Cache attacks | Active research | Memory access pattern obfuscation; not guaranteed secure |
| Electromagnetic | Hardware-dependent | Requires physical access; standard shielding practices |
Note
Audit Tip: When auditing TFHE-rs integrations, verify that wrapper code doesn't introduce timing variations (e.g., early returns on error conditions).
┌─────────────────────────────────────────────────────────────┐
│ fhEVM │
├─────────────────────────────────────────────────────────────┤
│ Standard EVM Opcodes │ FHE Precompiles │
│ ───────────────────── │ ───────────────── │
│ ADD, MUL, CALL, etc. │ FHE.add(euint, euint) │
│ │ FHE.mul(euint, euint) │
│ │ FHE.select(ebool, euint, euint) │
│ │ FHE.decrypt(...) │
└─────────────────────────────────────────────────────────────┘
| Type | Description | Size |
|---|---|---|
ebool |
Encrypted boolean | - |
euint8 |
Encrypted 8-bit unsigned | 8-bit plaintext |
euint16 |
Encrypted 16-bit unsigned | 16-bit plaintext |
euint32 |
Encrypted 32-bit unsigned | 32-bit plaintext |
euint64 |
Encrypted 64-bit unsigned | 64-bit plaintext |
eaddress |
Encrypted address | 160-bit plaintext |
sequenceDiagram
participant User
participant Contract
participant KMS
participant Threshold Network
User->>Contract: Submit encrypted input
Contract->>Contract: FHE computation
Contract->>KMS: Request decryption
KMS->>Threshold Network: Threshold signature request
Threshold Network->>KMS: Partial signatures
KMS->>Contract: Decrypted result (if authorized)
Trust Assumptions:
- KMS nodes are semi-honest (follow protocol but may try to learn)
- Threshold (e.g., 2-of-3) prevents single-point-of-failure
- Network must remain available for decryption
| Optimization | Security Implication |
|---|---|
| Batching FHE operations | Reduces gas but increases complexity |
| Lazy decryption | Saves gas but defers security checks |
| Ciphertext caching | Memory efficient but risks stale data |
| Reduced precision types | Lower gas but potential overflow |
Warning
For Auditors: Be skeptical of gas optimizations in FHE contracts. The 1000x+ cost difference incentivizes shortcuts that may compromise security.
Zama targets 128-bit IND-CPA security based on lattice assumptions:
- Resistant to all known classical attacks (lattice reduction, dual attacks)
- Based on quantum-resistant LWE/RLWE assumptions (secure against Shor's algorithm)
- Parameters sized to maintain 128-bit security against Grover's quadratic speedup
- Formally verified parameter selection using lattice estimator tools
- Decryption latency: Threshold decryption adds network delay (typically 1-5 seconds)
- Ciphertext size: ~1KB per encrypted value (storage cost)
- Operation cost: Variable by operation type:
- Addition/Subtraction: ~100-200x more gas than plaintext
- Multiplication: ~1,000-5,000x more gas
- Comparisons/Select: ~500-2,000x more gas
- Bootstrapping (if needed): ~10,000x+ more gas
- Bootstrapping requirements: Deep computations need periodic noise refresh
Fhenix's CoFHE (Confidential FHE) is a coprocessor architecture that offloads FHE computations from the blockchain:
graph TB
subgraph "Client Layer"
A[cofhejs SDK] --> B[Encrypt inputs locally]
B --> C[Generate proofs]
end
subgraph "Blockchain Layer"
D[Smart Contract] --> E[cofhe-contracts library]
E --> F[FHE precompiles]
end
subgraph "CoFHE Coprocessor"
G[FHE Compute Engine]
H[Threshold Decryption]
end
C --> D
F --> G
G --> H
H --> D
Tip
API Note: Code examples in this handbook are illustrative and may not reflect exact production API syntax. Always refer to official Fhenix documentation for current SDK usage.
Trust Model:
- User's browser/client performs encryption
- Private key never leaves client
- Contract receives only ciphertexts
// cofhejs example - client-side encryption
import { Cofhe } from 'cofhejs';
const cofhe = await Cofhe.create({ provider });
// Encrypt locally before sending to contract
const encryptedAmount = await cofhe.encrypt(100, 'uint64');
const proof = await cofhe.generateProof(encryptedAmount);
// Send to contract
await contract.deposit(encryptedAmount, proof);Security Considerations:
- Client must use secure random number generation
- Encryption must be performed in secure context (not exposed to XSS)
- Proof generation must be deterministic to prevent manipulation
Core Solidity library for FHE operations:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {FHE, euint64, ebool} from "@fhenix/cofhe-contracts/FHE.sol";
contract ConfidentialToken {
mapping(address => euint64) private balances;
function transfer(address to, euint64 encAmount) external {
// FHE comparison - no plaintext exposure
ebool hasBalance = FHE.gte(balances[msg.sender], encAmount);
// Conditional update using encrypted select
balances[msg.sender] = FHE.select(
hasBalance,
FHE.sub(balances[msg.sender], encAmount),
balances[msg.sender]
);
balances[to] = FHE.select(
hasBalance,
FHE.add(balances[to], encAmount),
balances[to]
);
}
}Security Patterns:
- Use
FHE.select()for conditional logic on confidential data (prevents branch-based leakage) - Avoid decrypting values in require/assert if it would leak confidential information
- Decryption is acceptable when the value is intentionally being revealed (e.g., final auction results)
- Always check ACL permissions before operations
| Aspect | Design | Security Implication |
|---|---|---|
| Computation | Off-chain in coprocessor | Must trust coprocessor correctness |
| Verification | On-chain proof checking | Computational integrity guaranteed |
| Availability | Centralized (current) | Single point of failure |
| Decryption | Threshold network | Requires threshold honesty |
Warning
Current Limitation: The FHE coprocessor is a trusted component. Malicious coprocessor could return incorrect results. Future versions may add ZK proofs of correct computation.
A technique for high-precision exact FHE:
| Feature | Standard BFV | dBFV |
|---|---|---|
| Precision | Limited by modulus | High precision via decomposition |
| Noise growth | Multiplicative | Better controlled |
| Bootstrapping | Expensive | Optimized |
| Parameter flexibility | Fixed | Adaptive |
- Parameter Dependencies: Security relies on correct decomposition parameters
- Implementation Complexity: More code paths = more potential bugs
- Interoperability: May not be compatible with standard BFV implementations
sequenceDiagram
participant Contract
participant Gateway
participant Node1 as TDN Node 1
participant Node2 as TDN Node 2
participant Node3 as TDN Node 3
Contract->>Gateway: Decrypt request + ACL proof
Gateway->>Node1: Partial decrypt request
Gateway->>Node2: Partial decrypt request
Gateway->>Node3: Partial decrypt request
Node1->>Gateway: Partial decryption σ1
Node2->>Gateway: Partial decryption σ2
Gateway->>Gateway: Combine partials (2-of-3)
Gateway->>Contract: Decrypted value + proof
Threshold Configuration (t-of-n):
- Threshold: t signatures required to decrypt
- Total nodes: n nodes in the network
- Liveness: Requires at least t honest nodes available to complete decryption
- Safety: Requires fewer than t nodes compromised to prevent unauthorized decryption
- Example: In 2-of-3 setup:
- Can tolerate 1 node failure (liveness: 2 nodes can still decrypt)
- Prevents compromise if only 1 node is malicious (safety: need 2 to decrypt)
Key Compromise Scenarios:
| Scenario | Impact | Mitigation |
|---|---|---|
| 1 node compromised | No impact (below threshold) | Node rotation |
| t nodes compromised | Decryption possible | Emergency key rotation |
| All nodes compromised | Full data exposure | Multi-layer encryption |
Every ciphertext has an associated ACL controlling:
- Who can perform operations on it
- Who can request decryption
- Transient vs persistent permissions
// ACL pattern
function grantAccess(euint64 ciphertext, address user) internal {
FHE.allow(ciphertext, user); // Persistent permission
}
function grantTransientAccess(euint64 ciphertext) internal {
FHE.allowTransient(ciphertext, msg.sender); // Transaction-scoped
}Caution
ACL misconfigurations are the #1 source of FHE smart contract vulnerabilities.
| Vulnerability | Description | Impact |
|---|---|---|
| Over-permissioning | Granting permanent access when transient needed | Data leakage |
| Missing validation | Not checking caller before granting access | Unauthorized decryption |
| Propagation to untrusted | Allowing helper contracts to pass permissions | Disclosure oracle |
Based on OpenZeppelin audit findings and production security research
The Problem: In traditional Solidity 0.8+, arithmetic overflow reverts. In FHE, this would leak information (attacker learns value was too large), so operations silently wrap.
Vulnerable Pattern:
// ❌ VULNERABLE: Fee can wrap to ~0 on large amounts
function calculateFee(euint64 amount) public returns (euint64) {
euint64 fee = FHE.mul(amount, FHE.asEuint64(feePercent));
return FHE.div(fee, FHE.asEuint64(100));
}
// Attack: amount = MAX_UINT64 / feePercent + 1 → fee wraps to tiny valueSecure Pattern:
// ✅ SECURE: Clamp fee to maximum safe value
function calculateFee(euint64 amount) public returns (euint64) {
euint64 fee = FHE.mul(amount, FHE.asEuint64(feePercent));
fee = FHE.div(fee, FHE.asEuint64(100));
// Encrypted overflow check
ebool overflowed = FHE.gt(amount, FHE.asEuint64(MAX_SAFE_AMOUNT));
// Clamp to maximum fee if overflow detected
return FHE.select(overflowed, FHE.asEuint64(MAX_FEE), fee);
}Audit Checklist:
- All multiplications checked for potential overflow
- Division results validated against reasonable bounds
- Select statements used to handle edge cases
The Problem: Contracts may grant decrypt permissions to helpers without validating the original caller's permissions.
Root Cause: The function accepts a ciphertext handle from the caller without verifying the caller has permission to use it. This allows attackers to pass ciphertext handles they don't own but have observed (e.g., from blockchain events, transaction logs, or contract storage). The vulnerability creates a permission amplification attack where the contract unwittingly grants its own access rights to unauthorized parties.
Vulnerable Pattern:
// ❌ VULNERABLE: Helper gets access without caller validation
contract VulnerableVault {
function processWithHelper(euint64 data) external {
// Grants helper permanent access - anyone calling this
// gives helper access to their ciphertext
FHE.allow(data, address(helper));
helper.process(data);
}
}
contract MaliciousHelper {
function process(euint64 data) external {
// Can now decrypt any ciphertext passed to processWithHelper
FHE.decrypt(data); // Disclosure oracle!
}
}Secure Pattern:
// ✅ SECURE: Validate caller has permission before granting
contract SecureVault {
function processWithHelper(euint64 data) external {
// Verify caller actually owns/has permission on this ciphertext
require(FHE.isAllowed(data, msg.sender), "Unauthorized");
// Use transient permission instead of persistent
FHE.allowTransient(data, address(helper));
helper.process(data);
}
}Attack Flow:
graph LR
A[Attacker] -->|Calls| B[Vulnerable Contract]
B -->|Grants access| C[Helper Contract]
C -->|Decrypts| D[Attacker learns data]
style A fill:#ff6b6b
style D fill:#ff6b6b
Audit Checklist:
- All
FHE.allow()calls preceded by permission checks - Transient permissions preferred over persistent
- Helper contracts audited for decrypt calls
- Callbacks authenticated via modifier checking msg.sender
- Gateway address immutable and set at deployment
The Problem: Async decryption uses callbacks. If the request state isn't invalidated, callbacks can be replayed.
Vulnerable Pattern:
// ❌ VULNERABLE: Request not invalidated after callback
mapping(bytes32 => WithdrawRequest) public requests;
function requestWithdraw(euint64 amount) external {
bytes32 reqId = keccak256(abi.encodePacked(msg.sender, block.number));
requests[reqId] = WithdrawRequest({user: msg.sender, amount: amount});
FHE.requestDecrypt(amount, this.withdrawCallback.selector, reqId);
}
function withdrawCallback(bytes32 reqId, uint64 decryptedAmount) external {
WithdrawRequest memory req = requests[reqId];
// Request still valid! Attacker can replay this callback
payable(req.user).transfer(decryptedAmount);
}Secure Pattern:
// ✅ SECURE: Invalidate before external call + authenticate caller
address public immutable AUTHORIZED_GATEWAY;
constructor(address _gateway) {
AUTHORIZED_GATEWAY = _gateway;
}
modifier onlyGateway() {
require(msg.sender == AUTHORIZED_GATEWAY, "Unauthorized callback");
_;
}
function withdrawCallback(bytes32 reqId, uint64 decryptedAmount)
external
onlyGateway // Critical: Only gateway can call
{
WithdrawRequest memory req = requests[reqId];
require(req.user != address(0), "Invalid or used request");
// DELETE BEFORE TRANSFER (CEI pattern)
delete requests[reqId];
payable(req.user).transfer(decryptedAmount);
}Audit Checklist:
- All async requests have unique identifiers
- Request state deleted before external calls
- Callback can only be called by authorized gateway
- Re-entrancy protection on callbacks
The Problem: In confidential auctions, a bid might fail silently (insufficient balance), but the auction still records the bid amount.
Vulnerable Pattern:
// ❌ VULNERABLE: Compares requested amount, not actual transfer
function bid(euint64 bidAmount) external {
// This might transfer 0 if user has insufficient balance!
euint64 transferred = FHE.transferFrom(msg.sender, address(this), bidAmount);
// But we compare against the REQUESTED amount
ebool isHigher = FHE.gt(bidAmount, highestBid);
highestBid = FHE.select(isHigher, bidAmount, highestBid);
// Attacker "wins" with 0 actual transfer!
}Secure Pattern:
// ✅ SECURE: Compare actual transferred amount
function bid(euint64 bidAmount) external {
euint64 transferred = FHE.transferFrom(msg.sender, address(this), bidAmount);
// Verify something was actually transferred
ebool validTransfer = FHE.gt(transferred, FHE.asEuint64(0));
// Compare TRANSFERRED amount, not requested
ebool isHigher = FHE.and(
validTransfer,
FHE.gt(transferred, highestBid)
);
highestBid = FHE.select(isHigher, transferred, highestBid);
highestBidder = FHE.select(isHigher, msg.sender, highestBidder);
}The Problem: If auction winner info is revealed immediately, a blockchain reorg could let a temporary winner learn secrets even if they're later replaced.
Mitigation:
// Two-step reveal with finality delay
uint256 public constant FINALITY_DELAY = 32; // blocks
function scheduleReveal() external {
require(auctionEnded, "Auction not ended");
revealTimestamp = block.number + FINALITY_DELAY;
}
function claimPrize() external {
require(block.number >= revealTimestamp, "Finality delay not passed");
// Now safe to decrypt winner info
}The Problem:
Contracts with generic execute() functions can be exploited to call the ACL contract directly.
Vulnerable Pattern:
// ❌ VULNERABLE: Arbitrary calls can manipulate ACL
contract VulnerableExecutor {
function execute(address target, bytes calldata data) external onlyOwner {
(bool success,) = target.call(data);
require(success);
}
}
// Attacker: execute(ACL_ADDRESS, FHE.allow(victimCiphertext, attacker))Secure Pattern:
// ✅ SECURE: Blocklist sensitive targets
contract SecureExecutor {
mapping(address => bool) public blockedTargets;
constructor() {
blockedTargets[FHE.ACL_ADDRESS] = true;
blockedTargets[FHE.GATEWAY_ADDRESS] = true;
}
function execute(address target, bytes calldata data) external onlyOwner {
require(!blockedTargets[target], "Blocked target");
(bool success,) = target.call(data);
require(success);
}
}The Problem: In AA environments, multiple user operations share a transaction. Transient permissions from one user could leak to another.
Vulnerable Scenario:
Transaction containing:
UserOp1 (Alice): Grants transient permission to ciphertext C
UserOp2 (Bob): Can access Alice's ciphertext C via shared transient storage!
Mitigation:
// AA wallet must clean transient storage between operations
function executeUserOp(UserOperation calldata op) external {
// ... execute operation ...
// Clean FHE transient storage after each user op
FHE.cleanTransientStorage();
}| Attack | Description | Mitigation |
|---|---|---|
| Timing | Operation duration reveals data | Constant-time implementations |
| Power | Power consumption patterns | Hardware countermeasures |
| Cache | Memory access patterns | Cache-oblivious algorithms |
| EM | Electromagnetic emissions | Shielding |
Risk: Malformed ciphertexts could cause:
- Buffer overflows in deserialization
- Type confusion in FHE libraries
- Denial of service via invalid parameters
Mitigation:
- Validate all incoming ciphertexts
- Use library-provided deserialization only
- Implement size limits and sanity checks
Before deploying any FHE smart contract:
┌─────────────────────────────────────────────────────────────┐
│ FHE DEPLOYMENT CHECKLIST │
├─────────────────────────────────────────────────────────────┤
│ [ ] All FHE arithmetic operations have overflow guards │
│ [ ] ACL permissions use transient where possible │
│ [ ] Async callbacks invalidate state before external calls │
│ [ ] Async callbacks authenticated with onlyGateway modifier│
│ [ ] Avoid decrypt in require/assert for confidential data │
│ [ ] Auction logic compares transferred, not requested │
│ [ ] No arbitrary external calls in privileged contexts │
│ [ ] Finality delays on sensitive reveals │
│ [ ] AA environments clean transient storage │
│ [ ] All ciphertext inputs validated via proofs │
│ [ ] Gas limits tested for worst-case FHE operations │
└─────────────────────────────────────────────────────────────┘
| Area | What to Check |
|---|---|
FHE.allow() |
Is caller permission verified? Transient preferred? |
FHE.select() |
Are both branches safe? No info leak in selection? |
| Callbacks | State invalidated? Gateway-only callable? Authentication enforced? |
| Arithmetic | Overflow guards? Clamping strategies? |
| Comparisons | Avoid decrypt in require/assert if leaking confidential data? |
| External calls | Blocklisted sensitive targets? |
Challenge: You can't inspect encrypted values in tests.
Solutions:
// 1. Mock mode for development
contract ConfidentialToken {
bool public mockMode;
function _transfer(euint64 amount) internal {
if (mockMode) {
// Use plaintext for testing
_mockTransfer(FHE.decrypt(amount));
} else {
// Production encrypted logic
_encryptedTransfer(amount);
}
}
}
// 2. Event-based verification (decrypt in test)
event BalanceUpdated(address indexed user, bytes32 encryptedBalance);
// 3. Formal verification of logic structure
// Use symbolic execution to verify pathsCore Challenge: You cannot inspect encrypted values during execution—this is by design. Debugging FHE contracts requires different strategies.
Instead of reverting (which leaks information), maintain encrypted error states:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {FHE, euint8, euint64, ebool} from "@fhenix/cofhe-contracts/FHE.sol";
contract DebugableToken {
// Encrypted state
mapping(address => euint64) private _balances;
mapping(address => euint8) public lastError;
uint64 public constant MAX_SUPPLY = type(uint64).max / 2;
// Error codes (must be initialized in constructor, not at declaration)
euint8 public ERR_NONE;
euint8 public ERR_INSUFFICIENT;
euint8 public ERR_OVERFLOW;
constructor() {
ERR_NONE = FHE.asEuint8(0);
ERR_INSUFFICIENT = FHE.asEuint8(1);
ERR_OVERFLOW = FHE.asEuint8(2);
}
function transfer(address to, euint64 amount) external returns (ebool) {
ebool sufficient = FHE.gte(_balances[msg.sender], amount);
ebool noOverflow = FHE.lte(FHE.add(_balances[to], amount), FHE.asEuint64(MAX_SUPPLY));
// Store encrypted error code instead of reverting
lastError[msg.sender] = FHE.select(
sufficient,
FHE.select(noOverflow, ERR_NONE, ERR_OVERFLOW),
ERR_INSUFFICIENT
);
ebool success = FHE.and(sufficient, noOverflow);
// ... transfer logic using FHE.select ...
return success;
}
// User can check their last error (only they can decrypt)
function getLastError() external view returns (euint8) {
FHE.allowTransient(lastError[msg.sender], msg.sender);
return lastError[msg.sender];
}
}Emit ciphertext handles for off-chain analysis:
event DebugCheckpoint(
string label,
bytes32 indexed handle,
address indexed actor,
uint256 timestamp
);
function _debug(string memory label, euint64 ct) internal {
emit DebugCheckpoint(label, FHE.getHandle(ct), msg.sender, block.timestamp);
}
function complexOperation(euint64 input) external {
_debug("input_received", input);
euint64 step1 = FHE.mul(input, multiplier);
_debug("after_multiply", step1);
euint64 step2 = FHE.add(step1, offset);
_debug("after_add", step2);
// In tests, decrypt handles to verify intermediate values
}Caution
Encrypted variables CANNOT be initialized at declaration. This is a common mistake.
// ❌ WRONG: Will fail to compile or behave unexpectedly
contract BadInit {
euint64 private value = FHE.asEuint64(0); // NOT ALLOWED
}
// ✅ CORRECT: Initialize in constructor
contract GoodInit {
euint64 private value;
constructor() {
value = FHE.asEuint64(0);
}
}The FHE transpiler has constraints that affect contract design:
| Pattern | Supported? | Workaround |
|---|---|---|
| Variable loop bounds | ❌ No | Use fixed-iteration loops with conditional execution |
| Early returns | ❌ No | Use result accumulator pattern |
| Dynamic arrays | ❌ No | Use fixed-size arrays |
| Recursion | ❌ No | Flatten to iterative |
// ❌ WRONG: Dynamic loop
function sum(euint64[] memory values) external returns (euint64) {
euint64 total = FHE.asEuint64(0);
for (uint i = 0; i < values.length; i++) { // Dynamic!
total = FHE.add(total, values[i]);
}
return total;
}
// ✅ CORRECT: Fixed iteration with bounds
uint256 public constant MAX_VALUES = 10;
function sum(euint64[MAX_VALUES] memory values, uint256 count) external returns (euint64) {
require(count <= MAX_VALUES, "Too many values");
euint64 total = FHE.asEuint64(0);
for (uint i = 0; i < MAX_VALUES; i++) { // Fixed bound
ebool shouldAdd = FHE.asEbool(i < count);
total = FHE.select(shouldAdd, FHE.add(total, values[i]), total);
}
return total;
}Complete workflow for testing FHE contracts from encryption to decryption.
// hardhat.config.js
require("@fhenix/hardhat-plugin");
module.exports = {
solidity: "0.8.20",
networks: {
fhenixLocal: {
url: "http://localhost:8545",
accounts: [process.env.PRIVATE_KEY],
},
},
fhenix: {
// Use mock mode for fast local testing
mockMode: true,
},
};const { expect } = require("chai");
const { ethers } = require("hardhat");
const { createCofheInstance } = require("@fhenix/cofhejs");
describe("Confidential Token E2E", function() {
let token, owner, alice, bob;
let cofhe;
before(async function() {
[owner, alice, bob] = await ethers.getSigners();
// Deploy token
const Token = await ethers.getContractFactory("SecureFHERC20");
token = await Token.deploy();
// Initialize FHE client
cofhe = await createCofheInstance({
provider: ethers.provider,
});
});
it("should transfer encrypted amounts correctly", async function() {
// 1. Mint to Alice (plaintext for setup)
await token.mint(alice.address, 1000);
// 2. Alice encrypts transfer amount client-side
const encAmount = await cofhe.encrypt(100, "uint64");
const proof = await cofhe.generateProof(encAmount);
// 3. Execute encrypted transfer
const tx = await token.connect(alice).transfer(
bob.address,
encAmount,
proof
);
await tx.wait();
// 4. Request decryption of Bob's balance
const bobBalanceHandle = await token.balanceOf(bob.address);
// 5. In mock mode, we can decrypt directly
const bobBalance = await cofhe.unseal(bobBalanceHandle, bob);
expect(bobBalance).to.equal(100);
// 6. Verify Alice's balance decreased
const aliceBalanceHandle = await token.balanceOf(alice.address);
const aliceBalance = await cofhe.unseal(aliceBalanceHandle, alice);
expect(aliceBalance).to.equal(900);
});
it("should handle insufficient balance gracefully", async function() {
// Try to transfer more than balance
const encAmount = await cofhe.encrypt(10000, "uint64");
const proof = await cofhe.generateProof(encAmount);
await token.connect(alice).transfer(bob.address, encAmount, proof);
// Check error code instead of revert
const errorHandle = await token.getLastError();
const errorCode = await cofhe.unseal(errorHandle, alice);
expect(errorCode).to.equal(1); // ERR_INSUFFICIENT
});
});// test/helpers/mockGateway.js
class MockGateway {
constructor(cofhe) {
this.cofhe = cofhe;
this.pendingDecryptions = new Map();
}
async requestDecryption(handle, callback) {
// Simulate network delay
await new Promise(r => setTimeout(r, 100));
// Decrypt using test key
const plaintext = await this.cofhe.decrypt(handle);
// Call the callback
await callback(handle, plaintext);
}
async processAllPending() {
for (const [handle, callback] of this.pendingDecryptions) {
await this.requestDecryption(handle, callback);
}
this.pendingDecryptions.clear();
}
}
module.exports = { MockGateway };# .github/workflows/fhe-tests.yml
name: FHE Contract Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Compile contracts
run: npx hardhat compile
- name: Run FHE tests (mock mode)
run: npx hardhat test --network fhenixLocal
env:
FHENIX_MOCK_MODE: true
- name: Run gas profiling
run: npx hardhat test --network fhenixLocal --gas-report// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {FHE, euint64, ebool} from "@fhenix/cofhe-contracts/FHE.sol";
contract SecureFHERC20 {
mapping(address => euint64) private _balances;
euint64 private _totalSupply;
// Maximum safe value to prevent overflow
uint64 public constant MAX_SUPPLY = type(uint64).max / 2;
function transfer(address to, euint64 amount) external returns (ebool) {
require(to != address(0), "Zero address");
require(to != msg.sender, "Self-transfer");
// Check sufficient balance
ebool sufficient = FHE.gte(_balances[msg.sender], amount);
// Check for overflow in recipient
euint64 newRecipientBalance = FHE.add(_balances[to], amount);
ebool noOverflow = FHE.gte(
FHE.asEuint64(MAX_SUPPLY),
newRecipientBalance
);
// Both conditions must be true
ebool canTransfer = FHE.and(sufficient, noOverflow);
// Atomic update using select
_balances[msg.sender] = FHE.select(
canTransfer,
FHE.sub(_balances[msg.sender], amount),
_balances[msg.sender]
);
_balances[to] = FHE.select(
canTransfer,
newRecipientBalance,
_balances[to]
);
return canTransfer;
}
// Secure balance reveal with permit
function revealBalance(address owner) external returns (uint64) {
require(msg.sender == owner || _hasPermit(msg.sender, owner));
FHE.allowTransient(_balances[owner], msg.sender);
return FHE.decrypt(_balances[owner]);
}
}contract SecureBlindAuction {
euint64 private highestBid;
address private highestBidder;
mapping(address => euint64) private bids;
uint256 public auctionEnd;
uint256 public revealBlock;
uint256 public constant FINALITY_DELAY = 32;
bool public auctionEnded;
bool public winnerRevealed;
function placeBid(euint64 encryptedBid, bytes calldata proof) external {
require(block.timestamp < auctionEnd, "Auction ended");
// Validate the ciphertext
euint64 validatedBid = FHE.asEuint64(encryptedBid, proof);
// Transfer bid amount (escrow)
euint64 transferred = token.encryptedTransferFrom(
msg.sender,
address(this),
validatedBid
);
// Verify actual transfer occurred
ebool validTransfer = FHE.gt(transferred, FHE.asEuint64(0));
// Update highest bid only if:
// 1. Transfer succeeded
// 2. Bid is higher than current highest
ebool isNewHighest = FHE.and(
validTransfer,
FHE.gt(transferred, highestBid)
);
// Update state
highestBid = FHE.select(isNewHighest, transferred, highestBid);
bids[msg.sender] = FHE.add(bids[msg.sender], transferred);
// Note: Address of highest bidder is tracked separately
// In production, use a mapping of bid -> address and resolve
// during the reveal phase after finality delay
_recordBidder(msg.sender, transferred);
}
function endAuction() external {
require(block.timestamp >= auctionEnd, "Auction not ended");
require(!auctionEnded, "Already ended");
auctionEnded = true;
revealBlock = block.number + FINALITY_DELAY;
}
function revealWinner() external {
require(auctionEnded, "Not ended");
require(block.number >= revealBlock, "Finality delay");
require(!winnerRevealed, "Already revealed");
winnerRevealed = true;
// Now safe to reveal winner
}
}contract SecurePrivateVoting {
struct Proposal {
euint64 yesVotes;
euint64 noVotes;
uint256 endTime;
bool tallied;
}
mapping(uint256 => Proposal) public proposals;
mapping(uint256 => mapping(address => bool)) public hasVoted;
function vote(uint256 proposalId, ebool encryptedVote) external {
Proposal storage p = proposals[proposalId];
require(block.timestamp < p.endTime, "Voting ended");
require(!hasVoted[proposalId][msg.sender], "Already voted");
hasVoted[proposalId][msg.sender] = true;
// Convert bool to uint64 for arithmetic
euint64 voteAsUint = FHE.asEuint64(encryptedVote);
euint64 one = FHE.asEuint64(1);
euint64 zero = FHE.asEuint64(0);
p.yesVotes = FHE.add(
p.yesVotes,
FHE.select(encryptedVote, one, zero)
);
p.noVotes = FHE.add(
p.noVotes,
FHE.select(encryptedVote, zero, one)
);
}
function tallyVotes(uint256 proposalId) external {
Proposal storage p = proposals[proposalId];
require(block.timestamp >= p.endTime, "Voting not ended");
require(!p.tallied, "Already tallied");
// Only reveal totals, not individual votes
p.tallied = true;
// Decrypt and emit results
}
}// ❌ BAD: Permanent access
FHE.allow(ciphertext, user);
// ✅ GOOD: Transaction-scoped access
FHE.allowTransient(ciphertext, user);
// ✅ GOOD: Time-limited access
function grantTemporaryAccess(euint64 ct, address user, uint256 duration) {
accessExpiry[ct][user] = block.timestamp + duration;
FHE.allowTransient(ct, user);
}// Two-step delegation with approval
mapping(address => mapping(address => bool)) public delegateApprovals;
function approveDelegation(address delegate) external {
delegateApprovals[msg.sender][delegate] = true;
}
function delegatedAccess(euint64 data, address owner) external {
require(delegateApprovals[owner][msg.sender], "Not approved");
require(FHE.isAllowed(data, owner), "Owner lacks permission");
// Grant transient only
FHE.allowTransient(data, msg.sender);
}- Entropy Quality: Use cryptographically secure RNG
- Key Storage: Never store keys in localStorage (use secure enclaves)
- Session Keys: Rotate keys periodically
- Key Derivation: Use proper KDF (HKDF, Argon2)
// EIP-712 style permit for FHE access
struct AccessPermit {
address owner;
address spender;
bytes32 ciphertextHash;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
function permitAccess(AccessPermit calldata permit) external {
require(block.timestamp <= permit.deadline, "Permit expired");
bytes32 digest = keccak256(abi.encode(
PERMIT_TYPEHASH,
permit.owner,
permit.spender,
permit.ciphertextHash,
nonces[permit.owner]++,
permit.deadline
));
address signer = ecrecover(digest, permit.v, permit.r, permit.s);
require(signer == permit.owner, "Invalid signature");
// Grant transient access
FHE.allowTransient(ciphertexts[permit.ciphertextHash], permit.spender);
}| Optimization | Safe? | Notes |
|---|---|---|
| Batch FHE operations | ✅ | Reduces overhead |
| Lazy evaluation | Only if state is consistent | |
| Smaller encrypted types | Check overflow risk | |
| Ciphertext caching | Ensure freshness | |
| Skip redundant ACL checks | ❌ | Security risk |
// ❌ DANGEROUS: Skipping validation for gas
function unsafeTransfer(euint64 amount) external {
// Skip balance check to save gas - NEVER DO THIS
_balances[msg.sender] = FHE.sub(_balances[msg.sender], amount);
}
// ❌ DANGEROUS: Reusing ciphertexts without re-validation
// Attacker could have modified storage| Traditional Audit | FHE Audit |
|---|---|
| Look for reentrancy | Look for callback replay |
| Check overflow/underflow reverts | Check for silent overflow handling |
| Validate access control | Validate ACL configurations |
| Test with concrete values | Test with encrypted value patterns |
| Focus on data exposure | Focus on information leakage channels |
flowchart TB
A[1. Scope Definition] --> B[2. Architecture Review]
B --> C[3. FHE-Specific Analysis]
C --> D[4. Traditional Smart Contract Audit]
D --> E[5. Integration Testing]
E --> F[6. Report & Remediation]
subgraph "FHE-Specific Analysis"
C1[ACL Configuration Review]
C2[Arithmetic Safety Check]
C3[Async Pattern Analysis]
C4[Information Leakage Assessment]
end
C --> C1
C --> C2
C --> C3
C --> C4
- FHE library version is recent and patched
- Security level is at least 128-bit
- Parameters match production recommendations
- No deprecated schemes (LTV, BLLN)
- All
FHE.allow()preceded by permission checks - Transient permissions used where possible
- No over-broad permission grants
- Helper contracts don't create disclosure oracles
- Delegation follows two-step approval
- No encrypted comparisons in require/assert
- No branching based on decrypted values
- Timing-safe patterns for sensitive operations
- Metadata (gas, access patterns) analyzed
- All multiplications have overflow guards
- Division checked for reasonable results
- FHE.select used for conditional updates
- Maximum value bounds enforced
- Request IDs are unique and unpredictable
- State invalidated before callbacks execute
- Only authorized gateway can call callbacks
- Reentrancy protection on callback functions
- Finality delays on sensitive reveals
- Client-side encryption uses secure RNG
- Permit signatures include replay protection
- Key rotation mechanisms exist
- Threshold configuration is appropriate
| Pattern | Risk | Location |
|---|---|---|
require(FHE.decrypt(...) > X) |
Info leak | Comparisons |
FHE.allow(ct, untrustedContract) |
Disclosure oracle | ACL |
| No state deletion before callback | Replay attack | Async |
execute(target, data) without blocklist |
ACL bypass | External calls |
| Pattern | Risk | Location |
|---|---|---|
FHE.mul(a, b) without overflow check |
Under-charging | Arithmetic |
| Only requested amount compared in auctions | Fake bids | Auction logic |
| Immediate reveal after auction end | Reorg info leak | Reveals |
| Persistent permission on every call | Over-permissioning | ACL |
| Pattern | Risk | Location |
|---|---|---|
| Complex permission logic | Likely misconfiguration | ACL |
| No finality delay on reveals | Reorg vulnerability | Timing |
| Gas optimization shortcuts | Various | Performance |
| Missing transient cleanup in AA | Cross-user leak | AA |
Finding: Loan amount overflow allowing under-collateralization
// VULNERABLE
function borrow(euint64 amount) external {
euint64 collateralRequired = FHE.mul(amount, collateralRatio);
// collateralRequired wraps on large amount!
require(FHE.gte(deposits[msg.sender], collateralRequired));
}Fix:
// SECURE
function borrow(euint64 amount) external {
ebool validAmount = FHE.lte(amount, FHE.asEuint64(MAX_BORROW));
euint64 collateralRequired = FHE.mul(amount, collateralRatio);
ebool sufficient = FHE.gte(deposits[msg.sender], collateralRequired);
ebool canBorrow = FHE.and(validAmount, sufficient);
// Proceed only if canBorrow is true (via FHE.select)
}Finding: Winner could learn auction parameters before finality
Attack Scenario:
- Alice wins auction in block N
- Alice decrypts prize info
- Reorg replaces block N, Bob now wins
- Alice still has decrypted info
Fix: Implement 32-block finality delay before any reveals.
Finding: Helper contract became disclosure oracle
// VULNERABLE - PriceOracle granted permanent access
function getPrice(euint64 amount) external {
FHE.allow(amount, address(priceOracle));
return priceOracle.calculate(amount);
}
// PriceOracle contract:
function calculate(euint64 amount) external returns (uint256) {
uint256 plainAmount = FHE.decrypt(amount); // DISCLOSURE!
return plainAmount * price;
}Fix:
- Use transient permissions
- Verify caller owns the ciphertext
- Price oracle should compute on encrypted values
| Tool | Purpose | FHE Support |
|---|---|---|
| Slither | General Solidity analysis | Limited - custom detectors needed |
| Mythril | Symbolic execution | No native FHE support |
| Custom rules | FHE-specific patterns | Build for each protocol |
# Fhenix local test environment
npm install @fhenix/cofhe-hardhat-plugin
# Run with mock FHE for faster testing
npx hardhat test --network fhenix-mockFor critical FHE contracts, consider:
- Certora Prover (custom specifications)
- Runtime verification
- Property-based testing with symbolic inputs
Invariants are properties that must always hold regardless of contract state or transaction sequence. For FHE contracts, these are critical for security proofs.
These ensure value is neither created nor destroyed:
// INV-1: Total Supply Conservation
// ∀ transfers: sum(all_balances) == totalSupply
//
// In FHE, we can't directly sum encrypted values, but we can:
// 1. Track encrypted totalSupply separately
// 2. Verify conservation at each operation
function transfer(euint64 amount) external {
// Before: sender_balance + receiver_balance + others = total
_balances[msg.sender] = FHE.sub(_balances[msg.sender], amount);
_balances[to] = FHE.add(_balances[to], amount);
// After: (sender - amount) + (receiver + amount) + others = total
// Invariant preserved by construction
}
// INV-2: Mint/Burn Conservation
function mint(address to, euint64 amount) external onlyMinter {
_balances[to] = FHE.add(_balances[to], amount);
_totalSupply = FHE.add(_totalSupply, amount);
// totalSupply increases exactly by minted amount
}Ensure ACL consistency:
// INV-3: No Unauthorized Decryption
// ∀ ciphertexts ct, users u:
// canDecrypt(ct, u) ⟹ ACL.isAllowed(ct, u) ∨ u == owner(ct)
// INV-4: Permission Monotonicity (optional)
// Once permission is revoked, it cannot be re-granted without owner consent
// Useful for compliance scenarios
// INV-5: Transient Permission Scope
// Transient permissions MUST NOT survive transaction boundaries// INV-6: Auction State Machine
// States: BIDDING → ENDED → REVEALED → SETTLED
// BIDDING: bids can be placed, winner unknown
// ENDED: no new bids, winner still encrypted
// REVEALED: winner decrypted, awaiting settlement
// SETTLED: funds distributed, immutable
// Invariant: state can only move forward
modifier validStateTransition(State newState) {
require(uint(newState) > uint(currentState), "Invalid transition");
_;
}// Express invariants as view functions for testing/verification
contract InvariantChecks {
// Returns encrypted bool - true if invariant holds
function checkConservationInvariant() external view returns (ebool) {
euint64 computedTotal = FHE.asEuint64(0);
for (uint i = 0; i < knownAddresses.length; i++) {
computedTotal = FHE.add(computedTotal, _balances[knownAddresses[i]]);
}
return FHE.eq(computedTotal, _totalSupply);
}
}Standard fuzzing needs adaptation for FHE because encrypted values can't be directly inspected.
| Standard Approach | FHE Problem |
|---|---|
| Inspect return values | Values are encrypted |
| Check balance changes | Balances are encrypted |
| Detect reverts | FHE uses select, not revert |
| Random inputs | Must be valid ciphertexts |
# echidna.config.yaml
testMode: assertion
corpusDir: "corpus"
testLimit: 50000
shrinkLimit: 5000
# FHE-specific settings
deployer: "0x1234..." # Account with decrypt permission
sender: ["0x1234...", "0x5678..."] # Test multiple users
# Property definitions
assertionMode: true// contracts/test/FHEFuzz.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "../SecureFHERC20.sol";
contract FHEFuzz is SecureFHERC20 {
uint256 private _expectedTotalSupply;
constructor() {
// Initialize with known state for invariant checking
_expectedTotalSupply = 1_000_000;
_mint(msg.sender, _expectedTotalSupply);
}
// PROPERTY 1: Total supply never changes (no mint/burn in transfer)
function echidna_total_supply_constant() public view returns (bool) {
// In mock mode, we can decrypt for testing
return FHE.decrypt(_totalSupply) == _expectedTotalSupply;
}
// PROPERTY 2: No single balance exceeds total supply
function echidna_balance_bounded(address user) public view returns (bool) {
uint64 balance = FHE.decrypt(_balances[user]);
return balance <= _expectedTotalSupply;
}
// PROPERTY 3: Transfer doesn't create value
function echidna_transfer_conserves(
address to,
uint64 amount
) public returns (bool) {
uint64 senderBefore = FHE.decrypt(_balances[msg.sender]);
uint64 receiverBefore = FHE.decrypt(_balances[to]);
uint64 sumBefore = senderBefore + receiverBefore;
// Execute transfer (with encrypted amount for realism)
euint64 encAmount = FHE.asEuint64(amount);
transfer(to, encAmount);
uint64 senderAfter = FHE.decrypt(_balances[msg.sender]);
uint64 receiverAfter = FHE.decrypt(_balances[to]);
uint64 sumAfter = senderAfter + receiverAfter;
return sumAfter == sumBefore;
}
// PROPERTY 4: ACL permissions are consistent
function echidna_acl_consistency() public view returns (bool) {
// Verify that all stored ciphertexts have valid ACL entries
return true; // Implement based on ACL structure
}
}# Install Echidna
pip install echidna
# Run with FHE mock mode enabled
FHENIX_MOCK_MODE=true echidna contracts/test/FHEFuzz.sol \
--config echidna.config.yaml \
--contract FHEFuzz
# Example output:
# echidna_total_supply_constant: passed! 🎉
# echidna_balance_bounded: passed! 🎉
# echidna_transfer_conserves: FAILED! ❌
# Counterexample: transfer(0x..., 18446744073709551615)
# → Overflow detected in conservation propertycontract ACLStatefulFuzz {
SecureFHERC20 token;
mapping(address => mapping(bytes32 => bool)) expectedPermissions;
function allow_permission(address user, euint64 ct) public {
bytes32 handle = FHE.getHandle(ct);
FHE.allow(ct, user);
expectedPermissions[user][handle] = true;
}
function revoke_permission(address user, euint64 ct) public {
bytes32 handle = FHE.getHandle(ct);
FHE.revoke(ct, user);
expectedPermissions[user][handle] = false;
}
// Property: ACL state matches our expectations
function echidna_acl_sync() public view returns (bool) {
// Verify all expected permissions match actual state
return true; // Implement verification logic
}
}| Property Type | Verifiable? | Approach |
|---|---|---|
| Control flow correctness | ✅ Yes | Standard symbolic execution |
| Arithmetic bounds | Bounds on plaintext operations only | |
| ACL consistency | ✅ Yes | State machine verification |
| Information flow | Taint analysis on handles | |
| Cryptographic security | ❌ No | Assumed from library |
// specs/FHEToken.spec
methods {
function totalSupply() external returns (uint256) envfree;
function balanceOf(address) external returns (uint256) envfree;
function transfer(address, uint256) external returns (bool);
}
// Ghost variable to track sum of all balances
ghost mathint sumBalances {
init_state axiom sumBalances == 0;
}
// Hook to update ghost on balance changes
hook Sstore _balances[KEY address user] uint256 newBalance
(uint256 oldBalance) STORAGE {
sumBalances = sumBalances - oldBalance + newBalance;
}
// RULE: Total supply equals sum of all balances
invariant totalSupplyInvariant()
to_mathint(totalSupply()) == sumBalances;
// RULE: Transfer preserves total supply
rule transferPreservesTotal(address from, address to, uint256 amount) {
env e;
require e.msg.sender == from;
mathint totalBefore = to_mathint(totalSupply());
transfer(e, to, amount);
mathint totalAfter = to_mathint(totalSupply());
assert totalBefore == totalAfter,
"Transfer must preserve total supply";
}
// RULE: No unauthorized balance increase
rule noUnauthorizedIncrease(address user) {
env e;
require e.msg.sender != user;
uint256 balanceBefore = balanceOf(user);
// Any function call by non-user
calldataarg args;
f(e, args);
uint256 balanceAfter = balanceOf(user);
// Balance can only increase via explicit transfer TO user
assert balanceAfter >= balanceBefore ||
f.selector == sig:transfer(address,uint256).selector,
"Balance cannot decrease without user action";
}
| Scenario | Recommended Approach |
|---|---|
| State machine correctness | Formal verification |
| Arithmetic overflow | Fuzzing + formal |
| ACL logic | Formal verification |
| Complex multi-step attacks | Fuzzing |
| Gas optimization validation | Fuzzing |
| Protocol-level invariants | Formal verification |
FHE-enabled Web3 applications typically fall into these categories:
| Category | Description | Security Considerations |
|---|---|---|
| Confidential DeFi | Trading, lending, DEX with encrypted state | Order book privacy, MEV protection |
| Private Governance | Encrypted voting and proposals | Vote privacy, threshold reveal |
| Confidential Tokens | FHERC20-style encrypted balances | ACL management, overflow handling |
| Sealed-Bid Auctions | Hidden bids until reveal | Finality delays, replay protection |
Note
For current production applications, refer to the official documentation of Fhenix and Zama for up-to-date ecosystem projects.
Private Prediction Market Pattern:
- Challenge: Hide bet amounts until resolution
- Solution: Encrypted positions with threshold reveal
- Security: Two-step reveal with finality delay
Confidential Credit Scoring Pattern:
- Challenge: Compute credit score without revealing data
- Solution: FHE computation on encrypted financial history
- Security: Zero-knowledge proof of computation correctness
-
Cross-chain FHE Attacks
- Ciphertext replay across chains
- Key material leakage via bridges
-
AI-Assisted Cryptanalysis
- ML models for lattice attacks
- Pattern recognition in ciphertexts
-
Coprocessor Trust Assumptions
- Malicious coprocessor detection
- Verifiable FHE computation
-
ZK Proofs of FHE Computation
- Prove correct computation without trusted coprocessor
- In development by Zama and others
-
Threshold Key Refresh
- Proactive secret sharing
- Automatic key rotation
-
Hardware-Backed FHE
- FHE accelerators with secure enclaves
- Tamper-resistant execution
| Threat | FHE Status | Timeline |
|---|---|---|
| Shor's Algorithm | ✅ Safe (lattice-based) | N/A |
| Grover's Algorithm | ✅ Mitigated (256-bit key space) | Already addressed |
| Unknown quantum attacks | ? | Unknown |
Note
Current FHE schemes are quantum-resistant. Grover's algorithm provides quadratic speedup (reducing 256-bit security to 128-bit against quantum computers), which is why modern implementations use 256-bit key spaces to maintain 128-bit post-quantum security. This is already accounted for in current parameter selections.
| Improvement | Security Impact |
|---|---|
| Smaller parameters | |
| Approximate (CKKS) | |
| Reduced threshold | |
| Ciphertext compression |
Challenges:
- Different chains have different trust assumptions
- Bridge protocols introduce attack surface
- Key material must not leak across chains
Recommendations:
- Use chain-specific encryption keys
- Implement ciphertext domain separation
- Audit bridge contracts for ciphertext handling
Note
Bug bounty amounts and programs change frequently. Always verify current bounty details from official sources.
| Protocol | Bounty Program | Verify At |
|---|---|---|
| Fhenix | Immunefi | immunefi.com |
| Zama | Direct submission | zama.ai |
- Join ecosystem audit firms
- Contribute to open-source security tools
- Participate in audit competitions
- Academic partnerships with Zama, Fhenix
- FHE standardization working groups
- Open-source contribution to TFHE-rs, concrete
FHE systems have unique incident types requiring specialized response procedures.
| Incident Type | Severity | Detection Method | Initial Response |
|---|---|---|---|
| Single TDN node compromise | Medium | Node health monitoring | Isolate node, continue operation |
| Threshold (t) nodes compromised | Critical | Anomalous decryption patterns | Pause all operations |
| ACL bypass vulnerability | Critical | Audit finding or exploit | Pause affected contracts |
| Ciphertext replay detected | High | Transaction monitoring | Invalidate affected handles |
| Gateway compromise | Critical | Callback anomalies | Switch to backup gateway |
| Key material exposure | Critical | External report | Emergency key rotation |
Threshold Compromise Response:
// Emergency pause pattern for FHE contracts
contract EmergencyPausable {
address public guardian;
bool public paused;
uint256 public pausedAt;
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
modifier whenPaused() {
require(paused, "Contract is not paused");
_;
}
// Guardian can pause immediately
function emergencyPause() external {
require(msg.sender == guardian, "Not guardian");
paused = true;
pausedAt = block.timestamp;
emit EmergencyPaused(msg.sender, block.timestamp);
}
// Unpause requires timelock for safety
uint256 public constant UNPAUSE_DELAY = 48 hours;
function unpause() external {
require(msg.sender == guardian, "Not guardian");
require(block.timestamp >= pausedAt + UNPAUSE_DELAY, "Timelock");
paused = false;
emit Unpaused(msg.sender);
}
// All FHE operations check pause state
function transfer(euint64 amount) external whenNotPaused {
// ... transfer logic
}
}Post-Incident Checklist:
## FHE Incident Response Checklist
### Immediate (0-1 hour)
- [ ] Activate emergency pause if warranted
- [ ] Notify TDN operators
- [ ] Preserve logs and transaction history
- [ ] Begin impact assessment
### Short-term (1-24 hours)
- [ ] Identify root cause
- [ ] Determine scope of compromise
- [ ] Notify affected users (if data exposed)
- [ ] Prepare fix or mitigation
### Recovery (24-72 hours)
- [ ] Deploy patched contracts (if needed)
- [ ] Initiate key rotation (if required)
- [ ] Re-encrypt affected data
- [ ] Conduct post-mortemKey rotation is essential for long-term security of FHE systems.
| Trigger | Urgency | Scope |
|---|---|---|
| Scheduled rotation | Low | Full system |
| TDN node compromise (< t) | Medium | Affected key shares |
| Threshold compromise | Critical | Full emergency rotation |
| Library vulnerability disclosed | Medium | Assess and plan |
| Employee departure (key custodian) | Medium | Affected ceremonies |
Lazy Re-encryption (Recommended for most cases):
contract LazyReencryption {
uint256 public currentKeyEpoch;
mapping(bytes32 => uint256) public ciphertextEpoch;
// Check if ciphertext needs re-encryption before use
function ensureCurrentEpoch(euint64 ct) internal returns (euint64) {
bytes32 handle = FHE.getHandle(ct);
if (ciphertextEpoch[handle] < currentKeyEpoch) {
// Request async re-encryption
euint64 newCt = FHE.reencrypt(ct);
ciphertextEpoch[FHE.getHandle(newCt)] = currentKeyEpoch;
return newCt;
}
return ct;
}
function transfer(address to, euint64 amount) external {
// Ensure sender balance is current
_balances[msg.sender] = ensureCurrentEpoch(_balances[msg.sender]);
// ... rest of transfer
}
}Eager Re-encryption (For small state sets):
contract EagerReencryption {
function rotateAllBalances() external onlyAdmin {
for (uint i = 0; i < users.length; i++) {
_balances[users[i]] = FHE.reencrypt(_balances[users[i]]);
}
currentKeyEpoch++;
}
}sequenceDiagram
participant Admin
participant Gateway
participant Node1 as TDN Node 1
participant Node2 as TDN Node 2
participant Node3 as TDN Node 3
Admin->>Gateway: InitiateKeyRefresh(newEpoch)
Gateway->>Node1: GenerateNewShare(epoch)
Gateway->>Node2: GenerateNewShare(epoch)
Gateway->>Node3: GenerateNewShare(epoch)
Node1-->>Gateway: NewShare1
Node2-->>Gateway: NewShare2
Node3-->>Gateway: NewShare3
Gateway->>Gateway: Verify shares combine to valid key
Gateway->>Admin: KeyRefreshComplete(newPubKey)
Production FHE systems require specialized monitoring beyond standard blockchain metrics.
| Metric | Normal Range | Alert Threshold | Indicates |
|---|---|---|---|
| Decrypt requests/min | 10-100 | >500 | Possible exfiltration |
| Failed ACL checks/hour | <1% of requests | >5% | Access control attack |
| Callback failure rate | <0.1% | >1% | Gateway issues |
| TDN node response time | <2s | >10s | Node health |
| Gas per FHE operation | Baseline ±20% | >50% variance | Unusual computation |
| Unique ciphertext handles/hour | Normal range | >3 std dev | Unusual activity |
# prometheus/fhe-alerts.yaml
groups:
- name: fhe-security
rules:
- alert: HighDecryptionRate
expr: rate(fhe_decrypt_requests_total[5m]) > 100
for: 5m
labels:
severity: warning
annotations:
summary: "Unusual decryption rate detected"
- alert: ACLViolations
expr: rate(fhe_acl_denied_total[5m]) / rate(fhe_acl_checks_total[5m]) > 0.05
for: 10m
labels:
severity: critical
annotations:
summary: "High rate of ACL denials"
- alert: TDNNodeUnhealthy
expr: fhe_tdn_node_response_time_seconds > 10
for: 2m
labels:
severity: warning
annotations:
summary: "TDN node responding slowly"
- alert: GatewayCallbackFailures
expr: rate(fhe_callback_failures_total[10m]) > 0.01
for: 15m
labels:
severity: high
annotations:
summary: "Gateway callback failures elevated"# monitoring/anomaly_detector.py
import numpy as np
from collections import defaultdict
class FHEAnomalyDetector:
def __init__(self, window_size=100):
self.window_size = window_size
self.user_patterns = defaultdict(list)
def record_operation(self, user: str, op_type: str, gas_used: int):
key = f"{user}:{op_type}"
self.user_patterns[key].append(gas_used)
# Keep rolling window
if len(self.user_patterns[key]) > self.window_size:
self.user_patterns[key].pop(0)
def is_anomalous(self, user: str, op_type: str, gas_used: int) -> bool:
key = f"{user}:{op_type}"
history = self.user_patterns[key]
if len(history) < 10:
return False # Not enough data
mean = np.mean(history)
std = np.std(history)
# Flag if >3 standard deviations from mean
return abs(gas_used - mean) > 3 * stdSafely composing FHE contracts requires careful ACL management.
| Challenge | Risk | Solution |
|---|---|---|
| ACL propagation | Permission inflation | Explicit transient grants |
| Cross-contract calls | Ciphertext escapes | Verify caller at each hop |
| Flash loan patterns | Temporary state inconsistency | Atomic operations |
| Delegate calls | Storage confusion | Never use with FHE state |
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {FHE, euint64, ebool} from "@fhenix/cofhe-contracts/FHE.sol";
interface IComposableCallback {
function onFHEReceived(
address from,
euint64 amount,
bytes calldata data
) external returns (bytes4);
}
contract ComposableFHERC20 {
bytes4 public constant CALLBACK_SUCCESS =
IComposableCallback.onFHEReceived.selector;
function transferWithCallback(
address to,
euint64 amount,
bytes calldata data
) external returns (ebool) {
ebool success = _transfer(msg.sender, to, amount);
// Only grant transient permission to receiver
FHE.allowTransient(amount, to);
// If receiver is contract, notify it
if (to.code.length > 0) {
bytes4 response = IComposableCallback(to).onFHEReceived(
msg.sender,
amount,
data
);
require(response == CALLBACK_SUCCESS, "Callback failed");
}
return success;
}
}
contract ComposableVault is IComposableCallback {
ComposableFHERC20 public token;
mapping(address => euint64) public deposits;
function onFHEReceived(
address from,
euint64 amount,
bytes calldata /* data */
) external override returns (bytes4) {
require(msg.sender == address(token), "Only token");
// Safely accumulate deposit
deposits[from] = FHE.add(deposits[from], amount);
return CALLBACK_SUCCESS;
}
}Combining ZK proofs with FHE provides both privacy and verifiability.
| Requirement | ZK Only | FHE Only | ZK + FHE |
|---|---|---|---|
| Prove statement about hidden data | ✅ | ❌ | ✅ |
| Compute on hidden data | ❌ | ✅ | ✅ |
| Verify computation correctness | Built-in | ✅ | |
| Multiple parties compute | ❌ | ✅ | ✅ |
| Performance critical | ✅ Faster | ❌ Slower |
// Prove encrypted value is within range without revealing it
contract ZKValidatedFHE {
// Verifier contract for ZK proofs
IGroth16Verifier public verifier;
function depositWithProof(
euint64 encryptedAmount,
bytes calldata rangeProof,
uint256[8] calldata zkProof
) external {
// 1. Verify ZK proof that plaintext is in valid range
// The proof attests: 0 < plaintext < MAX_DEPOSIT
// without revealing the actual value
uint256[2] memory publicInputs = [
uint256(FHE.getHandle(encryptedAmount)),
MAX_DEPOSIT
];
require(verifier.verify(zkProof, publicInputs), "Invalid range proof");
// 2. Now safe to use the encrypted value
deposits[msg.sender] = FHE.add(deposits[msg.sender], encryptedAmount);
}
}graph LR
subgraph "User Side"
A[Input] --> B[Encrypt]
B --> C[Send to Chain]
end
subgraph "FHE Coprocessor"
C --> D[FHE Computation]
D --> E[Generate ZK Proof]
end
subgraph "On-Chain Verification"
E --> F[Verify ZK Proof]
F --> G[Accept Result]
end
Upgrading FHE contracts requires special handling of encrypted state.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract FHEProxy is ERC1967Proxy {
// FHE state lives in implementation, not proxy
// This is safe because handles are deterministic
constructor(address implementation, bytes memory data)
ERC1967Proxy(implementation, data)
{}
}
contract FHETokenV1 {
mapping(address => euint64) internal _balances;
euint64 internal _totalSupply;
// ... V1 implementation
}
contract FHETokenV2 is FHETokenV1 {
// New storage MUST be appended, never reordered
mapping(address => euint64) internal _frozen;
// New functionality
function freeze(address user) external onlyAdmin {
_frozen[user] = _balances[user];
_balances[user] = FHE.asEuint64(0);
}
}Warning
FHE library upgrades may change ciphertext format. Always verify compatibility before upgrading.
contract VersionAwareFHE {
uint256 public constant CURRENT_FHE_VERSION = 2;
mapping(bytes32 => uint256) public ciphertextVersion;
function migrateIfNeeded(euint64 ct) internal returns (euint64) {
bytes32 handle = FHE.getHandle(ct);
uint256 version = ciphertextVersion[handle];
if (version < CURRENT_FHE_VERSION) {
// Decrypt with old version, re-encrypt with new
// This requires gateway support for version migration
return FHE.migrate(ct, version, CURRENT_FHE_VERSION);
}
return ct;
}
}## FHE Contract Upgrade Checklist
### Pre-Upgrade
- [ ] Verify new implementation against old storage layout
- [ ] Test upgrade path on testnet with production state snapshot
- [ ] Verify FHE library version compatibility
- [ ] Check for pending decryption requests
- [ ] Notify TDN operators of planned upgrade
### During Upgrade
- [ ] Pause contract (if possible)
- [ ] Execute upgrade transaction
- [ ] Verify proxy points to new implementation
- [ ] Run smoke tests on critical functions
### Post-Upgrade
- [ ] Unpause contract
- [ ] Monitor for anomalies
- [ ] Verify ciphertext operations work correctly
- [ ] Document migration for audit trailThis section documents unsolved problems and fundamental limitations in FHE for Web3. Honest acknowledgment of these gaps is essential for scientific rigor.
Problem: How do we prove that an FHE coprocessor computed correctly without trusting it?
Current State:
- Zama fhEVM and Fhenix CoFHE rely on trusted coprocessors
- Malicious coprocessor could return incorrect results
- ZK proofs of FHE computation are theoretically possible but prohibitively expensive
Research Directions:
| Approach | Status | Challenge |
|---|---|---|
| ZK-SNARKs for FHE | In development | Proof generation ~1000x slower than FHE |
| Optimistic verification | Proposed | Requires fraud proofs on encrypted data |
| Multi-coprocessor consensus | Proposed | Coordination overhead, trust assumptions |
| Verifiable arithmetic circuits | Research | Limited to specific operations |
References:
- Verifiable FHE - Initial feasibility study
- RISC Zero + FHE - Potential integration path
Important
Current Audit Stance: FHE coprocessor trust is a documented limitation, not a vulnerability. Audits should verify the threat model acknowledges this.
Problem: FHE operations are complex and may leak information through side channels.
Known Challenges:
| Attack Vector | Mitigation Status | Open Questions |
|---|---|---|
| Timing attacks | Partially mitigated | Bootstrapping reveals circuit depth |
| Cache-timing | Active research | Memory access patterns during NTT |
| Power analysis | Hardware-dependent | FHE accelerators create new surfaces |
| EM emissions | Not addressed | Especially on custom hardware |
Specific Concerns for Web3:
- Running FHE in TEEs (e.g., Intel SGX) introduces combined attack surfaces
- GPU/FPGA acceleration changes side-channel profile
- Cloud deployment exposes to hypervisor-level attacks
Research Gap: No formal side-channel security model for FHE exists.
Problem: Distributedly generating FHE keys is harder than RSA/EC distributed key gen.
Attack Surfaces in DKG for FHE:
| Component | Risk | Current Mitigation |
|---|---|---|
| Share reconstruction | Collusion of |
Threshold security assumption |
| Key share refresh | Proactive security needed | Complex protocols, not widely deployed |
| Malicious participant detection | Byzantine fault tolerance | Verifiable secret sharing (VSS) |
| Network partitioning | Split-brain scenarios | Consensus layer dependency |
Open Questions:
- What happens when DKG participants disagree during FHE computation?
- How to rotate FHE keys without re-encrypting all historical data?
- Optimal threshold
$(t, n)$ for security vs. availability?
Problem: Using FHE across multiple blockchains introduces new attack vectors.
Challenges:
| Issue | Description | Severity |
|---|---|---|
| Ciphertext replay | Same ciphertext valid on multiple chains | High |
| Key domain separation | Different chains may share key material | Critical |
| Bridge custody | Bridge holds decryption capability | Critical |
| Finality differences | Consensus on one chain, not on another | Medium |
No Standardized Solution Exists.
Problem: No mature tools exist for formally verifying FHE smart contract correctness.
Current State:
| Tool | FHE Support | Limitations |
|---|---|---|
| Certora | Custom specs needed | No native encrypted type support |
| Echidna | Requires mocks | Cannot test actual FHE operations |
| Slither | Limited | No FHE-specific detectors |
| Mythril | None | Symbolic execution fails on encrypted types |
Research Needed:
- Type systems for encrypted data flow
- Symbolic execution over ciphertext handles
- Property-based specification languages for FHE invariants
Problem: CKKS (approximate HE) offers performance benefits but inherent security risks.
Fundamental Tension:
- Higher precision → smaller noise flooding → easier CPAD attacks
- Aggressive noise flooding → unusable precision for ML
Current Guidance: Avoid CKKS for threshold decryption unless noise flooding is rigorously analyzed. Prefer exact schemes (BFV, BGV, TFHE) for Web3.
| Limitation | Impact | Mitigation Timeframe |
|---|---|---|
| Trusted coprocessor | Protocol trust assumption | 2-5 years (ZK-FHE) |
| Side-channel leakage | Implementation trust assumption | Ongoing research |
| DKG complexity | Key rotation challenges | 1-3 years |
| Cross-chain security | No standard exists | Unknown |
| Verification tooling | Audit difficulty | 2-3 years |
Note
For Auditors: These limitations should be documented in the audit scope. They are not vulnerabilities but accepted trust assumptions that clients must understand.
| ID | Category | Severity | Description | Mitigation |
|---|---|---|---|---|
| V1 | Arithmetic | High | Unchecked overflow | FHE.select with bounds |
| V2 | ACL | Critical | Missing authorization | Check before allow |
| V3 | ACL | High | Over-permissioning | Use transient |
| V4 | Async | Critical | Callback replay | Delete before execute |
| V5 | Auction | High | Fake winning bid | Compare transferred |
| V6 | Reveal | Medium | Reorg disclosure | Finality delay |
| V7 | External | Critical | ACL bypass via execute | Blocklist targets |
| V8 | AA | Medium | Transient leak | Clean storage |
| V9 | Impl | High | Side-channels | Constant-time |
| V10 | Impl | Medium | Serialization | Validate inputs |
# FHE Smart Contract Security Audit Report
## Executive Summary
- Protocol Name:
- Audit Period:
- Commit Hash:
- Severity Summary: X Critical, Y High, Z Medium, W Low
## Scope
- Contracts Audited:
- FHE Library Version:
- Out of Scope:
## Findings
### [C-01] Critical: [Title]
**Location:** `Contract.sol:L123`
**Description:**
**Impact:**
**Recommendation:**
**Protocol Response:**
### [H-01] High: [Title]
...
## FHE-Specific Checklist Results
- [ ] Cryptographic parameters validated
- [ ] ACL configuration reviewed
- [ ] Arithmetic safety verified
- [ ] Async patterns secured
- [ ] Information leakage assessed
## Appendix: Test Cases| Term | Definition |
|---|---|
| ACL | Access Control List - permissions for ciphertext operations |
| Bootstrapping | Noise reduction operation in FHE |
| Ciphertext | Encrypted data that can be computed on |
| CoFHE | Confidential FHE - Fhenix's coprocessor architecture |
| Disclosure Oracle | Vulnerability allowing unauthorized decryption |
| dBFV | Decomposed BFV - Fhenix's high-precision FHE scheme |
| fhEVM | Fully Homomorphic EVM - Zama's encrypted Ethereum VM |
| IND-CPA | Indistinguishability under Chosen Plaintext Attack |
| KMS | Key Management System |
| LWE | Learning With Errors - hardness assumption |
| Noise | Random error in ciphertexts, grows with operations |
| RLWE | Ring-LWE - efficient variant of LWE |
| TDN | Threshold Decryption Network |
| TFHE | Torus FHE - fast bootstrapping scheme |
| Transient Permission | Transaction-scoped ACL grant |
Curated from fhe.org - the community resource hub for FHE
| Paper | Authors | Year | Significance |
|---|---|---|---|
| Fully Homomorphic Encryption Using Ideal Lattices | Craig Gentry | 2009 | First FHE construction (STOC) |
| Fully Homomorphic Encryption without Bootstrapping | Brakerski, Gentry, Vaikuntanathan | 2011 | BGV scheme |
| Somewhat Practical Fully Homomorphic Encryption | Fan, Vercauteren | 2012 | BFV scheme |
| FHEW: Bootstrapping in Less Than a Second | Ducas, Micciancio | 2015 | Fast bootstrapping (EUROCRYPT) |
| TFHE: Fast FHE Over the Torus | Chillotti, Gama, Georgieva, Izabachène | 2016 | TFHE scheme (J. Cryptology) |
| Homomorphic Encryption for Approximate Numbers | Cheon, Kim, Kim, Song | 2017 | CKKS scheme (ASIACRYPT) |
| On Data and Privacy Homomorphisms | Rivest, Adleman, Dertouzos | 1978 | Original concept proposal |
Curated from awesome-fhe-attacks by Hexens and IACR ePrint
IND-CPAD & Key Recovery Attacks (2024):
| Paper | Authors | Venue | Impact |
|---|---|---|---|
| On the Practical CPAD Security of "Exact" and Threshold FHE | Checri, Sirdey, Boudguiga, Bultel | CRYPTO 2024 | Key recovery on BFV/BGV/TFHE in <1 hour |
| Attacks Against the IND-CPAD Security of Exact FHE | Cheon, Choe, Passelègue, Stehlé | ePrint 2024 | Exploits imperfect correctness |
| Security Guidelines for Implementing HE | HomomorphicEncryption.org | Comm. Cryptology 2025 | Implementation best practices |
| Key Recovery on Approximate HE (Noise Flooding) | Guo et al. | USENIX Security 2024 | CKKS noise flooding attacks |
Lattice Attacks:
- A Successful Subfield Lattice Attack on FHE - Breaks NTRU-based schemes
- On Dual Lattice Attacks Against Small-Secret LWE - HElib/SEAL parameter analysis
- Revisiting Lattice Attacks on Overstretched NTRU - NTRU cryptanalysis
Side-Channel & Other Attacks:
- Leaking Secrets in HE with Side-Channel Attacks - Timing/power analysis
- A Practical Full Key Recovery on TFHE and FHEW - Decryption error induction
- Model Stealing Attacks on FHE-based ML - FHE ML vulnerabilities
Curated from awesome-he and fhe.org
| Library | Language | Schemes | Maintainer | Notes |
|---|---|---|---|---|
| TFHE-rs | Rust | TFHE | Zama | Audited, fhEVM backend |
| Concrete | Python/Rust | TFHE | Zama | High-level FHE compiler |
| Concrete ML | Python | TFHE | Zama | Privacy-preserving ML |
| OpenFHE | C++ | All major | Duality Tech | Production-ready, all schemes |
| SEAL | C++ | BFV, CKKS | Microsoft | Widely used, well-documented |
| HElib | C++ | BGV, CKKS | IBM | Bootstrapping support |
| Lattigo | Go | BGV, CKKS, RLWE | Tune Insight | Multiparty HE |
| HEaaN | C++ | CKKS | CryptoLab | GPU acceleration, bootstrapping |
| Swift HE | Swift | BFV, BGVL | Apple | iOS/macOS integration |
| HEIR | MLIR | Multiple | Compiler toolchain | |
| Jaxite | JAX | TFHE | TPU acceleration |
| Repository | Maintainer | Focus |
|---|---|---|
| fhe.org/resources | FHE.org Community | Comprehensive FHE resources, meetups, tutorials |
| awesome-zama | Zama AI | TFHE-rs, Concrete, fhEVM, research papers |
| awesome-he | Community | HE libraries, applications, toolkits |
| awesome-openfhe | OpenFHE | OpenFHE ecosystem resources |
| awesome-fhe-attacks | Hexens | Security research, attack papers, tools |
| awesome-fhenix | Fhenix | Fhenix/CoFHE ecosystem |
Courses:
- CSE208: Advanced Cryptography (FHE) - UC San Diego, Daniele Micciancio
- Homomorphic Encryption and Lattices - Shai Halevi (2011)
- Foundations of Private Computation - OpenMined
Textbooks:
- Homomorphic Encryption for Data Science (HE4DS) - IBM, 2024
- FHE Textbook - Academic reference
- Crypto 101 - Cryptography fundamentals
Talks & Presentations:
- Attacks Against IND-CPAD Security - Damien Stehlé
- Estimating Lattice-Based Crypto Security - Martin Albrecht
- Danger of Using FHE - Zhiniang Peng
| Tool | Purpose | Link |
|---|---|---|
| Lattice Estimator | Estimate lattice-based crypto security | GitHub |
| IND-CPAD Attack PoC | Threshold FHE attack demonstration | GitHub |
| LWE Benchmarking | Attack benchmarks on LWE | GitHub |
| Audit | Auditor | Scope | Status |
|---|---|---|---|
| Zama Protocol | Trail of Bits, OpenZeppelin (verify others with team) | TFHE-rs, fhEVM, KMS, Gateway | Check official sources |
| Fhenix CoFHE | - | Threshold Decryption Network | Check official documentation |
Note
Zama's TFHE-rs has undergone extensive auditing. For the latest audit reports and security updates, check zama.ai/blog and docs.fhenix.io.
- Fhenix Developer Docs - CoFHE integration, cofhejs SDK
- Zama Documentation - fhEVM, TFHE-rs, Concrete
- OpenFHE Documentation - OpenFHE usage
- OpenZeppelin FHEVM Security Guide - Security patterns
- Homomorphic Encryption Standard - Parameter recommendations
| Standard | Organization | Relevance |
|---|---|---|
| Post-Quantum Cryptography Guidelines | NIST | Lattice-based security parameters |
| Homomorphic Encryption Standard | HomomorphicEncryption.org | FHE security parameter recommendations |
| ERC-20 Token Standard | Ethereum | FHERC20 compatibility |
# Install Fhenix development tools
npm install -g @fhenix/cli
npm install @fhenix/cofhe-contracts @fhenix/cofhejs
# Clone starter template
git clone https://github.com/fhenixprotocol/cofhe-hardhat-starter
cd cofhe-hardhat-starter
npm install
# Configure for audit
cp .env.example .env
# Edit .env with testnet RPC and keys├── IDE: VS Code + Solidity extension
├── Framework: Hardhat + cofhe-hardhat-plugin
├── Testing: Mocha + Chai + fhenix-mock
├── Static Analysis: Slither + custom FHE detectors
├── Fuzzing: Echidna (with FHE mocks)
└── Documentation: Solidity NatSpec
| Network | Faucet |
|---|---|
| Ethereum Sepolia | Alchemy |
| Arbitrum Sepolia | Alchemy |
| Base Sepolia | Alchemy |
End of Handbook
Version 1.0 - January 2026
For updates and corrections, please refer to the official documentation of Zama and Fhenix protocols.