Install
$ agentstack add skill-0xe1337-fhevm-skill-fhevm ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
fhEVM Confidential Smart Contract Development
Build 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.
When to Use
Use this skill when any of these apply:
- Writing or modifying a Solidity contract that imports
@fhevm/solidity - Adding encrypted state (
euint,ebool,eaddress) to an existing contract - Reviewing fhEVM code (your own or AI-generated) for correctness or privacy leaks
- Building or modifying a frontend that uses
@zama-fhe/relayer-sdkfor client-side encryption or async decryption - Migrating a regular Solidity contract to fhEVM
- Writing or debugging Hardhat tests that use
fhevm.createEncryptedInput - Designing data flow for any application where on-chain values must stay encrypted under threshold MPC
Symptoms in chat or code that should trigger this skill:
- "encrypted balance / score / vote / bid"
- API references:
FHE.add,FHE.select,FHE.fromExternal,FHE.allowThis,FHE.makePubliclyDecryptable,ZamaEthereumConfig,externalEuintXX - File patterns:
@fhevm/solidityimports,*.solfiles declaringeuint*state, test files importingfhevmfrom hardhat,relayer-sdkin frontend deps - Error patterns: ACL access denied, "ebool cannot be evaluated",
FHE.decrypt is not a function, decryption returning garbage
When NOT to Use
This skill is overkill or off-topic for:
- Regular Solidity contracts with no encrypted state — vanilla Solidity guidance is enough; loading this skill adds noise.
- ZK-SNARK / ZK-STARK circuits that aren't fhEVM-related — different paradigm (zero-knowledge ≠ fully homomorphic).
- Concrete ML / TFHE-rs Rust applications — those use Python/Rust APIs and a different deployment model; this skill is Solidity-only.
- Generic Ethereum security audits where FHE is not present — use general security review skills, not this one.
- Contracts that only call fhEVM contracts but hold no encrypted state themselves — usually only the
references/access-control.mdcross-contract section is needed; full skill is unnecessary.
Development Philosophy
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:
- Understand the goal — What data must stay private? What can be public? Define the privacy boundary before writing code.
- Choose the right pattern — Use the decision tables below. Don't branch on encrypted values. Don't try to decrypt synchronously. Don't forget ACL.
- Verify at each step — After every FHE operation, ask: "Did I re-grant ACL on the new ciphertext? Did I use
FHE.selectinstead ofif? Did I validate the input withFHE.fromExternal?" - Test with mock mode — Run
npx hardhat testlocally. Mock FHE simulates all operations without a real coprocessor. Verify the logic works before deploying.
How fhEVM Works (Architecture)
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:
- Every operation creates a new handle → FHE math produces a new ciphertext (different noise/randomness), so you must re-grant ACL permissions on the new handle
- Handles cannot be evaluated as booleans → no node has the secret key; the EVM literally cannot evaluate
if (eboolHandle) - Decryption requires the off-chain KMS → 9/13 threshold MPC, always async, no
FHE.decrypt() - Uninitialized handles are
bytes32(0)→ not encrypted zero, just an invalid pointer - Operations are expensive → each FHE op involves noise management on large polynomial structures; bootstrapping refreshes noise but costs HCU
> See references/architecture.md for the full 6-component system, TFHE principles, handle format, and HCU metering details.
Quick Start
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);
}
}
Decision Tables
When to use which encrypted type
| 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 |
Solidity pattern → fhEVM equivalent
| 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 |
When to load reference files
| 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 |
Encrypted Types (Summary)
| 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";
FHE Operations (Summary)
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 +, -, *, &, |, ^, ~.
Access Control (ACL) — The #1 Source of Bugs
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) |
Input Validation
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.
Decryption (Always Async)
There is no FHE.decrypt(). Decryption goes through the off-chain Gateway → KMS threshold MPC:
- On-chain:
FHE.makePubliclyDecryptable(ct)— mark for decryption - Off-chain:
relayer-sdk.publicDecrypt(handles)— get cleartext + proof - On-chain (optional):
FHE.checkSignatures(handles, clearValues, proof)— verify
Frontend SDK: fhevmjs is renamed to @zama-fhe/relayer-sdk
If 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.
Configuration
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
contract MyContract is ZamaEthereumConfig { ... } // Auto-detects chainId
Hardhat: must set evmVersion: "cancun" in solidity settings.
Testing (Hardhat Mock)
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.
Confidential Token Design (ERC-7984)
When building encrypted ERC20 tokens, these rules prevent critical bugs:
- Decimals MUST be 6, not 18.
euint64max ≈ 18.4 quintillion. With 18 decimals → max ~18.4 tokens. With 6 decimals → max ~18.4 trillion tokens. - Transfers never revert on insufficient balance — they silently transfer 0 (privacy preservation).
- Events emit
type(uint256).maxas placeholder amount — not the real encrypted value. balanceOf()takes no arguments — returns caller's own encrypted balance handle.transferFromchecks both balance AND allowance:FHE.and(hasBalance, hasAllowance).- Composability trap: When contract A calls B's confidential transfer and it silently returns 0, A must check the effective result before updating its own state:
``solidity euint64 transferred = token.confidentialTransfer(to, amount); ebool didTransfer = FHE.gt(transferred, FHE.asEuint64(0)); // Only update state if transfer actually happened ``
OpenZeppelin Confidential Contracts
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-confidential package is deprecated and renamed. Use @openzeppelin/confidential-contracts for 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: `ERC7
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: 0xE1337
- Source: 0xE1337/fhevm-skill
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.