oz-utils-safemath
FHESafeMath for Confidential Contracts
Use this skill when a contract mutates encrypted euint64 balances or counters and must
detect overflow, underflow, or uninitialized-handle edge cases. Raw FHE.add and FHE.sub
wrap silently and do not tell you whether the result was valid. FHESafeMath wraps those
operations, returns an ebool success flag, and provides explicit fallback behavior when the
operation would have been unsafe.
When To Use
- Updating encrypted balances in an ERC7984-style token, vault, or ledger
- Incrementing or decrementing encrypted counters where overflow is possible
- Summing encrypted deltas from untrusted sources (user deposits, stream flows)
- Handling the case where an operand may still be an uninitialized handle
- Replacing ad-hoc
FHE.select(FHE.ge(...), newValue, oldValue)patterns with the library form
If the operands are bounded by construction (for example, a small fixed-length tally where
you know the sum fits in euint64), raw FHE.add is fine. Use FHESafeMath where the risk
of wrap-around is real.
Core Mental Model
Each safe op returns (ebool success, euint64 updated):
successis an encrypted boolean. The caller can forward it through furtherFHE.selectbranches or surface it via async public decryption.updatedfollows the specific function's fallback rule:tryIncrease/tryDecreasepreserve the previous value on failure, whiletryAdd/trySubfall back to zero.
The library treats an uninitialized handle (euint64.wrap(bytes32(0))) as zero. So
tryIncrease(0-handle, delta) returns (true, delta) without needing you to branch on
FHE.isInitialized yourself.
The library returns encrypted handles according to each function's branch. On some uninitialized-input fast paths it can return an existing handle directly rather than creating a fresh one. You still need to grant ACL on anything you persist or pass downstream. See skills/fhevm-acl-lifecycle/SKILL.md.
The Four Operations
| Function | Returns success when | Returns updated as |
|---|---|---|
tryIncrease(oldValue, delta) |
oldValue + delta >= oldValue (no overflow) |
oldValue + delta on success, else oldValue |
tryDecrease(oldValue, delta) |
oldValue >= delta (no underflow) |
oldValue - delta on success, else oldValue |
tryAdd(a, b) |
a + b >= a (no overflow) |
a + b on success, else 0 |
trySub(a, b) |
a - b <= a (no underflow) |
a - b on success, else 0 |
Key behavior split: tryIncrease / tryDecrease fall back to the OLD VALUE on failure;
tryAdd / trySub fall back to ZERO. Pick the one whose failure mode matches your
invariant. For balance updates you almost always want tryIncrease / tryDecrease.
Uninitialized-Handle Semantics
An euint64 slot that has never been written is bytes32(0) and evaluates as plaintext 0.
FHESafeMath checks this explicitly with FHE.isInitialized:
tryIncrease(uninit, delta)→(true, delta)— no FHE work, just returnsdeltatryDecrease(uninit, delta)→(delta == 0, 0)— decreasing an empty slot by zero is ok, anything else failstryAdd(uninit, b)/tryAdd(a, uninit)→ returns the initialized side withsuccess = truetrySub(a, uninit)→ returnsawithsuccess = true
This matters when you are reading from a mapping whose default entry may never have been touched. It keeps the ACL surface and gas cost down in cold-path branches.
Canonical Usage Pattern
import {FHESafeMath} from "@openzeppelin/confidential-contracts/utils/FHESafeMath.sol";
import {FHE, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol";
contract Vault {
using FHESafeMath for euint64;
mapping(address => euint64) private _balances;
function deposit(address to, euint64 amount) internal {
(ebool ok, euint64 updated) = FHESafeMath.tryIncrease(_balances[to], amount);
// On overflow `ok` is false and `updated == _balances[to]` — the write is a no-op
_balances[to] = updated;
FHE.allowThis(updated);
FHE.allow(updated, to);
// Optionally propagate `ok` (e.g. via a transient flag or async public decryption)
}
function withdraw(address from, euint64 amount) internal returns (ebool ok) {
euint64 updated;
(ok, updated) = FHESafeMath.tryDecrease(_balances[from], amount);
_balances[from] = updated;
FHE.allowThis(updated);
FHE.allow(updated, from);
}
}
Hard Rules
- The library operates on
euint64only. Cast to/from other widths explicitly before and after. - Every returned handle that you persist or share still needs the appropriate ACL grants — call
FHE.allowThisbefore relying on it in a later transaction, andFHE.allowfor any decryption authority. successis encrypted. You cannotrequire(ok). Forward it throughFHE.select, store it, or use async public decryption — see skills/fhevm-control-flow/SKILL.md.- The library adds FHE ops (comparison + select) on top of the raw arithmetic. Expect a measurable gas overhead versus raw
FHE.add/FHE.sub. - Failure is silent at the contract level. If you need to reject the caller on failure, plumb
okthrough to an async decryption step that reverts, or design the UX around a best-effort update.
Anti-Patterns
Anti-Pattern 1: Ignoring success
(, euint64 updated) = FHESafeMath.tryDecrease(balance, amount);
balance = updated;
The balance is safe, but you have discarded the information about whether the decrease
happened. Downstream checks on totals, receipts, or events will be wrong whenever the
operation silently no-oped. Always either persist success, fold it into the caller's
return value, or log it.
Anti-Pattern 2: Using tryAdd for a balance update
tryAdd returns zero on failure. Wiring it into a balance map means an overflow wipes
the balance. Use tryIncrease for balance updates; reserve tryAdd for sums where the
accumulator is a scratch value with no persistence guarantee.
Anti-Pattern 3: Forgetting ACL on the new handle
(, euint64 updated) = FHESafeMath.tryIncrease(balance, delta);
_balances[user] = updated;
// missing FHE.allowThis(updated) and FHE.allow(updated, user)
The next transaction cannot read the balance, and the user cannot decrypt it. Safe math does not do the ACL plumbing for you.
Anti-Pattern 4: Wrapping already-safe arithmetic
If both operands are bounded by construction (for example a small fixed-size accumulator
over encrypted votes where totalSupply * maxCount fits in euint64), the extra
FHE.ge + FHE.select only adds gas. Use raw FHE.add in hot paths where the invariant
is already proven.
Review Checklist
- Is the operation
Increase/Decrease(preserves old value on failure) orAdd/Sub(zeros out on failure)? Does that match the invariant? - Is
successused, persisted, or asynchronously decrypted — not discarded? - Does the caller grant ACL on the returned handle to every party that needs it?
- Is the uninitialized-handle path handled intentionally, or are you relying on the library's implicit zero-treatment?
- On hot paths, has overflow safety been traded off against gas cost?
Related Skills
- skills/fhevm-arithmetic-ops/SKILL.md — raw FHE arithmetic, overflow behavior, and when bounds are already safe
- skills/fhevm-acl-lifecycle/SKILL.md — granting ACL on the new handles returned by safe ops
- skills/fhevm-control-flow/SKILL.md — forwarding the
ebool successthroughFHE.select - skills/oz-erc7984-confidential-tokens/SKILL.md — primary consumer of safe math in OZ confidential contracts
Reference
- Source: OpenZeppelin
FHESafeMath.sol
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.
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