Help us improve
Share bugs, ideas, or general feedback.
From bn-fhevm
Used when reviewing, debugging, auditing, or pre-commit-checking FHEVM Solidity or client code. Catalogues the v0.11-era breaking renames (TFHE→FHE, SepoliaZamaFHEVMConfig→ZamaEthereumConfig, ConfidentialFungibleToken→ERC7984, TFHESafeMath→FHESafeMath, tokenURI→contractURI, isUserAllowed→canTransact), the removed FHE.requestDecryption API, deprecated fhevmjs, and high-frequency footguns (missing FHE.allowThis after state writes, view functions returning encrypted types, .env-based Hardhat secrets). Triggers on FHEVM code review, anti-pattern, audit, migration from pre-v0.9/v0.11, pre-commit check, ACLNotAllowed, EmptyDecryptionProof, or DeclarationError.
npx claudepluginhub bootnodedev/zama-s2-bounty-skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/bn-fhevm:fhevm-antipatternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A review and audit checklist. Every entry has: the **wrong pattern**, **why it fails**, the **correct pattern**, and a **verification** step (compile check, grep, runtime assertion, or doc cross-reference). Run through this list before opening a PR that touches FHEVM code.
Measures whether skills, rules, and agent definitions are actually followed by auto-generating test scenarios at 3 strictness levels and reporting compliance rates with full tool call timelines.
Share bugs, ideas, or general feedback.
A review and audit checklist. Every entry has: the wrong pattern, why it fails, the correct pattern, and a verification step (compile check, grep, runtime assertion, or doc cross-reference). Run through this list before opening a PR that touches FHEVM code.
Many FHEVM antipatterns compile cleanly. TFHE.* against a v0.11 @fhevm/solidity gives a DeclarationError, yes — but missing FHE.allowThis(handle) compiles and deploys fine, then reverts with ACLNotAllowed() on the second transaction. Never treat "it compiles" as validation.
fhevmjs is dead — if you see it, it is wrongAny code or doc snippet that imports from fhevmjs is at least a year out of date. Migrate to @zama-fhe/sdk@3.0.0 (the primary, provider-agnostic client SDK — works with viem, ethers, or any EVM library). @zama-fhe/relayer-sdk@~0.4.x is the legacy low-level transport that v3 wraps as a transitive dep; do not install it directly for new code. Even import paths in README examples need fixing.
.env is forbidden in Hardhat secretsThis skill set treats .env-based secret loading in Hardhat configs as a forbidden pattern — hardhat vars is the single source of truth. See fhevm-setup for the full workflow.
The eight breaking renames between v0.9 and v0.11 (TFHE → FHE, SepoliaZamaFHEVMConfig → ZamaEthereumConfig, removed FHE.requestDecryption, fhevmjs → @zama-fhe/*, ConfidentialFungibleToken → ERC7984, TFHESafeMath → FHESafeMath, tokenURI → contractURI, isUserAllowed → canTransact) live in references/v0.11-migration.md as AP-1 through AP-8. Each entry has the wrong form, why it fails, the correct v0.11 form, and a grep to verify a clean codebase. Read that file first if you are migrating an older codebase or reviewing a fresh-cloned repo for stale imports.
The behavioral antipatterns below — missing ACL grants, view-returning encrypted types, .env secrets, Sepolia-only failure modes — are AP-9 through AP-12. They survive even on a freshly migrated v0.11 codebase, and they tend to compile cleanly while reverting at runtime.
FHE.allowThis after a state writeWrong:
function increment(externalEuint64 enc, bytes calldata proof) external {
euint64 delta = FHE.fromExternal(enc, proof);
_count = FHE.add(_count, delta);
// NO FHE.allowThis(_count) — contract cannot reuse the handle next tx
}
Why wrong: Every new handle produced by an FHE.* op carries no ACL grants from its inputs. On the next transaction, the contract will read _count and immediately fail with ACLNotAllowed() — because the contract's own stored handle has no permission to be read by the contract's own code. The handle permission resets every time a new op creates a new handle.
Correct:
function increment(externalEuint64 enc, bytes calldata proof) external {
euint64 delta = FHE.fromExternal(enc, proof);
_count = FHE.add(_count, delta);
FHE.allowThis(_count); // contract can reuse next tx
FHE.allow(_count, msg.sender); // user can decrypt
}
Verify: grep -A 2 "FHE\\.add\\|FHE\\.sub\\|FHE\\.mul\\|FHE\\.select\\|FHE\\.min\\|FHE\\.max" contracts/ — every result stored in state should be followed (within the same function) by an FHE.allowThis on that handle. If the caller needs to decrypt, also an FHE.allow(handle, msg.sender).
Wrong:
// REVERTS — view cannot emit the symbolic events FHE needs
function balanceOf(address u) external view returns (euint64) {
return _balances[u];
}
Why wrong: Two failure modes, depending on the body:
FHE.* ops — reverts. FHE.* calls emit symbolic events to FHEVMExecutor; view cannot emit events.bytes32 under the wrapper), but is misleading: the euint64 return type implies a plaintext value the caller can't actually consume without an ACL grant + off-chain userDecrypt.The non-view rule is different — function _update(...) returns (euint64 transferred) and similar non-view returns of encrypted handles are permitted and routine (see ERC-7984 confidentialTransfer overloads). The problem is specifically view + the encrypted-type return shape.
ERC-7984 caveat: The OpenZeppelin IERC7984 interface declares confidentialBalanceOf and confidentialTotalSupply as external view returns (euint64) because the implementation reads storage only. These work mechanically and you must keep the signature to conform to the standard. For non-standard accessors in your own contracts, prefer the bytes32 form below.
Correct:
// Return the handle as bytes32 — the caller must hold an ACL grant
// (granted via FHE.allow on a prior state-mutating call).
function balanceHandleOf(address u) external view returns (bytes32) {
return euint64.unwrap(_balances[u]);
}
Then, on the client, use instance.userDecrypt(handle, signer) — the decryption happens off-chain. See fhevm-decryption.
Verify: grep -En "function.*\\bview\\b.*returns \\(euint|function.*\\bview\\b.*returns \\(ebool|function.*\\bview\\b.*returns \\(eaddress" contracts/ — no hits.
.env for Hardhat secrets instead of hardhat varsWrong:
// hardhat.config.ts
import "dotenv/config";
const config: HardhatUserConfig = {
networks: {
sepolia: {
url: `https://eth-sepolia.g.alchemy.com/v2/${process.env.RPC_KEY}`,
accounts: [process.env.PRIVATE_KEY!],
},
},
};
Why wrong: .env files land in git one copy-paste mistake away from leaking a mnemonic. They also do not survive CI cleanly (.env not present → undefined → silent "" URL). The repo convention (see CLAUDE.md) is hardhat vars exclusively.
Correct:
// hardhat.config.ts
import { HardhatUserConfig, vars } from "hardhat/config";
const MNEMONIC = vars.get("MNEMONIC");
// RPC_KEY is the API key for whichever Sepolia provider you wire below
// (Alchemy by default — swap the URL host for Infura / paid RPC).
const RPC_KEY = vars.get("RPC_KEY");
const config: HardhatUserConfig = {
networks: {
sepolia: {
url: `https://eth-sepolia.g.alchemy.com/v2/${RPC_KEY}`,
accounts: { mnemonic: MNEMONIC },
},
},
};
Set values via npx hardhat vars set MNEMONIC etc. For CI, export HARDHAT_VAR_MNEMONIC=... — no .env file created.
Verify: grep -rn "dotenv\\|process\\.env\\.\\(MNEMONIC\\|PRIVATE_KEY\\|RPC_KEY\\|ETHERSCAN_API_KEY\\)" . — no hits. See fhevm-setup.
The mock coprocessor on the hardhat network masks several Sepolia-only
failure modes. Mock tests pass; deployment to Sepolia silently rejects the
transaction. Three known sub-modes — all warrant pre-deployment audit and a
Sepolia smoke-test in CI.
transaction gas limit too high (cap: 16777216) on SepoliaWrong (frontend write call without explicit gas):
await writeContractAsync({
address, abi, functionName: "deposit",
args: [encrypted, proof],
});
Why wrong: Sepolia enforces a per-tx gas cap of 2^24 = 16_777_216. wagmi
/ viem's default gas estimation for FHE-heavy calls (FHE.fromExternal,
confidentialTransferFrom, FHE.gt / FHE.select over euint64, multiple
FHE.allowThis) typically estimates ~5M actual but adds a buffer that pushes
tx.gas over the cap. The relayer rejects with
transaction gas limit too high (cap: 16777216, tx: <N>). Mock mode does not
enforce this cap, so the call passes locally.
Correct:
await writeContractAsync({
address, abi, functionName: "deposit",
args: [encrypted, proof],
gas: 15_000_000n, // below Sepolia's 2^24 cap
});
Verify: grep -rEn "useWriteContract|writeContractAsync\\(" frontend/ — every
call to a function whose ABI takes encrypted-input markers (bytes32/bytes)
must include gas: 15_000_000n. See fhevm-frontend/workflows/debug-errors.md
Phase 6 for the full diagnostic.
INVALID_ARGUMENT — invalid object key - customData on npx hardhat deploy --network sepoliaWrong (hardhat-deploy in the prescribed stack):
// hardhat.config.ts
import "hardhat-deploy";
// deploy/01-counter.ts
const { deployments, getNamedAccounts } = hre;
const { deployer } = await getNamedAccounts();
await deployments.deploy("Counter", { from: deployer });
// → fails on Sepolia broadcast: invalid object key - customData
Why wrong: hardhat-deploy@0.14.x (and 0.11.x) transitively depends on
@ethersproject/properties@5.x (ethers v5), which validates tx keys against a
hardcoded allowlist. @fhevm/hardhat-plugin@0.4.2 injects a customData key
on every tx (for relayer routing); the v5 validator rejects it. ethers v6
(via @nomicfoundation/hardhat-ethers) accepts customData cleanly.
Mock-mode "deployments" don't broadcast, so the conflict is invisible until
the first Sepolia attempt.
Correct:
// hardhat.config.ts — drop the hardhat-deploy import; npm uninstall hardhat-deploy
// (no `import "hardhat-deploy"` anywhere)
// scripts/deploy.ts
import hre from "hardhat";
async function main() {
const Counter = await hre.ethers.getContractFactory("Counter");
const counter = await Counter.deploy();
await counter.waitForDeployment();
console.log("Counter deployed to:", await counter.getAddress());
}
main().catch((e) => { console.error(e); process.exitCode = 1; });
Then deploy via npx hardhat run scripts/deploy.ts --network sepolia — the tx
routes through @nomicfoundation/hardhat-ethers (ethers v6) which accepts the
customData key.
Verify: npm ls hardhat-deploy returns (empty) after npm uninstall hardhat-deploy. grep -rn 'import "hardhat-deploy"' . — no hits. See fhevm-setup for the updated bootstrap sequence.
Invalid @zama-fhe/relayer-sdk version — exact-pin requirementWrong:
{
"devDependencies": {
"@zama-fhe/relayer-sdk": "^0.4.1"
}
}
Why wrong: @fhevm/hardhat-plugin@0.4.2 declares an exact-version peer
dependency on @zama-fhe/relayer-sdk@0.4.1 (verified in
node_modules/@fhevm/hardhat-plugin/package.json line 84:
"@zama-fhe/relayer-sdk": "0.4.1" — no caret). At runtime, the plugin imports
@zama-fhe/relayer-sdk/node and validates the resolved version; any other
version (including 0.4.2, currently the latest published) raises
Invalid @zama-fhe/relayer-sdk version. A caret pin lets npm resolve to
0.4.2 on a fresh install → broken Hardhat.
Correct:
{
"devDependencies": {
"@zama-fhe/relayer-sdk": "0.4.1"
}
}
Verify: npm ls @zama-fhe/relayer-sdk must show exactly 0.4.1 (not
0.4.2 or higher). When @fhevm/hardhat-plugin bumps and relaxes this peer
dep (or pins to a newer exact version), the project pin can move with it.
All three sub-modes share a property: the mock coprocessor on the hardhat
network does not exercise them. Catching them requires either (a) a Sepolia
smoke-test in CI or (b) eyes-on-Sepolia review before declaring a deployment
"working." The
"mock passes" signal is necessary but not sufficient for any
encrypted-write code path. Drop hardhat-deploy first — it removes 12b
entirely, which would otherwise block 12a from being observable (the tx never
broadcasts).
references/debug-checklist.md.fhevm-overview.hardhat-deploy) → fhevm-setup.customData reject, relayer-sdk exact pin).ACLNotAllowed(), EmptyDecryptionProof(), or DeclarationError at compile.fhevmjs.fhevm-contracts.fhevm-overview.fhevm-setup.fhevm-testing.fhevm-frontend.DeclarationError → references/v0.11-migration.md AP-1, AP-2, AP-5, AP-6.references/debug-checklist.md § ACL, § Mock KMS.fhevmjs → references/v0.11-migration.md AP-4.references/debug-checklist.md § Silent Transfer Returns Zero..env for secrets → AP-11.function foo() external view returns (euint64) → AP-10.FHE.requestDecryption → references/v0.11-migration.md AP-3 + v0.9 migration guide.transaction gas limit too high (cap: 16777216, tx: <N>) → AP-12a.INVALID_ARGUMENT — invalid object key - customData on hardhat deploy → AP-12b. Drop hardhat-deploy.Invalid @zama-fhe/relayer-sdk version → AP-12c. Pin exact 0.4.1.references/v0.11-migration.md, then references/debug-checklist.md.
See fhevm-overview for the canonical list. The OZ Confidential Contracts CHANGELOG is the most useful single page for the rename catalogue this skill covers.