fhevm-control-flow
FHE Control Flow
Use this skill when writing or reviewing any Solidity logic that needs to branch on
encrypted conditions. Normal Solidity control flow does not work with encrypted values.
Every if, require, and ternary operator on encrypted state must be replaced with
FHE.select for inline encrypted branching, or with an async public-decryption flow if the
result must drive plaintext business logic.
When To Use
- Replacing
if/elseblocks that depend on encrypted balances or state - Replacing
require()guards that check encrypted conditions - Designing error handling and user feedback for confidential operations
- Reviewing contracts for illegal branching on encrypted values
- Planning product UX around silent-failure semantics
Core Mental Model
In standard Solidity, a failed guard often reverts immediately. In FHEVM, a guard that depends
on encrypted data cannot directly revert on that encrypted condition. Instead, the contract
usually computes a fallback value with FHE.select.
This is not a bug. This is the fundamental tradeoff of computing on encrypted data: the contract cannot see the values, so it cannot branch inline on them like plaintext Solidity. The false branch often becomes a no-op or a capped/clamped result, but later public checks can still revert normally.
Hard Constraints
require(encryptedCondition)does not work. Encrypted booleans (ebool) are notbool.if (encryptedValue > threshold)does not work. Comparisons on encrypted types returnebool, notbool. Solidityifneedsbool.FHE.select(condition, valueIfTrue, valueIfFalse)is the only inline onchain way to branch on encrypted conditions.- If encrypted logic must drive plaintext business logic, you need an async public-decryption flow with proof verification.
FHE.selectreturns a new handle. It needs ACL grants like every other FHE operation result.- A false encrypted condition yields the configured fallback value, not an automatic revert. Whether the transaction later succeeds still depends on subsequent public logic.
FHE.select Is Your Only Inline Branch
Replace every encrypted conditional with FHE.select:
// WRONG: `ebool` is not `bool`, so Solidity `if` cannot consume the comparison
if (FHE.ge(balance, amount)) { ... }
// WRONG: `require` cannot take `ebool` either
require(FHE.ge(balance, amount), "Insufficient balance");
// Also WRONG: there is no synchronous `FHE.decrypt` that returns plaintext
// inside a transaction — plaintext is only available via the two-step
// public-decryption flow (see `fhevm-public-decryption`).
// CORRECT: select between outcomes
ebool hasEnough = FHE.ge(balance, amount);
euint64 actualAmount = FHE.select(hasEnough, amount, FHE.asEuint64(0));
When the condition is false, actualAmount becomes 0. The transfer logic then proceeds with
that fallback amount. No automatic revert occurs on the encrypted condition itself.
Branching Back To Plaintext Requires Async Decryption
FHE.select keeps the entire branch inside encrypted space. If the contract must take a public
action based on an encrypted result, such as revealing a winner, distributing a public prize, or
executing plaintext settlement logic, it cannot branch inline.
That boundary requires:
- computing and storing the encrypted result onchain
- making it publicly decryptable
- decrypting offchain
- submitting the cleartext plus proof back onchain
- verifying the proof before taking the plaintext action
Use skills/fhevm-public-decryption/SKILL.md for that path.
The Silent-Failure Model
This is the hardest mental shift for Solidity developers. Every guarded encrypted operation becomes a fallback path rather than an immediate revert:
| Standard Solidity | FHEVM Equivalent | On Failure |
|---|---|---|
require(balance >= amount) then transfer |
FHE.select then transfer |
Transfers fallback amount (often 0), no automatic revert |
require(allowance >= amount) then spend |
FHE.select then spend |
Spends fallback amount (often 0), no automatic revert |
require(deadline > block.timestamp) |
Still works if deadline is public | Reverts normally |
Public values can still use require normally. Only encrypted conditions lose revert semantics.
Composing Multiple Conditions
Chain conditions using FHE.and and FHE.or before the select:
ebool hasBalance = FHE.ge(senderBalance, amount);
ebool isApproved = FHE.ge(allowance, amount);
ebool canTransfer = FHE.and(hasBalance, isApproved);
euint64 actualAmount = FHE.select(canTransfer, amount, FHE.asEuint64(0));
// Deduct from both balance and allowance using actualAmount
senderBalance = FHE.sub(senderBalance, actualAmount);
allowance = FHE.sub(allowance, actualAmount);
FHE.allowThis(senderBalance);
FHE.allowThis(allowance);
Every result handle needs ACL grants. Do not forget them after the select.
Product Design Implications
Because failures are silent, you must design the user experience differently:
- Users cannot rely on standard revert errors for encrypted failures. A failed confidential transfer may look like a successful transaction unless the app verifies the post-state.
- Balance-before-and-after is the primary feedback mechanism. The user decrypts their balance before and after the operation to see if it changed.
- Frontend UX must set expectations. Show "transaction submitted" not "transfer successful" until the user verifies the outcome.
- Timeouts and retries need care. A user who retries a "failed" transfer may double-send if the first one actually succeeded.
Plaintext to FHE Translation Table
| Plaintext Solidity | FHEVM Equivalent |
|---|---|
if (a >= b) { x = a; } else { x = b; } |
x = FHE.select(FHE.ge(a, b), a, b) |
require(a >= b) |
ebool ok = FHE.ge(a, b); then handle via FHE.select or async decryption |
a > b ? x : y |
FHE.select(FHE.gt(a, b), x, y) |
a == b |
FHE.eq(a, b) returns ebool |
a != b |
FHE.ne(a, b) returns ebool |
cond1 && cond2 |
FHE.and(cond1, cond2) |
cond1 || cond2 |
FHE.or(cond1, cond2) |
!cond |
FHE.not(cond) |
min(a, b) |
FHE.select(FHE.le(a, b), a, b) or FHE.min(a, b) |
max(a, b) |
FHE.select(FHE.ge(a, b), a, b) or FHE.max(a, b) |
assert(cond) |
No equivalent -- use FHE.select with a safe fallback |
Nested Selects
For multi-branch logic (like tiered pricing or graduated fees), nest selects:
ebool isHighTier = FHE.ge(amount, highThreshold);
ebool isMidTier = FHE.ge(amount, midThreshold);
euint64 fee = FHE.select(isHighTier, highFee,
FHE.select(isMidTier, midFee, lowFee));
FHE.allowThis(fee);
Each nested FHE.select returns a new handle. Grant ACL on the outermost result at minimum,
and on intermediates if the contract stores them.
Anti-Patterns
Anti-Pattern 1: Decrypt Then Branch
Calling FHE.decrypt inside contract logic to get a bool or uint for an if statement.
This defeats confidentiality and is not available in standard contract execution context.
Anti-Pattern 2: Assume Transaction Success Means The Encrypted Guard Passed
In standard ERC20, a reverted transfer means insufficient balance. In confidential flows, the transaction may still succeed while the encrypted guard selects a fallback amount. Do not equate transaction success with business success.
Anti-Pattern 3: Forget ACL on Select Results
FHE.select returns a new handle. If you store it without FHE.allowThis, the contract
loses access. If a user needs to decrypt it, they need FHE.allow.
Anti-Pattern 4: Use Public Require for Encrypted State
Mixing require with encrypted values by accidentally using a public proxy or stale
cached value instead of the actual encrypted state.
Review Checklist
- Is every branch on encrypted state using
FHE.selectinstead ofiforrequire? - If an encrypted result must drive public logic, is the design using async public decryption rather than pretending to branch inline?
- Does the product design account for silent failures in the user experience?
- Are all
FHE.selectresult handles granted appropriate ACL? - Are multiple encrypted conditions composed with
FHE.and/FHE.orbefore the select? - Is there any code path that attempts
FHE.decryptinside contract logic for branching? - Does the frontend communicate uncertainty rather than assuming success?
Output Expectations
When applying this skill, structure recommendations around:
- which conditions are encrypted vs public
- where
FHE.selectreplaces each branch - what the silent-failure behavior is for each guarded operation
- how the product UX communicates outcomes to users
Related Skills
skills/fhevm-arithmetic-ops/SKILL.md— comparisons produce theeboolthat drivesFHE.selectskills/fhevm-security-audit/SKILL.md— silent-fallback analysis is a top audit procedureskills/fhevm-testing/SKILL.md— everyFHE.selectfalse branch needs an explicit test
More from z-korp/fhevm-cookbook
fhevm-router
Routes Zama FHEVM tasks to the right official docs path and next step
10fhevm-testing
Use when writing, structuring, or debugging tests for FHEVM contracts. Covers mocked mode vs real protocol, Hardhat decrypt helpers, input encryption in tests, and the false-confidence gap between local and testnet behavior.
10fhevm-acl-lifecycle
Use when granting, auditing, or debugging ACL permissions on encrypted handles in FHEVM. Covers FHE.allow, FHE.allowThis, FHE.allowTransient, and the critical rule that new handles do not inherit prior persistent ACL grants.
10oz-utils-safemath
Use when you need overflow-safe encrypted arithmetic on euint64 values. Covers the OpenZeppelin FHESafeMath library (tryIncrease, tryDecrease, tryAdd, trySub), uninitialized-handle semantics, and when to prefer it over raw FHE.add / FHE.sub.
10fhevm-public-decryption
Use when implementing two-step public decryption for state-changing operations in FHEVM. Covers makePubliclyDecryptable, off-chain proof retrieval, onchain verification with checkSignatures, and the critical single-step unwrap bug.
10fhevm-cross-contract
Use when passing encrypted handles between contracts, designing multi-contract FHE flows, or debugging handle-not-accessible errors at contract boundaries. Covers allowTransient, allow, permission chains, and factory patterns.
10