zama/fhevm-dev-guide

Zama fhEVM — Solidity Dev Guide

zamatechnical-doc👥 Communityconfidence mediumhealth 100%
v1.0.0·Updated 4/5/2026

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 CounterFHE Counter
State typeuint32euint32
Inputuint32externalEuint32 + inputProof
Arithmetic+=FHE.add()
VisibilityPublicAuthorized 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));
    }
}
MethodDescription
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 before confidentialTransferFrom (not FHE.allow)
  • ZamaEthereumConfig inherited (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 — use FHE.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-listener config included a host_chain_id field.
  • After: host_chain_id is no longer a valid/required field in the gw-listener configuration.
  • Action: If you are running a coprocessor node, audit your gw-listener config and remove any host_chain_id entry to avoid unexpected behavior.

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.

<!-- AUTO-UPDATED:END -->

⚡ Quick Reference — Exact Package Names & Imports

These values are canonical. The old @zama/fhevm package 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 ints
  • ebool — encrypted boolean
  • eaddress — encrypted address
  • externalEuint32 etc. — 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