From fhevm
Use when writing, modifying, or reviewing any Solidity contract that uses Zama fhEVM encrypted types (euint, ebool, eaddress) — confidential DeFi, private voting, sealed-bid auctions, encrypted ERC20 tokens, encrypted balances, or any application where on-chain data must remain encrypted under fully homomorphic encryption. Symptoms in code: imports from @fhevm/solidity, uses externalEuintXX or FHE.fromExternal, calls FHE.add/sub/mul/select, FHE.allowThis, FHE.makePubliclyDecryptable, ZamaEthereumConfig, fhevm.createEncryptedInput. Use also when frontend integrates relayer-sdk for client-side encryption or async decryption.
How this skill is triggered — by the user, by Claude, or both
Slash command
/fhevm:fhevmThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Build Solidity contracts where on-chain data stays **encrypted** using Fully Homomorphic Encryption.
anti-patterns/ANTI-PATTERNS.mdexamples/AgentRegistry.solexamples/BlindAuction.solexamples/ConfidentialERC20.solexamples/ConfidentialTreasury.solexamples/ConfidentialTreasury.test.tsexamples/ConfidentialVoting.solexamples/EncryptedCounter.solreferences/access-control.mdreferences/architecture.mdreferences/decryption-patterns.mdreferences/encrypted-types.mdreferences/fhe-operations.mdreferences/frontend-integration.mdreferences/input-validation.mdreferences/testing-foundry.mdreferences/testing-guide.mdtemplates/contract-template.soltemplates/hardhat.config.tstemplates/test-template.tsBuild Solidity contracts where on-chain data stays encrypted using Fully Homomorphic Encryption.
Validation: This skill has been A/B tested against 15 prompts (7 demo + 8 adversarial traps), with one live verification run on 2026-05-09. Without this skill, vanilla LLM output triggers 47 distinct anti-pattern occurrences across 14 of the 20 patterns in the catalog (predicted), plus 9 anti-pattern occurrences in a single live run (verified — full transcript at
validation/transcripts/2026-05-09-vanilla-erc20.md); only 1/15 vanilla outputs both compile and have no privacy leak. With this skill loaded, all of these move to 0 and 15/15 respectively. Full report and reproducibility protocol:validation/agent-effectiveness.md.
Use this skill when any of these apply:
@fhevm/solidityeuint, ebool, eaddress) to an existing contract@zama-fhe/relayer-sdk for client-side encryption or async decryptionfhevm.createEncryptedInputSymptoms in chat or code that should trigger this skill:
FHE.add, FHE.select, FHE.fromExternal, FHE.allowThis, FHE.makePubliclyDecryptable, ZamaEthereumConfig, externalEuintXX@fhevm/solidity imports, *.sol files declaring euint* state, test files importing fhevm from hardhat, relayer-sdk in frontend depsFHE.decrypt is not a function, decryption returning garbageThis skill is overkill or off-topic for:
references/access-control.md cross-contract section is needed; full skill is unnecessary.Think in ciphertexts, not plaintexts. This is the fundamental shift from regular Solidity.
You are not writing "if this, then that" logic anymore. You are writing "compute both branches, select the correct result homomorphically." Every if/else becomes FHE.select. Every require becomes a guard that silently clamps to zero on failure. Your contract always succeeds at the EVM level — the question is what the encrypted result contains.
Four-step approach to every fhEVM task:
FHE.select instead of if? Did I validate the input with FHE.fromExternal?"npx hardhat test locally. Mock FHE simulates all operations without a real coprocessor. Verify the logic works before deploying.Understanding the architecture prevents entire classes of bugs:
User encrypts value (client-side, ZKPoK generated)
↓
Transaction carries encrypted handle (bytes32) + proof
↓
Contract calls FHE.fromExternal() → validates ZKPoK
↓
FHE.add(a, b) → FHE.sol → Impl.sol → FHEVMExecutor (on-chain)
↓
FHEVMExecutor does SYMBOLIC execution only:
- Generates new handle via keccak256(op, operands, chainId, blockhash...)
- Grants transient ACL permission for result
- Emits event (e.g., FheAdd)
- Meters HCU cost
↓
Off-chain coprocessor watches events, performs REAL TFHE computation
↓
Result ciphertext stored off-chain, indexed by the same handle
Key insight: The on-chain system is entirely symbolic. Handles (bytes32) are NOT ciphertexts — they are deterministic identifiers computed via keccak256. The actual encrypted data lives in 5 off-chain coprocessor nodes. Decryption requires 9 of 13 KMS MPC nodes (run by Etherscan, Fireblocks, Ledger, OpenZeppelin, etc. in AWS Nitro Enclaves). This is why:
if (eboolHandle)FHE.decrypt()bytes32(0) → not encrypted zero, just an invalid pointerSee
references/architecture.mdfor the full 6-component system, TFHE principles, handle format, and HCU metering details.
git clone https://github.com/zama-ai/fhevm-hardhat-template.git my-fhevm-project
cd my-fhevm-project && npm install
npm test # Run tests in mock mode
npm run compile # Compile contracts
Minimal contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { FHE, euint64, externalEuint64 } from "@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
contract EncryptedCounter is ZamaEthereumConfig {
euint64 private _count;
function increment(externalEuint64 encValue, bytes calldata proof) external {
euint64 value = FHE.fromExternal(encValue, proof);
_count = FHE.add(_count, value);
FHE.allowThis(_count);
FHE.allow(_count, msg.sender);
}
}
| Need | Type | Why |
|---|---|---|
| Token balances, amounts | euint64 | Fits up to ~18.4 quintillion; use 6 decimals not 18 |
| Scores, counters, vote tallies | euint32 | Cheaper ops than euint64, sufficient range |
| Tiers, flags, small enums | euint8 | 2-10x cheaper than euint64 |
| Boolean conditions | ebool | Result of all comparisons |
| Hash values, bitfields | euint256 | No arithmetic — bitwise and equality only |
| Encrypted addresses | eaddress | Only eq, ne, select — very limited |
| Large financial values | euint128 | Expensive (mul costs ~1,686K HCU) — use sparingly |
| Regular Solidity | fhEVM Equivalent |
|---|---|
if (a > b) { x = a; } else { x = b; } | x = FHE.select(FHE.gt(a, b), a, b); |
require(balance >= amount) | ebool ok = FHE.ge(balance, amount); + silent failure |
balance -= amount | balance = FHE.sub(balance, amount); FHE.allowThis(balance); |
return balance | Return handle; user decrypts off-chain |
| Revert on error | Silent failure: transfer 0, bid 0 |
a / b (both variables) | Impossible if both encrypted; restructure math |
mapping(addr => uint) | mapping(address => euint64) + ACL on every update |
| Situation | Load |
|---|---|
| Need full type details, casting, initialization | references/encrypted-types.md |
| Need exact operation signatures or HCU costs | references/fhe-operations.md |
| Writing ACL logic, cross-contract permissions, AA bundles | references/access-control.md |
| Handling user-submitted encrypted inputs | references/input-validation.md |
| Implementing reveal/result publication, decryption callbacks | references/decryption-patterns.md |
| Setting up or writing Hardhat tests | references/testing-guide.md |
| Building frontend encryption/decryption | references/frontend-integration.md |
| Reviewing contract for common mistakes (20 patterns) | anti-patterns/ANTI-PATTERNS.md |
| Writing/grading agent-generated code against trap cases | validation/prompts.md |
| Reviewing the validation evidence | validation/agent-effectiveness.md |
| Type | Arithmetic | Comparison | Bitwise | Select |
|---|---|---|---|---|
ebool | No | eq, ne | and, or, xor, not | Yes |
euint8–euint128 | Full | Full | Full | Yes |
euint256 | No | eq, ne only | Full | Yes |
eaddress | No | eq, ne only | No | Yes |
Import only what you use: import { FHE, euint64, ebool, externalEuint64 } from "@fhevm/solidity/lib/FHE.sol";
Arithmetic (euint8–128): FHE.add, FHE.sub, FHE.mul, FHE.div*, FHE.rem*, FHE.min, FHE.max, FHE.neg
*div/rem require plaintext right operand — encrypted divisor is NOT supported.
Comparison (→ ebool): FHE.eq, FHE.ne, FHE.ge, FHE.gt, FHE.le, FHE.lt
Branching: FHE.select(ebool, ifTrue, ifFalse) — the ONLY way to do conditional logic.
Random: FHE.randEuint8() – FHE.randEuint256(). Bounded: FHE.randEuint16(upperBound) (power-of-2). Transactions only, not view functions.
Type casting: FHE.asEuint32(plaintext) to encrypt. FHE.asEuint64(euint32Value) to upcast.
Scalar operations (one plaintext operand) are significantly cheaper. Prefer FHE.add(enc, 5) over FHE.add(enc1, enc2) when possible.
Operator overloading: With using FHE for *, you can write a + b instead of FHE.add(a, b). Supports +, -, *, &, |, ^, ~.
Every FHE operation creates a NEW ciphertext handle with ZERO permissions. Re-grant after EVERY operation:
euint64 newBalance = FHE.add(balance, amount);
_balances[to] = newBalance;
FHE.allowThis(newBalance); // Contract can use it in future txs
FHE.allow(newBalance, to); // Owner can request decryption
| Function | Scope | When |
|---|---|---|
FHE.allowThis(ct) | Persistent | Always — contract must access its own state |
FHE.allow(ct, addr) | Persistent | User/protocol needs long-term decrypt access |
FHE.allowTransient(ct, addr) | Single tx | Helper contract, cheaper gas |
FHE.makePubliclyDecryptable(ct) | Anyone, forever | Publishing results (vote tallies) |
FHE.isSenderAllowed(ct) | Check | Verify caller permission |
FHE.cleanTransientStorage() | Cleanup | Between AA bundled ops (prevent cross-op leaks) |
Users encrypt values client-side. Contracts validate via externalEuintXX + FHE.fromExternal:
function deposit(externalEuint64 encAmount, bytes calldata inputProof) external {
euint64 amount = FHE.fromExternal(encAmount, inputProof); // Validates ZKPoK
_balances[msg.sender] = FHE.add(_balances[msg.sender], amount);
FHE.allowThis(_balances[msg.sender]);
FHE.allow(_balances[msg.sender], msg.sender);
}
Never accept raw euintXX as function parameters — always use externalEuintXX + proof.
There is no FHE.decrypt(). Decryption goes through the off-chain Gateway → KMS threshold MPC:
FHE.makePubliclyDecryptable(ct) — mark for decryptionrelayer-sdk.publicDecrypt(handles) — get cleartext + proofFHE.checkSignatures(handles, clearValues, proof) — verifyfhevmjs is renamed to @zama-fhe/relayer-sdkIf you are reading older Zama tutorials, blog posts, or third-party guides, you will see the package name fhevmjs. This is the previous name for the official frontend SDK. The current package is @zama-fhe/relayer-sdk (sometimes called "relayer-sdk"). Same intent, refreshed API, native ESM, and handles client-side encryption + async decryption end-to-end.
| Old (deprecated) | New (use this) |
|---|---|
npm install fhevmjs | npm install @zama-fhe/relayer-sdk |
import { initFhevm, createInstance } from "fhevmjs" | import { initSDK, createInstance, SepoliaConfig } from "@zama-fhe/relayer-sdk" |
instance.createEncryptedInput(addr, user) | fhevm.createEncryptedInput(addr, user) (same shape) |
instance.reencrypt(...) (legacy) | fhevm.userDecrypt([handles], eip712Signature) |
instance.publicDecrypt(...) (legacy) | fhevm.publicDecrypt([handles]) |
If a search engine sends an agent to a fhevmjs snippet, mentally rename it to @zama-fhe/relayer-sdk and use the API table in references/frontend-integration.md. Don't npm install fhevmjs on a new project — the package is no longer maintained for current Zama Protocol releases.
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
contract MyContract is ZamaEthereumConfig { ... } // Auto-detects chainId
Hardhat: must set evmVersion: "cancun" in solidity settings.
import { ethers, fhevm } from "hardhat";
it("should work", async function () {
if (!fhevm.isMock) { this.skip(); }
const enc = await fhevm.createEncryptedInput(contractAddr, alice.address)
.add64(1000).encrypt();
await contract.connect(alice).deposit(enc.handles[0], enc.inputProof);
});
Input methods: .addBool, .add8, .add16, .add32, .add64, .add128, .add256, .addAddress
Foundry alternative: forge-fhevm (early-stage) supports pure Solidity tests with native fuzz testing. See references/testing-foundry.md.
When building encrypted ERC20 tokens, these rules prevent critical bugs:
euint64 max ≈ 18.4 quintillion. With 18 decimals → max ~18.4 tokens. With 6 decimals → max ~18.4 trillion tokens.type(uint256).max as placeholder amount — not the real encrypted value.balanceOf() takes no arguments — returns caller's own encrypted balance handle.transferFrom checks both balance AND allowance: FHE.and(hasBalance, hasAllowance).euint64 transferred = token.confidentialTransfer(to, amount);
ebool didTransfer = FHE.gt(transferred, FHE.asEuint64(0));
// Only update state if transfer actually happened
For production-grade ERC-7984 tokens, prefer the audited base contracts from @openzeppelin/confidential-contracts (verified against 0.4.0) over rolling your own:
npm install @openzeppelin/confidential-contracts
Note: The earlier
@openzeppelin/contracts-confidentialpackage is deprecated and renamed. Use@openzeppelin/confidential-contractsfor any new project.
import { ERC7984 } from "@openzeppelin/confidential-contracts/token/ERC7984/ERC7984.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
contract MyConfidentialToken is ERC7984, ZamaEthereumConfig {
constructor() ERC7984("My Token", "MTK", "https://example.com/uri") {}
}
(Constructor signature: ERC7984(string name_, string symbol_, string contractURI_).)
What ERC7984 already gives you — so you don't reinvent it:
balanceOf(address) with proper ACL re-grants on every transferconfidentialTransfer(to, externalEuint64, proof) + confidentialTransferFrom(...) with FHE.and(hasBalance, hasAllowance) checkseuint64)Transfer / Approval events that emit type(uint256).max as the value placeholder (anti-pattern #16 prevention)Override only domain-specific logic (mint/burn rules, transfer hooks). Do not re-implement ACL re-grants by hand — the base contract already does it correctly.
Standard extensions in @openzeppelin/confidential-contracts/token/ERC7984/extensions/ (v0.4.0):
ERC7984ERC20Wrapper — bidirectional wrap/unwrap with a plaintext ERC-20 (see next section)ERC7984Votes — votes-style governance hooks for confidential supplyERC7984Freezable — admin-controlled per-account freezesERC7984Restricted — allowlist / denylist gatingERC7984Omnibus — multi-account omnibus accountingERC7984Rwa — real-world-asset hooks (admin-pause, force-transfer)ERC7984ObserverAccess — third-party observer ACL grantsOur examples/ConfidentialERC20.sol is a from-scratch reference implementation for didactic purposes; for new dApps subclass ERC7984 instead.
ERC-7984 tokens commonly need to bridge in/out of plaintext ERC-20 liquidity. The pattern:
Transfer event) — privacy starts after wrapping.@openzeppelin/confidential-contracts ships ERC7984ERC20Wrapper (abstract — you subclass it):
import { ERC7984 } from "@openzeppelin/confidential-contracts/token/ERC7984/ERC7984.sol";
import { ERC7984ERC20Wrapper } from "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
contract WrappedUSDC is ERC7984ERC20Wrapper, ZamaEthereumConfig {
constructor(IERC20 underlying)
ERC7984ERC20Wrapper(underlying)
ERC7984("Confidential USDC", "cUSDC", "")
{}
}
Real wrapper API (v0.4.0, verified):
wrap(address to, uint256 amount) returns (euint64) — pulls plaintext ERC-20 via safeTransferFrom, mints encrypted token; returns the encrypted amount actually wrapped (after rate division).unwrap(address from, address to, euint64 amount) returns (bytes32 requestId) — burns encrypted balance and triggers async KMS decryption to release plaintext ERC-20 to to.unwrap(address from, address to, externalEuint64 encryptedAmount, bytes calldata inputProof) returns (bytes32 requestId) — variant that takes a fresh externalEuint64 + ZK proof.unwrapAmount(bytes32 requestId) returns (euint64) — view to read the encrypted amount handle for a pending unwrap.onTransferReceived is supported, so ERC-1363 tokens can transferAndCall directly into the wrapper (one-tx wrap).Frontend flow (relayer-sdk):
// 1. Wrap: amount is plaintext, public on-chain (the privacy boundary starts AFTER this).
await usdc.approve(wrapper.address, 1_000_000n); // 1 USDC (6 decimals)
const wrappedHandle = await wrapper.wrap(user.address, 1_000_000n);
// wrappedHandle is an euint64 — the actually-wrapped amount, encrypted.
// 2. Encrypted balance now lives inside the wrapper; only the user can decrypt off-chain.
const handle = await wrapper.balanceOf(user.address);
const cleartext = await fhevm.userDecrypt([handle], sig); // off-chain only
// 3. Encrypted confidential transfer to another wrapper user — fully private.
const enc = fhevm.createEncryptedInput(wrapper.address, user.address);
enc.add64(50_000n);
const { handles, inputProof } = await enc.encrypt();
await wrapper.confidentialTransfer(recipient, handles[0], inputProof);
// 4. Unwrap: returns a requestId; KMS async-decrypts and releases plaintext ERC-20.
// The unwrap amount IS public on settlement (necessary to move ERC-20).
const requestId = await wrapper.unwrap(user.address, recipient, handles[0], inputProof);
// Listen for the wrapper's settlement event; ERC-20 transfers to `recipient` once KMS finalises.
Privacy boundary reminder: wrap/unwrap amounts are public by necessity (they cross a plaintext boundary). What stays private is balances and intra-system transfers between wraps. Document this in your dApp UI so users don't assume wrapping is fully confidential.
20 anti-patterns are catalogued in anti-patterns/ANTI-PATTERNS.md, organized in three layers:
Top 12 quick reference (full list + production-grade #15–20 in the catalog):
| # | Anti-Pattern | Why It's Wrong | Fix |
|---|---|---|---|
| 1 | if (FHE.lt(a,b)) | ebool is bytes32, not bool | FHE.select(FHE.lt(a,b), x, y) |
| 2 | Missing ACL after op | New handle has 0 permissions | FHE.allowThis() + FHE.allow() |
| 3 | FHE.decrypt(x) | Function doesn't exist | FHE.makePubliclyDecryptable(x) + async |
| 4 | FHE.div(enc, enc) | Encrypted divisor unsupported | FHE.div(enc, plaintext) |
| 5 | FHE.add on euint256 | No arithmetic for 256-bit | Use euint128 or smaller |
| 6 | Uninitialized euint64 | Default is invalid, not zero | FHE.asEuint64(0) + allowThis |
| 7 | fn(euint64 amount) | Skips ZKPoK validation | fn(externalEuint64, bytes proof) |
| 8 | Assume tx success = transfer success | Silent failure transfers 0 | Check effective result |
| 15 | FHE.asEuint(plaintextParam) | Trivial encryption — value is in calldata | externalEuintXX + fromExternal |
| 16 | emit Transfer(from, to, amount) | Events are public — leaks encrypted value | emit Transfer(from, to, type(uint256).max) |
| 17 | Cross-contract pass without allowTransient | Callee has no ACL | allowTransient(ct, callee) before call |
| 18 | Trust callback cleartext directly | Callback is public — anyone forges values | FHE.checkSignatures(handles, vals, sigs) |
Transaction limit: 20M HCU total, 5M HCU depth. Key costs (non-scalar, euint64):
add/sub: 162K — max ~123/txmul: 596K — max ~33/txselect: 55K (constant) — cheaptrivialEncrypt/cast: 32 — freeRule: use the smallest type that fits. Prefer scalar ops. Avoid rem.
| Contract | Key Patterns | File |
|---|---|---|
| EncryptedCounter | Input validation, ACL, basic ops | examples/EncryptedCounter.sol |
| ConfidentialERC20 | Silent failure, allowances, ERC-7984 | examples/ConfidentialERC20.sol |
| ConfidentialVoting | Encrypted booleans, homomorphic tally | examples/ConfidentialVoting.sol |
| BlindAuction | FHE.select for max, async reveal | examples/BlindAuction.sol |
| AgentRegistry | Multi-input, threshold checks | examples/AgentRegistry.sol |
| ConfidentialTreasury | Full lifecycle, guardian voting, balance check | examples/ConfidentialTreasury.sol |
examples/ConfidentialTreasury.test.ts — Complete test file (15 tests, verified passing) demonstrating all SKILL.md testing patterns.
if/else/require on encrypted values — all use FHE.select + silent failureFHE.allowThis() after every operation storing a new ciphertextFHE.allow(ct, user) for every user needing decrypt accessexternalEuintXX + FHE.fromExternal() + proofZamaEthereumConfigFHE.decrypt() callscreateEncryptedInput addressesnode scripts/fhevm-lint.js path/to/Contract.sol — automated check of 12 of the 20 anti-patterns (regex-based, zero dependencies, <100ms)| Path | Load when |
|---|---|
references/encrypted-types.md | Need type details, casting, ranges |
references/fhe-operations.md | Need exact signatures or HCU costs |
references/access-control.md | Writing ACL, cross-contract, security, AA bundles |
references/input-validation.md | Handling user encrypted inputs |
references/decryption-patterns.md | Implementing reveal, callbacks, checkSignatures |
references/testing-guide.md | Setting up or writing Hardhat tests |
references/testing-foundry.md | Foundry (forge-fhevm) testing |
references/frontend-integration.md | Building frontend encryption/decryption |
anti-patterns/ANTI-PATTERNS.md | 20 anti-patterns: logic / operational / privacy boundary |
templates/contract-template.sol | Starting a new contract |
templates/hardhat.config.ts | Setting up Hardhat |
templates/test-template.ts | Starting a new test file |
examples/*.sol | Working reference implementations (6 contracts) |
examples/ConfidentialTreasury.test.ts | E2E-verified test pattern (15/15 passing) |
validation/prompts.md | 7 demo prompts + 8 adversarial trap prompts for testing agent error-prevention |
validation/agent-effectiveness.md | A/B validation report: 47 anti-patterns prevented, reproducibility protocol |
../../scripts/fhevm-lint.js (from repo root) | Static checker (12 rules, 0 deps) — run node scripts/fhevm-lint.js path/to/Contract.sol from the repo root. Catches the layer-3 privacy patterns Slither/Solhint/Mythril cannot. |
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
npx claudepluginhub 0xe1337/fhevm-skill --plugin fhevm