Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build, test, and deploy confidential smart contracts on Zama Protocol using FHEVM. Use this skill whenever the user wants to write encrypted Solidity (FHE types, encrypted inputs, access control, decryption, frontend integration), work with euint/ebool/eaddress types, handle ZK input proofs, request async decryption, or deploy on Sepolia/mainnet. Covers the full FHEVM development lifecycle including common anti-patterns to avoid.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 118% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 184% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 282% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 318% | 0% |
You are an expert Zama Protocol / FHEVM developer. When asked to build a confidential smart contract or FHEVM application, follow this skill precisely.
bashmkdir my-fhevm-app && cd my-fhevm-app npm init -y npm install --save-dev hardhat @fhevm/hardhat-plugin @fhevm/solidity npm install --save-dev @nomicfoundation/hardhat-toolbox dotenv npx hardhat init # choose "TypeScript project"
typescriptimport "@fhevm/hardhat-plugin"; // MUST be first import import "@nomicfoundation/hardhat-toolbox"; import { HardhatUserConfig } from "hardhat/config"; import * as dotenv from "dotenv"; dotenv.config(); const config: HardhatUserConfig = { solidity: "0.8.24", networks: { hardhat: { chainId: 31337 }, // local mock FHE — no testnet needed for dev sepolia: { url: process.env.SEPOLIA_RPC_URL || "", accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], chainId: 11155111, }, }, fhevm: { mock: true }, // set false for real testnet }; export default config;
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_KEY
PRIVATE_KEY=0xabc...| FHE Type | Solidity Equivalent | Notes | |-----------------|--------------------|------------------------------------| | ebool | bool | ~2× cheaper than euint8 for flags | | euint8 | uint8 | 0–255 | | euint16 | uint16 | | | euint32 | uint32 | | | euint64 | uint64 | Default for token amounts | | euint128 | uint128 | | | euint256 | uint256 | Most expensive — use sparingly | | eaddress | address | Supports eq, ne only |
External (input) variants — used as function parameters, never stored: externalEbool, externalEuint8 … externalEuint256, externalEaddress
Every FHEVM contract follows this exact structure:
solidity// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; // ① Always import FHE library first import { FHE, euint64, ebool, eaddress, externalEuint64, externalEbool } from "@fhevm/solidity/lib/FHE.sol"; // ② Import the network config (drives gateway + KMS addresses) import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; // ③ Inherit the network config contract ConfidentialVault is SepoliaConfig { // ④ NEVER store externalEuintXX — only euintXX mapping(address => euint64) private _balances; // ⑤ Emit handles, never plaintext event ConfidentialDeposit(address indexed user, euint64 amount); // ─── Input Handling ────────────────────────────────────────────────────── // ⑥ externalEuintXX + bytes calldata inputProof → always paired function deposit( externalEuint64 encryptedAmount, bytes calldata inputProof ) external { // ⑦ FHE.fromExternal validates ZKPoK — always do this before any use euint64 amount = FHE.fromExternal(encryptedAmount, inputProof); // ⑧ Grant ACL permissions immediately after fromExternal FHE.allowThis(amount); // contract can use it FHE.allow(amount, msg.sender); // depositor can read it _balances[msg.sender] = FHE.add(_balances[msg.sender], amount); FHE.allowThis(_balances[msg.sender]); FHE.allow(_balances[msg.sender], msg.sender); emit ConfidentialDeposit(msg.sender, amount); } // ─── ACL-Gated Read ────────────────────────────────────────────────────── // ⑨ Return euintXX, never decrypt on-chain unless truly required function getBalance() external view returns (euint64) { return _balances[msg.sender]; } // ─── Async Decryption ──────────────────────────────────────────────────── bool private _decryptionPending; uint256 private _latestRequestId; uint64 public revealedTotal; // only populated after callback function requestReveal() external { // ⑩ Guard against double requests require(!_decryptionPending, "Decryption already in progress"); bytes32[] memory handles = new bytes32[](1); handles[0] = FHE.toBytes32(_balances[msg.sender]); _latestRequestId = FHE.requestDecryption( handles, this.onRevealCallback.selector ); _decryptionPending = true; } // ⑪ Callback MUST be external, MUST have onlyGateway modifier (auto-injected by config) function onRevealCallback( uint256 requestId, bytes memory cleartexts, bytes memory decryptionProof ) external onlyGateway returns (bool) { require(requestId == _latestRequestId, "Unknown request"); revealedTotal = abi.decode(cleartexts, (uint64)); _decryptionPending = false; return true; } }
solidityeuint64 sum = FHE.add(a, b); euint64 diff = FHE.sub(a, b); // ⚠ wraps on underflow — use FHE.ge guard euint64 prod = FHE.mul(a, b); // expensive — minimize usage euint64 q = FHE.div(a, b); // b must be plaintext uint (not euint) euint64 r = FHE.rem(a, b); // b must be plaintext uint euint64 mn = FHE.min(a, b); euint64 mx = FHE.max(a, b); euint64 neg = FHE.neg(a);
solidityeuint64 andVal = FHE.and(a, b); euint64 orVal = FHE.or(a, b); euint64 xorVal = FHE.xor(a, b); euint64 notVal = FHE.not(a); // only for ebool and euintX euint64 shl = FHE.shl(a, b); // b is plaintext euint64 shr = FHE.shr(a, b); // b is plaintext
eboolsolidityebool eq = FHE.eq(a, b); ebool ne = FHE.ne(a, b); ebool lt = FHE.lt(a, b); ebool le = FHE.le(a, b); ebool gt = FHE.gt(a, b); ebool ge = FHE.ge(a, b);
solidity// FHE.select(condition: ebool, ifTrue: T, ifFalse: T) → T euint64 result = FHE.select(condition, valueA, valueB); eaddress winner = FHE.select(isHigher, candidateA, candidateB);
solidityeuint32 narrow = FHE.asEuint32(someEuint64); // truncates high bits euint64 wide = FHE.asEuint64(someEuint32); // zero-extends ebool flag = FHE.asEbool(someEuint8); // 0 → false, else true euint64 fromPT = FHE.asEuint64(uint64(42)); // plaintext → encrypted eaddress ea = FHE.asEaddress(plainAddr);
solidityeuint64 rand64 = FHE.randEuint64(); euint8 rand8 = FHE.randEuint8(); // ⚠ randEuint is non-deterministic — do NOT use in view functions
Every encrypted value has an ACL. If an address is not in the ACL, it cannot decrypt or use that handle — even view functions will revert or return garbage.
solidityFHE.allowThis(handle); // grants THIS CONTRACT access (required for storage) FHE.allow(handle, addr); // grants addr access (user, another contract) FHE.allowForDecryption(handle); // makes handle globally decryptable (use with care)
FHE.fromExternal — call FHE.allowThis(handle) immediately.allowThis OR it cannot read the value later.FHE.allow(handle, user) or they get Access Denied on decrypt.FHE.add / any operation — the result is a NEW handle — re-grant ACL.solidityfunction transfer(address to, externalEuint64 encAmt, bytes calldata proof) external { euint64 amount = FHE.fromExternal(encAmt, proof); FHE.allowThis(amount); euint64 senderBal = _balances[msg.sender]; ebool canPay = FHE.ge(senderBal, amount); // check without revealing euint64 newSenderBal = FHE.select(canPay, FHE.sub(senderBal, amount), senderBal ); euint64 newRecipientBal = FHE.select(canPay, FHE.add(_balances[to], amount), _balances[to] ); euint64 transferred = FHE.select(canPay, amount, FHE.asEuint64(0)); // Grant ACL for every new handle before storing FHE.allowThis(newSenderBal); FHE.allow(newSenderBal, msg.sender); FHE.allowThis(newRecipientBal); FHE.allow(newRecipientBal, to); FHE.allowThis(transferred); FHE.allow(transferred, msg.sender); FHE.allow(transferred, to); _balances[msg.sender] = newSenderBal; _balances[to] = newRecipientBal; }
typescriptimport { fhevm } from "hardhat"; // Build an encrypted input — address order matters for ZKPoK const input = fhevm.createEncryptedInput( contractAddress, // the contract that will call fromExternal signerAddress // the EOA submitting the tx ); // Add values in ANY order — handles are indexed input.addBool(true); // index 0 input.add64(BigInt(1000)); // index 1 input.add8(3); // index 2 const enc = await input.encrypt(); // enc.handles[0] → externalEbool // enc.handles[1] → externalEuint64 // enc.handles[2] → externalEuint8 // enc.inputProof → bytes (single ZKPoK for all inputs) await contract.myFunc(enc.handles[0], enc.handles[1], enc.handles[2], enc.inputProof);
typescriptimport { BrowserProvider } from "ethers"; import { createInstance } from "fhevmjs"; const provider = new BrowserProvider(window.ethereum); const instance = await createInstance({ provider }); const input = instance.createEncryptedInput(contractAddress, userAddress); input.add64(BigInt(transferAmount)); const enc = await input.encrypt(); // Pass to contract await contract.deposit(enc.handles[0], enc.inputProof);
typescript// User decrypts their own value using their private key via the gateway const balance = await contract.getBalance(); // returns euint64 handle (bytes32) const decrypted = await instance.decrypt(contractAddress, balance); console.log("My balance:", decrypted.toString());
Decryption is always asynchronous. Pattern:
Contract → requestDecryption() → emit EventDecryption
↓
Relayer picks up event
↓
KMS decrypts
↓
Gateway calls callback on contractsolidityfunction revealAuction() external { require(block.timestamp > auctionEnd, "Auction ongoing"); bytes32[] memory handles = new bytes32[](2); handles[0] = FHE.toBytes32(_highestBid); handles[1] = FHE.toBytes32(_highestBidder); FHE.requestDecryption(handles, this.auctionCallback.selector); } function auctionCallback( uint256 requestId, bytes memory cleartexts, bytes memory decryptionProof ) external onlyGateway returns (bool) { // Decode in the SAME order as handles[] (uint64 bid, address bidder) = abi.decode(cleartexts, (uint64, address)); winningBid = bid; winningBidder = bidder; emit AuctionSettled(bidder, bid); return true; }
externalonlyGateway modifier (provided by config inheritance)(uint256 requestId, bytes memory cleartexts, bytes memory decryptionProof)booltypescript// In hardhat.config.ts: fhevm: { mock: true } // Mock mode: FHE ops are computed in plaintext locally — instant, free import { ethers } from "hardhat"; import { fhevm } from "hardhat"; describe("ConfidentialVault", () => { it("deposits and reads balance", async () => { const [alice] = await ethers.getSigners(); const Vault = await ethers.getContractFactory("ConfidentialVault"); const vault = await Vault.deploy(); const input = fhevm.createEncryptedInput(vault.target, alice.address); input.add64(BigInt(500)); const enc = await input.encrypt(); await vault.connect(alice).deposit(enc.handles[0], enc.inputProof); // Read encrypted balance — mock mode can decrypt directly const encBal = await vault.connect(alice).getBalance(); const bal = await fhevm.decrypt64(encBal); expect(bal).to.equal(500n); }); });
bashnpx hardhat test # mock mode — runs offline npx hardhat test --network sepolia # real testnet (slower)
typescriptimport { ethers } from "hardhat"; async function main() { const [deployer] = await ethers.getSigners(); console.log("Deploying with:", deployer.address); const Vault = await ethers.getContractFactory("ConfidentialVault"); const vault = await Vault.deploy(); await vault.waitForDeployment(); console.log("Vault deployed to:", await vault.getAddress()); } main().catch(console.error);
bashnpx hardhat run scripts/deploy.ts --network sepolia
| Network | Config Import | Chain ID | |------------------|----------------------------------------------------|----------| | Sepolia testnet | SepoliaConfig from @fhevm/solidity/config/... | 11155111 | | Ethereum mainnet | ZamaEthereumConfig from @fhevm/solidity/config | 1 | | Local mock | MockZamaConfig (auto via hardhat plugin) | 31337 |
The following patterns cause silent failures, security holes, or wasted gas:
if on encrypted valuessolidity// WRONG — you cannot branch on encrypted data if (FHE.gt(balance, amount)) { ... } // CORRECT — use FHE.select euint64 result = FHE.select(FHE.gt(balance, amount), trueVal, falseVal);
externalEuintXX (input types)solidity// WRONG — externalEuintXX is a one-time input wrapper, not a storage type mapping(address => externalEuint64) balances; // compilation error in new versions // CORRECT mapping(address => euint64) balances;
FHE.allowThis before storingsolidity// WRONG — contract will not be able to read its own state _balances[user] = FHE.add(a, b); // CORRECT euint64 newBal = FHE.add(a, b); FHE.allowThis(newBal); _balances[user] = newBal;
solidity// WRONG — result of FHE.add is a NEW handle — old ACL does not carry over euint64 sum = FHE.add(a, b); // sum has NO ACL — storing it means contract cannot read it // CORRECT euint64 sum = FHE.add(a, b); FHE.allowThis(sum); FHE.allow(sum, user);
solidity// WRONG — TFHE.decrypt() was removed in v0.5+ uint64 plain = TFHE.decrypt(encryptedVal); // CORRECT — use async requestDecryption + callback
solidity// WRONG — inputProof is a ZKPoK bound to ONE tx context // Replaying it will fail validation silently or revert // CORRECT — generate fresh encrypted input per transaction
FHE.div / FHE.rem with encrypted divisorsolidity// WRONG — divisor must be a plaintext uint euint64 q = FHE.div(a, b); // b must be uint64, NOT euint64 // CORRECT uint64 divisor = 100; euint64 q = FHE.div(a, divisor);
euint256 for token balancessolidity// WRONG — euint256 is 4–8× more gas expensive than euint64 mapping(address => euint256) balances; // CORRECT — euint64 (18.4 quadrillion units) is enough for ERC-20 with 18 decimals mapping(address => euint64) balances;
requestDecryption replaysolidity// WRONG — no guard, allows flooding the gateway function requestReveal() external { FHE.requestDecryption(...); } // CORRECT bool private _pending; function requestReveal() external { require(!_pending, "pending"); _pending = true; FHE.requestDecryption(...); } // Reset _pending = false in callback
FHE.allowForDecryption in transfer logicsolidity// WRONG — makes balance globally readable by anyone with the handle FHE.allowForDecryption(balance); // CORRECT — only use allowForDecryption for intentional public reveals (e.g. auction end)
solidity// WRONG — leaks whether condition was true emit Transfer(msg.sender, to, FHE.decrypt(amount)); // deprecated + leaks // CORRECT — emit the encrypted handle; let authorized parties decrypt client-side emit Transfer(msg.sender, to, amount); // amount is euint64 (bytes32 handle)
onlyGateway on decryption callbacksolidity// WRONG — anyone can call the callback with fake data function myCallback(uint256 id, bytes memory data, bytes memory proof) external { ... } // CORRECT — inherited from config, protects against spoofed decryption results function myCallback(...) external onlyGateway returns (bool) { ... }
| Operation | Relative Cost | Notes | |--------------------|---------------|------------------------------------------| | FHE.add | 1× | Baseline | | FHE.mul | 4–6× | Avoid in hot paths | | FHE.div/rem | 8–12× | Use only when necessary | | FHE.gt/lt/eq | 1.5× | Cheap comparisons | | FHE.select | 1× | Prefer over branches | | euint8 ops | 0.6× | Use smallest type that fits | | euint256 ops | 6–8× | Avoid unless required | | randEuint64 | 2× | Don't call in loops |
Pack multiple inputs into ONE inputProof call — minimizes ZKPoK computation.
contracts/ConfidentialERC20.sol)solidity// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { FHE, euint64, eaddress, externalEuint64 } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; contract ConfidentialERC20 is SepoliaConfig { string public name; string public symbol; uint8 public decimals = 6; address public owner; uint256 public totalSupply; mapping(address => euint64) private _balances; event Transfer(address indexed from, address indexed to); event Mint(address indexed to); constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function mint(address to, uint64 amount) external onlyOwner { euint64 encAmt = FHE.asEuint64(amount); FHE.allowThis(encAmt); FHE.allow(encAmt, to); _balances[to] = FHE.add(_balances[to], encAmt); FHE.allowThis(_balances[to]); FHE.allow(_balances[to], to); totalSupply += amount; emit Mint(to); } function transfer( address to, externalEuint64 encAmount, bytes calldata inputProof ) external { euint64 amount = FHE.fromExternal(encAmount, inputProof); FHE.allowThis(amount); FHE.allow(amount, msg.sender); FHE.allow(amount, to); euint64 senderBal = _balances[msg.sender]; ebool ok = FHE.ge(senderBal, amount); euint64 sent = FHE.select(ok, amount, FHE.asEuint64(0)); euint64 newFrom = FHE.select(ok, FHE.sub(senderBal, amount), senderBal); euint64 newTo = FHE.add(_balances[to], sent); FHE.allowThis(sent); FHE.allow(sent, msg.sender); FHE.allow(sent, to); FHE.allowThis(newFrom); FHE.allow(newFrom, msg.sender); FHE.allowThis(newTo); FHE.allow(newTo, to); _balances[msg.sender] = newFrom; _balances[to] = newTo; emit Transfer(msg.sender, to); } function balanceOf(address user) external view returns (euint64) { return _balances[user]; } }
src/App.tsx)typescriptimport { createInstance } from "fhevmjs"; import { BrowserProvider, Contract } from "ethers"; import ABI from "./abi/ConfidentialERC20.json"; const CONTRACT = "0xYOUR_CONTRACT_ADDRESS"; async function getBalance(): Promise<bigint> { const provider = new BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const instance = await createInstance({ provider }); const contract = new Contract(CONTRACT, ABI, signer); const handle = await contract.balanceOf(await signer.getAddress()); const decrypted = await instance.decrypt(CONTRACT, handle); return decrypted; } async function transfer(to: string, amount: bigint): Promise<void> { const provider = new BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const instance = await createInstance({ provider }); const contract = new Contract(CONTRACT, ABI, signer); const input = instance.createEncryptedInput(CONTRACT, await signer.getAddress()); input.add64(amount); const enc = await input.encrypt(); const tx = await contract.transfer(to, enc.handles[0], enc.inputProof); await tx.wait(); }
Before submitting to mainnet:
FHE.fromExternal calls have inputProof in the same txFHE.allowThis before storageFHE.allow(handle, user)if/else branching on encrypted values (use FHE.select)euint256 where euint64 is sufficientTFHE.decrypt (deprecated) — async callback pattern usedonlyGateway modifier_decryptionPending guard in placeinputProof never reused across transactionsSepoliaConfig / ZamaEthereumConfig)fhevm: { mock: true } before testnet deploymentnpx hardhat fhevm-lint to catch anti-patterns automaticallyBefore deployment, run the companion linter to catch all anti-patterns:
bashnode scripts/fhevm-lint.js contracts/
Expected output for a clean contract: ✅ 0 issues found.
Other measured skills in the registry, with their headline benchmark lift.