How to Use This Checklist
- Go through each section during development (shift-left security)
- Run through the complete checklist before deployment
- During audits, check every item and document findings
- 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: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.