Help us improve
Share bugs, ideas, or general feedback.
From bn-fhevm
Used when writing, refactoring, or reviewing FHEVM Solidity contracts that implement or extend ERC-7984 confidential tokens. Covers the IERC7984 interface (confidentialBalanceOf, confidentialTransfer, setOperator), FHESafeMath (tryIncrease, tryDecrease, tryAdd, trySub), the v0.4.0 extensions (Freezable, Restricted, Rwa, Omnibus, ObserverAccess, Votes, ERC7984ERC20Wrapper), the v0.4.0 finance/governance/utils/interfaces directories (VestingWalletConfidential, VotesConfidential, HandleAccessManager, IERC7984Rwa), the operator pattern that replaces approve/allowance, the euint64 supply ceiling and default-6-decimals trap, the v0.4 breaking change that switched unwrap request IDs from euint64 to bytes32, and the 2-step async-decrypt swap pattern for confidential AMMs (FHE.div plaintext-divisor constraint). Explicitly NOT for confidential NFTs — ERC-7984 is fungibles-only; ERC-721 + eaddress is the alternate route. Triggers on ERC7984, ERC-7984, IERC7984, confidentialBalanceOf, confidentialTransfer, setOperator, FHESafeMath, ConfidentialFungibleToken (legacy), ERC20Wrapper, ERC7984Restricted, ERC7984Rwa, ERC7984Freezable, ERC7984Omnibus, ERC7984ObserverAccess, ERC7984Votes, ERC7984Hooked, canTransact, IERC7984Rwa, forceConfidentialTransferFrom, confidentialFrozen, VestingWalletConfidential, BatcherConfidential, VotesConfidential, HandleAccessManager, confidential AMM, confidential DEX, euint64 overflow, decimals 6, or writing a confidential token / wrapper / RWA / governance token / AMM contract.
npx claudepluginhub bootnodedev/zama-s2-bounty-skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/bn-fhevm:fhevm-erc7984The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Write confidential ERC-7984 tokens with `@openzeppelin/confidential-contracts@0.4.0`. ERC-7984 has materially different semantics from ERC-20: balances are encrypted handles, transfers cap silently on insufficient balance instead of reverting, and the approve/allowance flow is replaced with a time-bounded operator pattern. This file is the routing core; ready-to-paste DeFi patterns live in [`re...
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.
Write confidential ERC-7984 tokens with @openzeppelin/confidential-contracts@0.4.0. ERC-7984 has materially different semantics from ERC-20: balances are encrypted handles, transfers cap silently on insufficient balance instead of reverting, and the approve/allowance flow is replaced with a time-bounded operator pattern. This file is the routing core; ready-to-paste DeFi patterns live in references/defi-blueprints.md (vault, lending, prize-pool, auction, governance, token, AMM).
euint64 — total supply ceiling and the default-6-decimals trapERC-7984 v0.4.0 stores all balances, allowances, totals, and transferred-amount return values as euint64. The arithmetic ceiling is 2^64 − 1 ≈ 1.844 × 10^19 raw units. Two consequences that bite agents who carry over ERC-20 mental models:
decimals() defaults to 6, not 18 (verified at token/ERC7984/ERC7984.sol:85-87). Override only when total_supply * 10^decimals < 2^64.1_000_000_000 * 10^18 = 10^27, which overflows euint64 and won't even compile as a Solidity literal.| Total supply | Decimals | Raw units | Fits in euint64? |
|---|---|---|---|
| 1,000,000,000 | 18 | 10^27 | NO — pick fewer decimals |
| 1,000,000,000 | 9 | 10^18 | yes — leaves ~18× headroom |
| 1,000,000,000 | 6 (default) | 10^15 | yes — comfortable |
| 18,000,000,000 | 9 | 1.8 × 10^19 | borderline — verify per-call math doesn't overflow on transfers |
If a user prompt names a "1 billion supply, 18 decimals" token, push back: pick a lower decimals (9 or default 6), not a higher supply. Silent overflow on FHE.add produces wrong balances with no revert.
ZamaEthereumConfig AND ERC7984 togetherSame rule as for any FHEVM contract (see fhevm-contracts Essential Principle 1) — the coprocessor wiring must be in place before any FHE.* call lands. ERC-7984 itself doesn't inherit ZamaEthereumConfig, so your concrete token contract carries it:
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {ERC7984} from "@openzeppelin/confidential-contracts/token/ERC7984/ERC7984.sol";
contract MyToken is ZamaEthereumConfig, ERC7984 {
constructor() ERC7984("MyToken", "MTK", "https://example.com/metadata.json") {}
}
ERC-20's approve(spender, amount) doesn't exist on ERC-7984 — there is no encrypted allowance. Instead, holders authorize an operator for a bounded time window:
function setOperator(address operator, uint48 until) external;
function isOperator(address holder, address spender) external view returns (bool);
until is a unix timestamp (uint48) at which authorization expires. While valid, the operator can call confidentialTransferFrom(holder, …) with no per-call amount cap. This is more permissive than approve — there is no encrypted spending limit — so operators must be trusted protocols (vaults, wrappers, routers), not arbitrary spenders. See fhevm-antipatterns for the v0.4 rename of isUserAllowed → canTransact on Restricted / Rwa, which is a separate access-control surface.
confidentialTransfer and confidentialTransferFrom return euint64 transferred — the actual amount moved, which equals min(requested, balance). They do not revert on insufficient balance; the caller has to decrypt the return value to know what happened. Code that assumes "if the call didn't revert, the full amount transferred" is wrong every time.
euint64 transferred = token.confidentialTransfer(to, amount);
// Later, off-chain or via a follow-up tx, decrypt `transferred`
// and compare to `amount` to detect partial transfers.
FHESafeMath is for explicit overflow/underflow detectionFHE.add / FHE.sub wrap silently on overflow/underflow (modular arithmetic over euint64). When you need an explicit success signal, use FHESafeMath:
import {FHESafeMath} from "@openzeppelin/confidential-contracts/utils/FHESafeMath.sol";
(ebool ok, euint64 newBalance) = FHESafeMath.tryIncrease(balance, delta);
// ok == FHE.asEbool(false) on overflow; newBalance is `balance` unchanged.
// On success, ok == true and newBalance == balance + delta.
| Function | When to use |
|---|---|
tryIncrease(oldValue, delta) | Adding to a stored balance/total; want to detect overflow |
tryDecrease(oldValue, delta) | Subtracting from a stored balance; want to detect underflow |
tryAdd(a, b) | Adding two operands; want to detect overflow |
trySub(a, b) | Subtracting one operand from another; want to detect underflow |
Note: an uninitialized euint64 (euint64.wrap(bytes32(0))) is treated as 0. All four functions return (ebool success, euint64 result); on failure, result is the unchanged left operand.
bytes32, not euint64ERC7984ERC20Wrapper.unwrap(...) returns a bytes32 request identifier. Pre-v0.4 versions identified unwrap batches by the euint64 unwrap amount; v0.4 switched to opaque bytes32 IDs that are stable across calls (per the v0.4.0 CHANGELOG):
Use a bytes32 unwrap request identifier instead of identifying batches by the euint64 unwrap amount.
Code that stores or compares euint64 "unwrap amounts" as request keys is pre-v0.4 and broken on the current pin. See fhevm-antipatterns for the broader v0.3.0 / v0.4.0 OZ rename history.
The v0.4.0 extensions are independent mixins. A typical compliant token might combine:
contract MyRwaToken is ZamaEthereumConfig, ERC7984Restricted, ERC7984Freezable, ERC7984Rwa {
/* ... */
}
Order matters for _update overrides — follow the standard Solidity-OZ linearization rule (most-derived first in the inheritance list).
IERC7984 — interface surfaceVerified against @openzeppelin/confidential-contracts@0.4.0 (contracts/interfaces/IERC7984.sol):
// State queries
function confidentialTotalSupply() external view returns (euint64);
function confidentialBalanceOf(address account) external view returns (euint64);
function isOperator(address holder, address spender) external view returns (bool);
// Operator management
function setOperator(address operator, uint48 until) external;
// Transfers — encrypted-input form (user-supplied ciphertext + ZK proof)
function confidentialTransfer(
address to,
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external returns (euint64 transferred);
// Transfers — handle form (re-using a handle this contract already has ACL on)
function confidentialTransfer(address to, euint64 amount) external returns (euint64 transferred);
// confidentialTransferFrom — same two overloads
function confidentialTransferFrom(
address from, address to, externalEuint64 encryptedAmount, bytes calldata inputProof
) external returns (euint64);
function confidentialTransferFrom(address from, address to, euint64 amount) external returns (euint64);
// confidentialTransferAndCall — fires a callback on `to` after transfer
function confidentialTransferAndCall(
address to, externalEuint64 encryptedAmount, bytes calldata inputProof, bytes calldata data
) external returns (euint64);
function confidentialTransferAndCall(
address to, euint64 amount, bytes calldata data
) external returns (euint64);
function confidentialTransferFromAndCall(/* … */) external returns (euint64); // 2 overloads
// Metadata
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8); // NOTE: default `6`, not 18 — see Essential Principle 0
function contractURI() external view returns (string memory); // NOTE: contractURI, not tokenURI (renamed in v0.3.0)
Returned euint64 transferred requires user decryption via fhevm-decryption if the caller needs the cleartext amount. The contract holds an automatic ACL grant on the return handle for one transaction — call FHE.allow(returned, callerAddress) if you need to surface it to the user.
All in contracts/token/ERC7984/extensions/. Each is a standalone mixin; combine via multiple inheritance.
| Extension | One-line purpose |
|---|---|
ERC7984ERC20Wrapper | Bridge between a plaintext ERC-20 and an encrypted ERC-7984 — wrap mints encrypted, unwrap decrypts via async oracle and returns a bytes32 request ID. v0.4 added ERC-165 detection, return values on wrap/unwrap, and overflow protection on wrap. |
ERC7984Freezable | Per-account freeze/unfreeze functionality — admin can pause transfers from specific holders without touching balances. |
ERC7984Restricted | Configurable transfer restrictions via canTransact(address) (renamed from isUserAllowed in v0.4.0) — blocklist / allowlist enforcement on every transfer. |
ERC7984Rwa | Real World Asset compliance — adds compliance checks, transfer controls, and enforcement actions over a base ERC7984Restricted. |
ERC7984Omnibus | Emits additional events for omnibus-account transfers, allowing off-chain accounting to disambiguate sub-account flows from a shared on-chain balance. |
ERC7984ObserverAccess | Lets designated observer addresses decrypt balances and transfer history for regulatory compliance, without granting transfer authority. |
ERC7984Votes | Encrypted voting power — analogous to OZ ERC20Votes but with confidential balances, suitable for confidential governance. |
ERC7984Hooked (in master since 2026-04-20 via PR #332; expected in next minor — not yet on the v0.4.0 npm package) | Calls registered modules' preTransfer and postTransfer hooks via _runPreTransferHooks / _runPostTransferHooks inside _update. To use today, install via git: npm i github:OpenZeppelin/openzeppelin-confidential-contracts#master. |
The "RWA stack" is the canonical compliant configuration: ERC7984 ← Restricted ← Freezable ← Rwa. Pick a subset for less regulated use cases.
IERC7984Rwa — full RWA surfaceERC7984Rwa implements IERC7984Rwa. The compliance API beyond plain IERC7984 is:
// State
function paused() external view returns (bool);
function canTransact(address account) external view returns (bool); // renamed from isUserAllowed in v0.4
function confidentialFrozen(address account) external view returns (euint64);
function confidentialAvailable(address account) external returns (euint64); // balance − frozen
// Pause / blocklist
function pause() external;
function unpause() external;
function blockUser(address account) external;
function unblockUser(address account) external;
// Freeze (encrypted-input + handle forms)
function setConfidentialFrozen(address, externalEuint64, bytes calldata) external;
function setConfidentialFrozen(address, euint64) external;
// Mint / burn
function confidentialMint(address, externalEuint64, bytes calldata) external returns (euint64);
function confidentialMint(address, euint64) external returns (euint64);
function confidentialBurn(address, externalEuint64, bytes calldata) external returns (euint64);
function confidentialBurn(address, euint64) external returns (euint64);
// Forced transfer (skips compliance checks — admin-only enforcement)
function forceConfidentialTransferFrom(address, address, externalEuint64, bytes calldata) external returns (euint64);
function forceConfidentialTransferFrom(address, address, euint64) external returns (euint64);
forceConfidentialTransferFrom is the regulator-grade lever — it can move tokens regardless of canTransact / freeze state. Audit access control on it carefully.
@openzeppelin/confidential-contracts@0.4.0 ships four directories beyond token/ERC7984/extensions/. Skill consumers that don't catalogue them tend to reinvent audited primitives — these are all production-ready in v0.4.0:
finance/| Contract | Purpose |
|---|---|
VestingWalletConfidential | Linear vesting of a confidential token to a single beneficiary; analogous to OZ VestingWallet over ERC-7984. Source: finance/VestingWalletConfidential.sol. |
VestingWalletCliffConfidential | Adds a cliff before vesting begins. Source: finance/VestingWalletCliffConfidential.sol. |
VestingWalletConfidentialFactory | Clone-factory deployer for vesting wallets — cheap per-beneficiary deployment via minimal proxies. Source: finance/VestingWalletConfidentialFactory.sol. |
BatcherConfidential | Multi-call helper for batching encrypted-input transactions. Useful for airdrops or merkle-claim flows over ERC-7984. Source: finance/BatcherConfidential.sol. |
ERC7821WithExecutor | Minimal ERC-7821 executor wrapper for confidential-context account abstraction. Source: finance/ERC7821WithExecutor.sol. |
governance/| Contract | Purpose |
|---|---|
governance/utils/VotesConfidential | Encrypted vote-tracking primitive that ERC7984Votes builds on. Use directly only if you need vote tracking detached from a token. Source: governance/utils/VotesConfidential.sol. |
utils/| Contract | Purpose |
|---|---|
FHESafeMath | Overflow-/underflow-detecting arithmetic on euint64. See Essential Principle 4. Source: utils/FHESafeMath.sol. |
HandleAccessManager | Helper for granting / revoking ACL on encrypted handles in batches. Use when a contract holds many handles whose access pattern changes over time. Source: utils/HandleAccessManager.sol. |
utils/structs/CheckpointsConfidential | Encrypted historical snapshots — analogous to OZ Checkpoints but storing euint64. Powers VotesConfidential. Source: utils/structs/CheckpointsConfidential.sol. |
interfaces/| Interface | Purpose |
|---|---|
IERC7984 | The primary interface — see the IERC7984 section above. |
IERC7984ERC20Wrapper | Wrapper-specific operations (wrap, unwrap). Source: interfaces/IERC7984ERC20Wrapper.sol. |
IERC7984Receiver | Callback surface for confidentialTransferAndCall recipients. Source: interfaces/IERC7984Receiver.sol. |
IERC7984Rwa | Full RWA surface — see the IERC7984Rwa section above. |
Quoting the v0.4.0 CHANGELOG:
Restricted / Rwa rename: "Rename isUserAllowed to canTransact" — contracts that override or call the old name fail to compile.For the full pre-v0.4 rename history (ConfidentialFungibleToken → ERC7984, TFHESafeMath → FHESafeMath, tokenURI → contractURI), see fhevm-antipatterns entries 5–8.
Ten ready-to-adapt DeFi patterns are in references/defi-blueprints.md:
| Pattern | Headline shape |
|---|---|
| Confidential ERC-4626 vault | encrypted shares + 2-step withdrawal via async decryption |
| Confidential lending pool | encrypted collateral + interest accrual + liquidation gate |
| Confidential prize pool | weighted random selection over encrypted deposits |
| Confidential auction (Dutch + sealed-bid + BlindAuction port) | port of Zama's BlindAuction.sol: eaddress hidden winner, per-bidder running totals, refund flow |
| Confidential governance | ERC7984Votes + threshold-decrypted tally |
| Confidential token recipes | dual-mint, dual-burn, ERC-20 wrap/unwrap, time-bounded operators, compliance |
| Confidential AMM (the structural problem) | 2-step async-decrypt swap; FHE.div only accepts plaintext divisors |
Batched Confidential Swap (BatcherConfidential) | the canonical answer to the encrypted-divisor problem: collect encrypted deposits → unwrap sum → plaintext route → publish plaintext rate → per-user claim() with FHE.div(*, plaintext) |
Plain ERC-20 ↔ ERC-7984 Wrapper (ERC7984ERC20Wrapper) | the only audited pattern for bridging USDC/DAI into FHEVM; bytes32 request-id unwrap, decimals reconciled via rate |
Confidential Vesting Wallets (VestingWalletConfidential) | linear / cliff vesting on ERC-7984; deterministic factory clones with fund-before-create flow; clone-init pitfall (must call FHE.setCoprocessor inside initialize) |
Canonical pins live in fhevm-overview. The ERC-7984-specific note is that @openzeppelin/confidential-contracts@0.4.0 MUST be pinned exact (no caret) — minor bumps in this package have shipped breaking renames.
fhevm-contracts — base FHE.* API, ACL lifecycle, control-flow patterns. Read first if you're new to encrypted Solidity.fhevm-antipatterns — v0.3.0 / v0.4.0 rename catalogue (ConfidentialFungibleToken → ERC7984, TFHESafeMath → FHESafeMath, tokenURI → contractURI, isUserAllowed → canTransact). Cross-reference before fixing legacy imports.fhevm-decryption — async decryption flows for revealing balances or unwrap-request status to users (@zama-fhe/sdk@3.0.0 sdk.publicDecrypt / sdk.userDecrypt).fhevm-overview — protocol-level mental model (coprocessor / KMS / gateway / relayer) for understanding why transfers can cap silently and why unwrap is async.Restricted, Freezable, Rwa, Omnibus, ObserverAccess, Votes).bytes32 unwrap IDs, canTransact).FHE.add and FHESafeMath.tryAdd / tryIncrease.ERC721 plus mapping(uint256 => eaddress) for hidden ownership; transfers expose the tokenId but the owner stays encrypted. See fhevm-antipatterns/references/debug-checklist.md entry 9 for the exact pattern. The ERC-7984 standard does NOT extend to non-fungibles — there is no ERC7984NFT companion.fhevm-contracts.ACLNotAllowed / EmptyDecryptionProof runtime errors → fhevm-antipatterns.fhevm-decryption.fhevm-testing.fhevm-static-analysis.approve?" → Essential Principle 2.FHESafeMath vs FHE.add?" → Essential Principle 4 + the FHESafeMath table.references/defi-blueprints.md.ConfidentialFungibleToken / tokenURI / isUserAllowed." → v0.4 breaking changes section + fhevm-antipatterns entries 5–8.euint64 and not matching?" → Essential Principle 5 (v0.4 switched to bytes32).references/defi-blueprints.md.euint64 ceiling and default-6 decimals).fhevm-antipatterns/references/debug-checklist.md entry 9.references/defi-blueprints.md (2-step async-decrypt swap; FHE.div plaintext-divisor constraint).BlindAuction." → Blueprint 4 in references/defi-blueprints.md (eaddress hidden winner, per-bidder running totals, two-step reveal).BatcherConfidential pattern: encrypted deposits → plaintext route → plaintext exchange rate → per-user claim with FHE.div(*, plaintext)).ERC7984ERC20Wrapper: rate-based decimals reconciliation, bytes32 request-id unwrap, one-tx wrap via transferAndCall).VestingWalletConfidential + Cliff + Factory; deterministic-clone fund-before-create flow; clone-init pitfall).references/defi-blueprints.md — IConfidentialTokenWrappersRegistry lookup with the deployed Sepolia / mainnet addresses and the _requireCanonicalWrapper gate function.
IERC7984 source — https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/blob/v0.4.0/contracts/interfaces/IERC7984.solFHESafeMath source — https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/blob/v0.4.0/contracts/utils/FHESafeMath.sol