Use when writing, structuring, or debugging tests for FHEVM contracts. Covers mocked mode vs real protocol, Hardhat decrypt helpers, input encryption in tests, and the false-confidence gap between local and testnet behavior.
Use this skill when setting up a test suite for FHEVM contracts, deciding between mocked and real protocol testing, or diagnosing why tests pass locally but fail on testnet. The encryption layer introduces bugs that only surface under specific runtime conditions.
The Hardhat plugin gives you two local mock modes plus one real-encryption mode:
localhost: mock encryption, persistent, useful for local app integrationNeither mocked mode is sufficient alone. Mocked modes give fast feedback on logic. Sepolia catches ACL, gas, and timing bugs that mocked execution structurally cannot.
Think of mocked mode as a type-checker. Think of real protocol as an integration test.
hre.fhevm (userDecryptEuint, userDecryptEbool, userDecryptEaddress). Treat any additional mock debug helpers as local-only diagnostics, not onchain APIs.@fhevm/hardhat-plugin — Hardhat integration for local FHE testing@fhevm/mock-utils — Mock FHE operations and debug helpers for unit tests// hardhat.config.js — mocked mode is the default, no coprocessor needed
require("@fhevm/hardhat-plugin");
module.exports = { solidity: "0.8.24", defaultNetwork: "hardhat" };
Prefer the official Hardhat runtime API from hre.fhevm / import { fhevm } from "hardhat":
const { fhevm } = require("hardhat");
const input = fhevm.createEncryptedInput(contractAddress, signerAddress);
input.add64(1000n);
const enc = await input.encrypt();
await contract.transfer(recipient, enc.handles[0], enc.inputProof);
@fhevm/mock-utils is the underlying mock library, but for test suites the official docs now
lead with the Hardhat plugin API rather than importing low-level mock helpers directly.
Prefer the Hardhat decrypt helpers when you want test assertions that match the actual decrypt flow more closely:
const { fhevm } = require("hardhat");
const { FhevmType } = require("@fhevm/hardhat-plugin");
const handle = await token.balanceOf(alice.address);
const clearBalance = await fhevm.userDecryptEuint(
FhevmType.euint64,
handle,
contractAddress,
alice
);
If your setup exposes mock debug decrypt helpers, treat them as local inspection tools for diagnosis, not as proof that the real user decryption flow works.
| Dimension | Mocked Mode | Real Protocol (Testnet) |
|---|---|---|
| Setup | npx hardhat test --network hardhat or --network localhost -- no real encryption | Testnet RPC, funded accounts, real relayer/coprocessor path |
| Speed | Fast local feedback | Slower due to real off-chain round-trips |
| FHE arithmetic | Simulated locally -- correct results | Real coprocessor -- correct results |
| ACL enforcement | Not representative of real ACL boundaries | Real ACL enforced |
| Gas costs | Not representative of real FHE costs | Realistic and materially higher |
| Async decrypt timing | Local mock execution, no representative off-chain latency | Real latency, separate off-chain step and follow-up tx |
| Cross-contract ACL | Works without grants (false positive) | Requires explicit allowTransient/allow |
| What it proves | Logic correctness, arithmetic, control flow | Deployment readiness, ACL correctness, gas feasibility |
A green mocked suite can still hide these classes of bugs. Plan dedicated testnet coverage for each:
FHE.allowThis / FHE.allow — mocked flows can appear to work despite missing grants. On testnet the next read, decrypt, or downstream computation fails.FHE.allow(newHandle, user) is missing, user decryption breaks only in production.allowTransient / allow; contract B fails at the real coprocessor boundary.it("grants ACL to recipient after transfer", async function () {
await token.connect(sender).transfer(recipient, encAmount, proof);
const canDecrypt = await acl.isAllowed(await token.balanceOf(recipient), recipient.address);
expect(canDecrypt).to.be.true;
});
Many encrypted token flows implement failure paths via FHE.select rather than reverting on the
encrypted condition itself:
it("insufficient balance results in zero transfer", async function () {
await token.connect(sender).transfer(recipient, encAmount200, proof);
const senderHandle = await token.balanceOf(sender.address);
const balance = await fhevm.userDecryptEuint(
FhevmType.euint64,
senderHandle,
contractAddress,
sender
);
expect(balance).to.equal(100n); // unchanged — transfer silently failed
});
Encrypted arithmetic wraps on overflow. Test boundaries if your contract does not guard them.
Before mainnet, run these on testnet with the real coprocessor:
Do not treat testnet runs as optional.
Treat these as signoff gates, not aspirations. A single missing item can mean funds-at-risk on mainnet.
Gate 1 — Mocked suite (fast feedback, logic only)
FHE.select has at least one test that exercises the false branchGate 2 — Testnet suite with real coprocessor (integration, must catch what mocked cannot)
FHE.allow)allowThis on the receiving side)inputProof rejection tested with a mismatched sender and a mismatched contract addressGate 3 — Pre-mainnet review
fhevm-security-audit checklist completed against the final codeShip only when all three gates pass. Mocked-mode green with no testnet run is a common path to a production incident.
False confidence. ACL bugs, gas issues, and timing problems surface in production.
Tests validate nothing about real user experience if every assertion relies on mock-only decrypt inspection.
Without failure-path tests, you have no idea what happens when contract-specific encrypted flows fall back, transfers fail, or ACL access is missing.
Mocked FHE costs near-zero gas. Real contracts may exceed block gas limits.
When applying this skill, structure test plans around:
skills/fhevm-acl-lifecycle/SKILL.md — the class of bugs mocked mode cannot catchskills/fhevm-security-audit/SKILL.md — what to assert against, especially silent fallbacks