fhevm-arithmetic-ops
FHE Arithmetic Operations
Use this skill when a contract performs computation on encrypted values. Every FHE operation
has constraints that differ from plaintext Solidity math. Some misuse panics immediately
(for example encrypted divisors in div/rem), while other mistakes create silent
fallbacks or inaccessible handles.
When To Use
- Writing arithmetic logic over
euint64,euint128, oreboolvalues - Reviewing whether division or modulo uses a plaintext divisor
- Checking for overflow in encrypted computations
- Deciding which operands should be public vs encrypted
- Optimizing gas by restructuring operations to use ciphertext-scalar forms
- Implementing comparisons or conditional logic with
FHE.select
Core Mental Model
FHE arithmetic produces new ciphertext handles. Every operation returns a fresh handle that must be stored, granted ACL access, or passed to the next operation. There is no in-place mutation. Think of it as functional transforms on opaque pointers, not register arithmetic.
The golden rule: if an operand can be plaintext without leaking private information, keep it plaintext. Ciphertext-scalar operations are significantly cheaper than ciphertext-ciphertext operations.
Hard Constraints
FHE.div(a, b)andFHE.rem(a, b)are only supported whenbis plaintext. Passing an encrypted right-hand side panics.- Arithmetic on
euint8,euint16,euint32,euint64, andeuint128is unchecked and wraps modulo the type width. There is no automatic overflow or underflow detection. euint256does not support add/sub/mul/div/rem/min/max or ordered comparisons (ge,gt,le,lt). Treat it as bitwise/opaque integer space.- Every FHE operation returns a NEW handle. The old handle still exists but is not the result. You must use and store the new handle.
- New handles need ACL grants. If you compute
c = FHE.add(a, b)but never grant ACL onc, no one can decrypt or usecin future transactions. require()cannot branch onebool. UseFHE.selectfor inline encrypted branching, or async public decryption if the result must drive plaintext logic.
Operation Support By Type
| Type | Supported operation families |
|---|---|
ebool |
and, or, xor, eq, ne, not, select, randEbool() |
euint8, euint16, euint32, euint64, euint128 |
add, sub, mul, div (plaintext rhs only), rem (plaintext rhs only), and, or, xor, shl, shr, rotl, rotr, eq, ne, ge, gt, le, lt, min, max, neg, not, select, randEuintX(), randEuintX(upperBound) |
eaddress |
eq, ne, select |
euint256 |
and, or, xor, shl, shr, rotl, rotr, eq, ne, neg, not, select, randEuint256(), randEuint256(upperBound) |
Use the official types page or FHE.sol when you need the full per-type overload matrix. The
sections below summarize the operator families that matter most in cookbook work.
Common Operation Families
Arithmetic
| Operation | Syntax | Divisor/Operand Rule |
|---|---|---|
| Add | FHE.add(a, b) |
Both can be encrypted |
| Subtract | FHE.sub(a, b) |
Both can be encrypted |
| Multiply | FHE.mul(a, b) |
Both can be encrypted |
| Divide | FHE.div(a, b) |
b MUST be plaintext |
| Remainder | FHE.rem(a, b) |
b MUST be plaintext |
| Min | FHE.min(a, b) |
Both can be encrypted |
| Max | FHE.max(a, b) |
Both can be encrypted |
| Negation | FHE.neg(a) |
Unary; supported on encrypted integer types |
These arithmetic operators are available on euint8, euint16, euint32, euint64, and
euint128. They are not available on euint256.
Comparisons
All comparison operators return ebool.
FHE.eq,FHE.nesupport encrypted booleans, encrypted integers, andeaddressFHE.ge,FHE.gt,FHE.le,FHE.ltsupporteuint8,euint16,euint32,euint64, andeuint128- Ciphertext-scalar overloads exist for the numeric integer types above
Bitwise
| Operation | Syntax | Notes |
|---|---|---|
| AND | FHE.and(a, b) |
Ciphertext-ciphertext or ciphertext-scalar |
| OR | FHE.or(a, b) |
Ciphertext-ciphertext or ciphertext-scalar |
| XOR | FHE.xor(a, b) |
Ciphertext-ciphertext or ciphertext-scalar |
| NOT | FHE.not(a) |
Unary bitwise/boolean inversion |
| Shift left | FHE.shl(a, b) |
b is uint8 or euint8; shift count is modulo bit width |
| Shift right | FHE.shr(a, b) |
b is uint8 or euint8; shift count is modulo bit width |
| Rotate left | FHE.rotl(a, b) |
b is uint8 or euint8; rotate count is modulo bit width |
| Rotate right | FHE.rotr(a, b) |
b is uint8 or euint8; rotate count is modulo bit width |
Shifts and rotations are supported on encrypted integer types, including euint256.
Conditional Selection
| Operation | Syntax | Notes |
|---|---|---|
| Select | FHE.select(cond, a, b) |
cond is ebool. Returns a if true, b if false. Does NOT revert. |
FHE.select supports ebool, euint8, euint16, euint32, euint64, euint128, euint256,
and eaddress.
Random
| Operation | Syntax | Notes |
|---|---|---|
| Random bool | FHE.randEbool() |
Transaction-only; cannot be used via eth_call |
| Random integer | FHE.randEuintX() |
Available for euint8/16/32/64/128/256 |
| Bounded random integer | FHE.randEuintX(upperBound) |
upperBound must be a power of 2 |
Type Conversion
| Operation | Syntax | Notes |
|---|---|---|
| Plaintext to encrypted int | FHE.asEuint8/16/32/64/128/256(value) |
Converts matching-width plaintext integers to encrypted values |
| Plaintext to encrypted address | FHE.asEaddress(value) |
Converts address to eaddress |
| Encrypted integer cast | FHE.asEuintX(value) |
Casts between encrypted integer widths, including from ebool |
| From external | FHE.fromExternal(externalE*, inputProof) |
Imports supported external encrypted inputs into onchain handles |
The Plaintext Divisor Rule
This is the most common source of bugs. Division and remainder require a plaintext second operand. If you need to divide by a value derived from user input, the design must ensure that value is public.
// CORRECT: elapsed and duration are plaintext (public timing)
euint128 deposit128 = FHE.asEuint128(deposit);
euint128 streamed = FHE.div(FHE.mul(deposit128, elapsed), duration);
// WRONG: encrypted divisor — will panic
euint64 result = FHE.div(amount, encryptedRate); // NOT SUPPORTED
Design pattern: keep time, rates, denominators, and divisors PUBLIC. Structure your formulas so division always uses a plaintext value.
Overflow Handling
euint64 has a maximum of 2^64 - 1 (roughly 18.4 * 10^18). Addition, subtraction,
and multiplication on encrypted integers are unchecked and wrap modulo the bit width.
Multiplication of two large euint64 values can therefore overflow silently.
Mitigation: use euint128 for intermediate precision when multiplying before dividing.
Do not assume euint256 is a drop-in wider arithmetic type; the current library does not
provide add/sub/mul/div/rem on euint256.
// Safe pattern: upcast, multiply, divide, then use result
euint128 a128 = FHE.asEuint128(amount);
euint128 product = FHE.mul(a128, rate); // rate is plaintext
euint128 result128 = FHE.div(product, PRECISION);
// Store or use result128, or downcast if within euint64 range
Always reason about the maximum possible value at each step. Document the overflow bounds in comments.
ACL Grants for New Handles
Every FHE operation creates a new handle. The current caller can use that fresh handle in the same transaction, but prior persistent grants do not carry over to it.
euint64 newBalance = FHE.add(oldBalance, amount);
// newBalance is a new handle — grant access
FHE.allowThis(newBalance); // contract can use it later
FHE.allow(newBalance, msg.sender); // user can decrypt it
Missing FHE.allowThis means the contract cannot use newBalance in a subsequent
transaction. Missing FHE.allow means the intended reader cannot decrypt it.
Conditional Logic With FHE.select
Since require() does not work on ebool, use FHE.select for branching:
ebool hasEnough = FHE.ge(balance, amount);
euint64 transferAmount = FHE.select(hasEnough, amount, FHE.asEuint64(0));
euint64 newBalance = FHE.sub(balance, transferAmount);
Critical: when the condition is false, FHE.select returns the fallback value (usually
zero). This is a silent fallback, not a revert from FHE.select itself. The enclosing
transaction may still revert later on public checks. Document what happens in the false
branch for every FHE.select.
Anti-Patterns
Anti-Pattern 1: Encrypted Divisor
Using an encrypted value as the second argument to FHE.div or FHE.rem. Restructure
the formula so the divisor is always plaintext.
Anti-Pattern 2: Ignoring New Handles
Computing FHE.add(a, b) but continuing to use a as if it were updated. FHE operations
are not in-place. Always capture and use the returned handle.
Anti-Pattern 3: Missing ACL on Computed Values
Creating a new handle via arithmetic but never calling FHE.allowThis or FHE.allow.
The value exists but is inaccessible in future transactions.
Anti-Pattern 4: Overflow Without Upcasting
Multiplying two euint64 values without considering that the product can exceed
2^64 - 1. Use euint128 for intermediates.
Anti-Pattern 4b: Assume euint256 Supports Normal Arithmetic
Using euint256 as if it were just a wider euint128 for add, sub, mul, div,
or ordered comparisons. In the current library, euint256 supports bitwise operations,
eq/ne, neg, not, select, and random generation, but not the usual arithmetic
operator set.
Anti-Pattern 5: Assuming FHE.select Reverts
Treating FHE.select as equivalent to require. It does not revert. The false branch
silently produces the fallback value. If the false case is a critical error, the contract
must handle it through a separate mechanism.
Review Checklist
- Does every
FHE.divandFHE.remuse a plaintext divisor? - Is every new handle from an FHE operation stored and granted appropriate ACL?
- Are overflow bounds documented for multiplication chains?
- Does every
FHE.selecthave a documented false-branch behavior? - Are ciphertext-scalar forms used wherever an operand can safely be public?
- Is
euint128used for intermediate precision when multiplying before dividing? - Does the contract call
FHE.allowThison handles it needs in future transactions?
Output Expectations
When applying this skill, structure analysis around:
- operation correctness: are operand types valid for each FHE function?
- handle lifecycle: is every new handle stored, granted ACL, or consumed?
- overflow bounds: what is the maximum value at each computation step?
- privacy tradeoff: which operands are public, and is that acceptable?
Related Skills
skills/fhevm-control-flow/SKILL.md— comparisons feedFHE.selectfor conditional logicskills/fhevm-acl-lifecycle/SKILL.md— every arithmetic result is a new handle that needs ACL
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.
10fhevm-control-flow
Use when replacing if/else, require, or any conditional logic that depends on encrypted values in FHEVM. Covers FHE.select as the inline branching primitive, fallback semantics on encrypted conditions, and async public decryption when logic must branch back to plaintext state.
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.
10