Learning Objectives
By the end of this module, you will:- Implement encrypted state machines with threshold-based transitions
- Apply the LastError pattern for encrypted error handling
- Design encrypted key-value registries with sharing capabilities
- Build composable encrypted contracts
- Handle cross-contract encrypted data flow
Introduction: Beyond Basic FHE
By now you know how to declare encrypted types, perform FHE operations, manage ACL permissions, handle encrypted inputs, request decryptions, and use conditional logic withFHE.select(). Those are the building blocks.
This module teaches you how to combine them into design patterns that solve real problems in production confidential applications.
Six Essential FHE Patterns
| Pattern | Problem It Solves | Key Technique |
|---|---|---|
| Encrypted State Machine | Private transition conditions in workflows | FHE.ge() + FHE.select() on encrypted thresholds |
| LastError | No feedback when FHE operations silently fail | Encrypted error codes per user |
| Encrypted Registry | Flexible encrypted data storage with sharing | Nested mappings + ACL grants |
| Cross-Contract Composability | Passing encrypted values between contracts | FHE.allow(handle, otherContract) |
| Encrypted Batch Processing | Multiple FHE operations in one transaction | Bounded loops + gas awareness |
| Time-Locked Encrypted Values | Values that become decryptable at a future time | block.timestamp guards + makePubliclyDecryptable() |
Pattern 1: Encrypted State Machines
Why This Pattern Exists
Many decentralized applications follow a state machine: an escrow moves from Funded to Released, a game moves from Lobby to Playing to Finished, a milestone payment moves from Pending to Approved. In traditional Solidity, the transition conditions are public — everyone can see why a state changed. With FHE, we can make the transition condition private while keeping the state itself public. This is a powerful separation:- Public: Which state the machine is in (observers need to know this to interact correctly)
- Private: Why and when the transition will happen (the encrypted threshold, counter, or condition)
Use Cases
- Escrow release: Funds release when an encrypted milestone metric reaches a private target
- Game logic: A game round ends when an encrypted score reaches a hidden threshold
- Milestone payments: Payment unlocks when an encrypted deliverable count hits a private goal
- Governance escalation: A proposal escalates when encrypted support reaches a private quorum
Implementation Walkthrough
Let’s examineEncryptedStateMachine.sol (contracts/17-advanced-patterns/EncryptedStateMachine.sol:52):
Setting the threshold
Incrementing the counter
actionCount is public (observers can see how many actions have been performed), but _counter is encrypted.Checking the transition
FHE.ge() computes whether counter >= threshold entirely in the encrypted domain. The result is an ebool — nobody can read it without decryption.Key Insight
The state machine pattern separates what (which state, public) from why (the condition, private). Observers seeACTIVE -> COMPLETED but never learn that the threshold was 42 or that the counter reached it on the 42nd action. They only learn that some encrypted condition was satisfied.
Pattern 2: The LastError Pattern
The Problem
This is arguably the most important pattern for FHE application usability. Here’s the core problem: In traditional Solidity, when a transfer fails due to insufficient balance, the transaction reverts with an error message. The user sees “Insufficient balance” in their wallet. Simple. In FHE Solidity, we cannot revert based on encrypted conditions. Why? Because a revert is a public signal. Iftransfer(encrypted_amount) reverts only when the balance is too low, an observer can learn about the user’s balance by watching which transactions revert.
The Solution: Encrypted Error Codes
The LastError pattern stores an encrypted error code per user after each operation:FHE.select():
Why This Is Private
The error code is encrypted and ACL-protected to the user. Only Alice can decrypt Alice’s error. An observer sees that Alice calledtransfer() and it didn’t revert — they learn nothing about whether it succeeded or failed.
Error Code Priority
When multiple conditions fail simultaneously, the lastFHE.select() in the chain wins. Design your priority order intentionally:
Best Practice: Clear Before New Operations
clearError() before performing a new operation. This prevents confusion from stale error codes.
Pattern 3: Encrypted Registries
The Problem
Applications often need flexible encrypted storage — not just tokens with fixed balance semantics, but arbitrary encrypted key-value pairs that users can store, retrieve, share, and delete. Consider these use cases:- A user stores encrypted medical records under keys like “blood_type”, “allergies”
- A credential system stores encrypted scores under keys like “credit_score”, “income”
- A configuration system stores encrypted parameters under keys like “risk_tolerance”, “max_position”
Implementation Walkthrough
EncryptedRegistry.sol (contracts/17-advanced-patterns/EncryptedRegistry.sol:14) implements a per-user encrypted key-value store:
_store[user][key] provides complete isolation between users. Alice’s “salary” key is completely separate from Bob’s “salary” key.
The _hasKey mapping is stored in plaintext. This is a design choice: existence of a key is public information, but the value is private.
Storing a value
Storing a value
_userKeys, _keyIndex) enable enumeration — the user can list all their keys and iterate over them.Sharing a value
Sharing a value
Deleting a value
Deleting a value
Design Decision: What Should Be Public?
In the registry pattern, you must decide what metadata is public:| Data | Public or Private? | Rationale |
|---|---|---|
| Key names | Public | String keys are stored in plaintext for cheap lookups |
| Key existence | Public | The _hasKey mapping is plaintext |
| Number of keys | Public | Array length is plaintext |
| Values | Private | Encrypted with per-user ACL |
If you need to hide key names, you could hash them (
keccak256(key)) and use bytes32 instead of string. If you need to hide even the number of keys, you would need a more complex data structure (e.g., a fixed-size array with encrypted “empty” markers).Pattern 4: Cross-Contract Composability
The Challenge
In a multi-contract system, Contract A might store an encrypted value that Contract B needs to read. FHE’s ACL system prevents unauthorized access — so how do you grant one contract access to another contract’s encrypted data?The Solution: Explicit ACL Grants
The key insight is that ACL permissions can be granted to any address, including contract addresses:The Flow
User grants Vault access
Calls
A.grantVaultAccess(contractB_address)
→ FHE.allow(balance, contractB)User calls B.deposit()
B reads
A.balanceOf(user) and gets the euint64 handleB can now perform FHE operations on that handleImportant: ACL Propagation
When Contract B creates a new encrypted value by operating on Contract A’s data, the new value has its own ACL. You must explicitly grant permissions on the new value:Cross-Contract Pattern Summary
Pattern 5: Encrypted Batch Processing
Why Batch?
Some operations naturally work on multiple items: distributing rewards to N users, updating N encrypted scores, or processing N encrypted votes. Doing these one-at-a-time costs N transactions, each with base gas overhead.The Rule: Bounded Iteration Only
Never use unbounded loops with FHE operations. Each FHE operation consumes significant gas. An unbounded loop can easily exceed the block gas limit.Gas Estimation
FHE operations are significantly more expensive than plaintext:| Operation | Approximate Gas |
|---|---|
FHE.add(euint64, euint64) | ~200k |
FHE.sub(euint64, euint64) | ~200k |
FHE.mul(euint64, euint64) | ~300k |
FHE.ge(euint64, euint64) | ~200k |
FHE.select(ebool, euint64, euint64) | ~250k |
FHE.fromExternal() | ~300k |
The PerformBatchActions Pattern
OurEncryptedStateMachine.sol (contracts/17-advanced-patterns/EncryptedStateMachine.sol:103) demonstrates a simple batch:
Pattern 6: Time-Locked Encrypted Values
The Concept
Sometimes you want an encrypted value to become publicly readable only after a certain time. Examples:- Sealed bids revealed after the bidding period
- Encrypted votes decrypted after the voting deadline
- Secret game moves revealed after the round ends
The Pattern
Combineblock.timestamp (plaintext time) with FHE.makePubliclyDecryptable():
revealTime, the value is encrypted and only the contract can operate on it. After revealTime, anyone can call reveal() and the value becomes publicly decryptable.
Important: Time Is Not Encrypted
block.timestamp is plaintext and controlled by validators. This means:
- The reveal time is public (everyone knows when the value will be decryptable)
- Validators have minor influence over
block.timestamp(typically +-15 seconds) - For high-stakes applications, use block numbers instead of timestamps for more predictability
Use Case: Commitment Schemes
Time-locked encryption enables commit-reveal schemes without the separate reveal transaction:Putting It All Together
How Patterns Combine
Real applications use multiple patterns simultaneously: Encrypted Escrow (exercise for this module):- State Machine: CREATED → FUNDED → RELEASED / DISPUTED / EXPIRED
- LastError: Encrypted error codes when funding or releasing fails
- Time-Lock: Funds auto-release after a deadline
- Cross-Contract: Escrow reads token balances via ACL
- State Machine: Order lifecycle (OPEN → MATCHED → SETTLED)
- Encrypted Registry: Order book with encrypted prices and quantities
- Batch Processing: Match multiple orders in one transaction
- Cross-Contract: DEX reads token balances for settlement
- State Machine: LOBBY → PLAYING → FINISHED
- LastError: Invalid move feedback without revealing game state
- Time-Lock: Moves revealed after round timer expires
- Encrypted Registry: Per-player encrypted game state
Design Principles for Production FHE Contracts
Minimize FHE operations
Every encrypted operation costs 10-100x more gas. Compute as much as possible in plaintext.
ACL is your access control layer
Don’t build your own permission system for encrypted data. Use
FHE.allow() and FHE.allowThis() consistently.Never branch on encrypted conditions
Use
FHE.select() instead of if/else. The control flow must be identical regardless of the encrypted values.Use the LastError pattern
Silent failures are terrible UX. Always give users an encrypted error code they can decrypt to understand what happened.
Keep state public when possible
Encrypt only what must be private. Public state is cheaper and easier to reason about.
Bound all loops
Never iterate over an unbounded collection with FHE operations inside the loop.
Design for composability
Use interfaces and ACL grants to enable contract-to-contract encrypted data flow.
Test with known values
Encrypt known plaintext values in tests, then decrypt results to verify correctness.
Pattern Selection Guide
| If you need… | Use this pattern |
|---|---|
| Workflow with private conditions | Encrypted State Machine |
| User feedback without info leaks | LastError Pattern |
| Flexible encrypted data storage | Encrypted Registry |
| Multi-contract encrypted data flow | Cross-Contract Composability |
| Multiple FHE operations per tx | Encrypted Batch Processing |
| Reveal encrypted data at a future time | Time-Locked Encrypted Values |
Summary
This module introduced six advanced patterns that transform basic FHE operations into production-ready design primitives:- Encrypted State Machine — Public states with private transition conditions. The “why” stays encrypted even as the “what” is visible.
- LastError Pattern — Encrypted error codes per user, solving the FHE usability problem of silent failures. Users decrypt their own error to learn what happened.
- Encrypted Registry — Per-user key-value storage with sharing via ACL grants. Flexible enough for medical records, credentials, or configuration.
- Cross-Contract Composability — Explicit ACL grants between contracts enable multi-contract encrypted architectures.
- Encrypted Batch Processing — Bounded iteration over encrypted data, with gas-aware batch sizes.
-
Time-Locked Encrypted Values — Combining plaintext timestamps with
makePubliclyDecryptable()for timed reveals.
Reference Contracts
- EncryptedStateMachine.sol (
contracts/17-advanced-patterns/EncryptedStateMachine.sol:1) — State transitions driven by encrypted thresholds - LastErrorPattern.sol (
contracts/17-advanced-patterns/LastErrorPattern.sol:1) — Encrypted error feedback without reverts - EncryptedRegistry.sol (
contracts/17-advanced-patterns/EncryptedRegistry.sol:1) — Key-value storage with per-user encryption and sharing