Skip to main content
A structured security review checklist for FHE smart contracts. Use this during development, code review, and security audits.

How to Use This Checklist

  1. Go through each section during development (shift-left security)
  2. Run through the complete checklist before deployment
  3. During audits, check every item and document findings
  4. Mark each item: PASS / FAIL / N/A with notes

1. Access Control (ACL)

1.1 Contract Self-Permission

Every stored encrypted value has FHE.allowThis() called after assignment.Every FHE operation produces a new ciphertext with an empty ACL. If the contract stores it without FHE.allowThis(), the contract cannot read its own value in subsequent transactions.
FHE.allowThis() is called after every operation that modifies a stored encrypted value.It is not enough to call allowThis only on initial assignment. Every reassignment creates a new ciphertext.

1.2 User Permissions

Users are granted FHE.allow() for encrypted values they should be able to view.Without explicit permission, users cannot decrypt or re-encrypt their own data.
Permissions are not granted to addresses that should not have access.Audit every FHE.allow() call. Ask: “Should this address be able to see this value?”
Permissions are updated after every state change, not just initial assignment.After a transfer, both the sender and recipient need fresh permissions on their updated balances.

1.3 Cross-Contract Permissions

When returning encrypted values to another contract, FHE.allowTransient() is used for the caller.Without this, the calling contract will fail to use the returned ciphertext.
FHE.allowTransient() is preferred over FHE.allow() for cross-contract calls.Transient permissions expire at the end of the transaction, limiting the exposure window.

2. Information Leakage

2.1 Revert Leakage

The contract does NOT revert based on encrypted conditions.Example: require(balance >= amount) on encrypted values leaks whether the condition is true. Use FHE.select() instead.
All business logic involving encrypted data uses the “silent fail” pattern.Compute both branches, select the result.

2.2 Event Leakage

Events do not emit encrypted values in plaintext.Emitting uint256 amount in a Transfer event defeats the purpose of encrypting balances.
Events are carefully designed to not leak metadata.Example: If you emit Transfer(from, to) every time, the transfer graph is still visible.

2.3 Gas Leakage

All code paths consume approximately the same gas regardless of encrypted input values.Different gas consumption reveals information about the encrypted values (side-channel attack).
There are no early returns or short-circuit evaluations based on encrypted conditions.
Loop iterations do not depend on encrypted values.If a loop runs a variable number of times based on an encrypted value, the gas usage reveals the value.

3. Input Handling

3.1 Input Conversion

All external encrypted inputs use externalEuintXX types (not euintXX).External functions cannot accept euint32 directly from user transactions.
All external inputs are converted with FHE.fromExternal() before use.
Converted inputs have FHE.allowThis() called if they will be stored.

3.2 Input Validation

Encrypted inputs are validated where possible using encrypted comparisons.Example: Use FHE.le(amount, maxAllowed) to ensure an encrypted amount is within range, then use FHE.select to enforce it.

4. Decryption Safety

4.1 Decryption Pattern

Decryption uses FHE.makePubliclyDecryptable() only for values that should be public.Only call makePubliclyDecryptable when the value genuinely needs to be revealed on-chain.
The contract enforces access control before calling FHE.makePubliclyDecryptable().Without proper checks, unauthorized callers could trigger decryption of sensitive values.

4.2 Decryption Timing

Decryption does not happen prematurely.Example: revealing vote tallies before voting ends.
The contract enforces timing constraints on when decryption can be requested.

5. Arithmetic Safety

5.1 Overflow/Underflow

The contract accounts for silent wrapping on overflow.FHE.add on euint8 with values 200 + 100 silently wraps to 44. There is no revert.
Subtraction underflow is handled with the FHE.select pattern.Always check FHE.ge(a, b) before FHE.sub(a, b) if underflow is not desired.

5.2 Division

Division by zero is handled.The behavior of FHE.div(a, 0) on encrypted zero may vary. Validate or use FHE.select to handle.

6. State Management

6.1 Initialization

Encrypted state variables are properly initialized before use.Using an uninitialized euint64 in operations may cause failures. Initialize with FHE.asEuint64(0).

6.2 Reentrancy

Standard reentrancy protections are in place.FHE does not change reentrancy risks. Use the checks-effects-interactions pattern or reentrancy guards.
Encrypted state is updated before external calls.

7. Code Quality

7.1 API Usage

The contract uses the new FHEVM API (FHE library, externalEuintXX, FHE.fromExternal()).
The contract inherits ZamaEthereumConfig for proper configuration.
All imports are from @fhevm/solidity/lib/FHE.sol and @fhevm/solidity/config/ZamaConfig.sol.

7.2 Testing

A comprehensive test suite exists covering happy paths, edge cases, failure modes, and ACL scenarios.
Tests verify that failed encrypted operations produce the correct “unchanged” state.
Tests verify ACL permissions: authorized users can access, unauthorized users cannot.

Audit Report Template

When completing a security review, document findings using this structure:
## Finding [ID]: [Title]

**Severity:** Critical / High / Medium / Low / Informational
**Category:** [ACL | Leakage | Arithmetic | Input | Decryption | State | Protocol]
**Location:** [contract.sol:line]

### Description
[What is the issue?]

### Impact
[What can go wrong? Who is affected?]

### Proof of Concept
[Steps to reproduce or exploit]

### Recommendation
[How to fix it]

### Status
[Open | Acknowledged | Fixed | Won't Fix]

Top 10 Security Rules

1. Call FHE.allowThis()

After every encrypted state update. Missing ACL is the #1 bug.

2. Never revert on encrypted conditions

Use FHE.select() for the silent fail pattern.

3. Never emit encrypted data

Design events carefully to prevent leakage.

4. Ensure constant gas

Gas differences are a side channel.

5. Use externalEuintXX for inputs

Convert with FHE.fromExternal().

6. Verify ACL before returning

Check access with FHE.isSenderAllowed().

7. Use the smallest type

Save gas, reduce attack surface.

8. Handle overflow/underflow

FHE wraps silently.

9. Document threat model

What is protected? What is not?

10. Test extensively

Happy path, edge cases, ACL, failure modes.

Build docs developers (and LLMs) love