Zama fhEVM — Solidity Dev Guide
Use this skill when writing, reviewing, or debugging Solidity contracts that use Zama's fhEVM library.
Architecture
User (browser/app)
│ encrypt input with ZKPoK
▼
Ethereum Smart Contract (Solidity + fhEVM lib)
│ FHE operations on ciphertext (add, sub, lt, select…)
▼
Coprocessor Network
│ executes FHE computation off-chain, posts result back
▼
Gateway / KMS (MPC)
│ manages decryption keys via threshold MPC
▼
Authorized Party ← userDecrypt (via Relayer SDK)
The blockchain never sees plaintext. Even validators and coprocessor operators cannot read values.
Encrypted Types
// Integer types
euint8 / euint16 / euint32 / euint64 / euint128 / euint256
// Address & boolean
eaddress
ebool
// External input handles (from user, with ZKPoK proof)
externalEuint8 / externalEuint16 / externalEuint32
externalEuint64 / externalEuint128 / externalEuint256
externalEbool / externalEaddress
Core FHE Operations
import { FHE, euint64, externalEuint64, ebool, eaddress } from "@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
// --- Input validation ---
euint64 value = FHE.fromExternal(externalHandle, inputProof); // verify ZKPoK, get ciphertext
// --- Arithmetic ---
euint64 sum = FHE.add(a, b);
euint64 diff = FHE.sub(a, b);
euint64 prod = FHE.mul(a, b);
// --- Comparison (returns ebool) ---
ebool gt = FHE.gt(a, b);
ebool lt = FHE.lt(a, b);
ebool eq = FHE.eq(a, b);
// --- Conditional select (no branch leakage) ---
euint64 result = FHE.select(condition, ifTrue, ifFalse);
// --- Type casting ---
euint64 big = FHE.asEuint64(plainValue); // plaintext → ciphertext
eaddress addr = FHE.asEaddress(msg.sender);
euint64 zero = FHE.asEuint64(0);
// --- Bitwise ---
euint64 r = FHE.and(a, b);
euint64 r = FHE.or(a, b);
euint64 r = FHE.xor(a, b);
euint64 r = FHE.shl(a, bits);
euint64 r = FHE.shr(a, bits);
// --- Utility ---
bool initialized = FHE.isInitialized(value); // check if euint has been set
bytes32 handle = FHE.toBytes32(value); // get raw handle for events/storage
Access Control — CRITICAL
Every ciphertext handle must be explicitly authorized before another address can use it. Forgetting this is the most common bug.
FHE.allowThis(value); // allow this contract to use the ciphertext in future txs
FHE.allow(value, address); // allow a specific address to decrypt / use
FHE.allowTransient(value, address); // temporary (single-tx) allow — used before confidentialTransfer
// Public decryption (for auctions, voting reveals)
FHE.makePubliclyDecryptable(value); // marks ciphertext for public decryption
FHE.checkSignatures(handles, clearResult, proof); // verify decryption proof on-chain
Contract Setup Pattern
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.27;
import { FHE, euint64, externalEuint64 } from "@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
contract MyContract is ZamaEthereumConfig {
euint64 private _secretValue;
function store(externalEuint64 inputHandle, bytes calldata inputProof) external {
_secretValue = FHE.fromExternal(inputHandle, inputProof);
FHE.allowThis(_secretValue);
FHE.allow(_secretValue, msg.sender);
}
function getValue() external view returns (euint64) {
return _secretValue;
}
}
Always inherit ZamaEthereumConfig — it injects the correct KMS/coprocessor addresses for Sepolia/mainnet.
Pattern: FHE Counter (simplest example)
contract FHECounter is ZamaEthereumConfig {
euint32 private _count;
function increment(externalEuint32 input, bytes calldata proof) external {
euint32 val = FHE.fromExternal(input, proof);
_count = FHE.add(_count, val);
FHE.allowThis(_count);
FHE.allow(_count, msg.sender);
}
function getCount() external view returns (euint32) { return _count; }
}
| Plain Counter | FHE Counter | |
|---|---|---|
| State type | uint32 | euint32 |
| Input | uint32 | externalEuint32 + inputProof |
| Arithmetic | += | FHE.add() |
| Visibility | Public | Authorized parties only |
Pattern: Sealed-Bid Auction
Compare bids without revealing them, then public-decrypt winner after auction ends.
// During auction — update highest bid with FHE comparison
euint64 currentBid = bids[msg.sender];
ebool isNewWinner = FHE.lt(highestBid, currentBid);
highestBid = FHE.select(isNewWinner, currentBid, highestBid);
winningAddr = FHE.select(isNewWinner, FHE.asEaddress(msg.sender), winningAddr);
FHE.allowThis(highestBid);
FHE.allowThis(winningAddr);
// After auction — request public decryption
FHE.makePubliclyDecryptable(winningAddr);
emit AuctionDecryptionRequested(winningAddr);
// Resolve with decryption proof
function resolveAuction(bytes memory clearResult, bytes memory proof) public {
bytes32[] memory cts = new bytes32[](1);
cts[0] = FHE.toBytes32(winningAddr);
FHE.checkSignatures(cts, clearResult, proof);
winner = abi.decode(clearResult, (address));
}
Pattern: ERC-7984 Confidential Token
OpenZeppelin's confidential token standard. Balances and transfer amounts are encrypted.
import { ERC7984 } from "@openzeppelin/confidential-contracts/token/ERC7984/ERC7984.sol";
contract MyToken is ZamaEthereumConfig, ERC7984, Ownable2Step {
constructor(address owner, uint64 amount, string memory name_, string memory symbol_, string memory contractURI_)
ERC7984(name_, symbol_, contractURI_) Ownable(owner)
{
_mint(owner, FHE.asEuint64(amount));
}
}
| Method | Description |
|---|---|
confidentialBalanceOf(addr) | Returns encrypted balance handle (euint64) |
confidentialTransfer(to, handle, proof) | Transfer with encrypted amount |
confidentialTransferFrom(from, to, handle, proof) | Delegated confidential transfer |
setOperator(addr, deadline) | Authorize operator for contract-to-contract calls |
Using ERC-7984 inside another contract (e.g., auction):
// Temporary authorize before transferFrom
FHE.allowTransient(amount, address(confidentialToken));
confidentialToken.confidentialTransferFrom(msg.sender, address(this), amount);
Security Checklist
When reviewing or writing fhEVM contracts:
-
FHE.allowThis()called after every state update to an encrypted variable -
FHE.allow(value, msg.sender)called so the caller can decrypt their own values -
FHE.allowTransient()used beforeconfidentialTransferFrom(notFHE.allow) -
ZamaEthereumConfiginherited (not just imported) - Input validated with
FHE.fromExternal(handle, proof)— never trust raw handles - Re-entrancy guard (
ReentrancyGuard) added to functions that call external token contracts - No comparison of ciphertext with
==operator in Solidity — useFHE.eq()instead -
FHE.checkSignatures()called before trusting any public decryption result
<!-- AUTO-UPDATED:START -->
Last updated: 2026-04-07
Common Pitfalls
Problem: Including host_chain_id in the gateway listener configuration causes misconfiguration or startup errors in the coprocessor setup.
Fix: Remove host_chain_id from the gw-listener configuration block entirely.
Version: Affects coprocessor deployments prior to this fix.
Breaking Changes & Migration Notes
Configuration Change (coprocessor gw-listener):
- Before:
gw-listenerconfig included ahost_chain_idfield. - After:
host_chain_idis no longer a valid/required field in thegw-listenerconfiguration. - Action: If you are running a coprocessor node, audit your
gw-listenerconfig and remove anyhost_chain_identry to avoid unexpected behavior.
<!-- AUTO-UPDATED:END -->Note: This change is infrastructure/node-operator facing rather than Solidity or SDK developer facing. No on-chain contract or SDK API changes are involved.
⚡ Quick Reference — Exact Package Names & Imports
These values are canonical. The old
@zama/fhevmpackage is DEPRECATED.
NPM Package
npm install @fhevm/solidity # ← correct (NOT @zama/fhevm)
Solidity Imports
import "@fhevm/solidity/lib/FHE.sol"; // FHE library + euint types
import "@fhevm/solidity/lib/Permissioned.sol"; // access control
Key Types
euint8,euint16,euint32,euint64,euint128,euint256— encrypted unsigned intsebool— encrypted booleaneaddress— encrypted addressexternalEuint32etc. — external encrypted handles (used in function inputs)
Gateway Decrypt
import "@fhevm/solidity/lib/Gateway.sol";
Gateway.requestDecryption(cts, this.decryptCallback.selector, 0, block.timestamp + 100, false);
Network Addresses (Sepolia testnet)
- Gateway contract:
0x33347831AEa6A6a68ceaa3ce478A50e6b31C9B9e - ACL contract:
0x2Fb4341027eb1d2aD8B5D9708187df8633cAFA92