fhevm-encrypted-inputs

Installation
SKILL.md

FHE Encrypted Inputs

Use this skill when building or reviewing the path encrypted data takes from a user's browser into an onchain contract. This is the entry point for all user-supplied confidential values in FHEVM, and the proof binding is what prevents replay and injection attacks.

When To Use

  • Implementing a contract function that accepts encrypted user input
  • Building frontend code that encrypts values before submitting transactions
  • Reviewing input validation and proof verification in contract entry points
  • Debugging "invalid proof" or "wrong sender" reverts on encrypted input submission
  • Designing contract interfaces that accept both external and onchain encrypted values

Core Mental Model

When a user input must stay confidential, the user does not send plaintext to the chain. They encrypt the value client-side using the Zama SDK, producing a ciphertext and a cryptographic proof. The proof binds the ciphertext to a specific sender address AND a specific contract address. The contract calls FHE.fromExternal to import the ciphertext, which verifies the proof and returns an onchain encrypted handle.

Two completely different types represent encrypted values at different lifecycle stages:

  • externalEuint64 + inputProof: encrypted client-side, not yet imported onchain
  • euint64: an onchain handle already living in the coprocessor

These are not interchangeable. A contract that accepts fresh confidential user input needs the first. A contract that receives an existing onchain handle from storage, another contract, or a user who already controls that handle needs the second.

Hard Constraints

  1. FHE.fromExternal(ciphertext, inputProof) is the only way to import user-encrypted values.
  2. The inputProof is cryptographically bound to msg.sender AND the target contract address.
  3. If the proof was generated for a different sender, the call reverts.
  4. If the proof was generated for a different contract, the call reverts.
  5. The handle returned by FHE.fromExternal is immediately usable by the current contract in the same transaction. If the handle must persist beyond the current transaction or be shared, grant ACL explicitly.
  6. Proofs are bound to the caller and contract context. Reusing a proof under a different sender or contract address reverts; the local refs do not support a blanket single-use rule across transactions.
  7. The contract must inherit a FHEVM config (ZamaEthereumConfig from @fhevm/solidity/config/ZamaConfig.sol, or the network-specific equivalent). Without it, FHE.fromExternal has no coprocessor/ACL/KMS addresses wired and calls revert at runtime.

Contract-Side: Accepting Encrypted Input

The enclosing contract must inherit a FHEVM config so the FHE.* calls know the coprocessor, ACL, and KMS addresses:

import { FHE, externalEuint64, euint64 } from "@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";

contract ConfidentialToken is ZamaEthereumConfig { /* ... */ }

Inside the contract, accept the external ciphertext and its proof, then import:

function transfer(address to, externalEuint64 encryptedAmount, bytes calldata inputProof) external {
    // Import the user's encrypted value — verifies proof binding
    euint64 amount = FHE.fromExternal(encryptedAmount, inputProof);

    // Use it in FHE operations
    ebool hasEnough = FHE.ge(_balances[msg.sender], amount);
    euint64 actualAmount = FHE.select(hasEnough, amount, FHE.asEuint64(0));

    _balances[msg.sender] = FHE.sub(_balances[msg.sender], actualAmount);
    FHE.allowThis(_balances[msg.sender]);
    FHE.allow(_balances[msg.sender], msg.sender);

    _balances[to] = FHE.add(_balances[to], actualAmount);
    FHE.allowThis(_balances[to]);
    FHE.allow(_balances[to], to);
}

Frontend-Side: Encrypting Values

Using the Zama React SDK (@zama-fhe/react-sdk):

import { useEncrypt } from "@zama-fhe/react-sdk";

const encrypt = useEncrypt();

// EncryptParams: { values: EncryptInput[], contractAddress, userAddress }
// EncryptResult: { handles: Uint8Array[], inputProof: Uint8Array }
const encrypted = await encrypt.mutateAsync({
  values: [{ value: amount, type: "euint64" }],
  contractAddress: tokenContractAddress,
  userAddress: account.address,
});

// Each value in `values` produces one entry in `handles`, in the same order.
// `inputProof` is shared across the whole batch.
const encryptedAmount = encrypted.handles[0];
const inputProof = encrypted.inputProof;

// Submit the transaction. Most viem/wagmi pipelines accept Uint8Array for
// `bytes` / `bytes32` parameters directly; convert with `toHex` if your
// toolchain needs a hex string.
await writeContract({
  address: tokenContractAddress,
  abi: tokenAbi,
  functionName: "confidentialTransfer",
  args: [recipientAddress, encryptedAmount, inputProof],
});

The contractAddress and userAddress parameters are critical. They are baked into the proof. If either is wrong, the onchain FHE.fromExternal call reverts.

Two Input Types: When To Use Which

Scenario Parameter Type Why
User sends encrypted amount from frontend externalEuint64 + inputProof Value originates off-chain, needs proof
Contract A passes encrypted value to Contract B euint64 Handle already exists onchain, no proof needed
User passes an existing onchain handle they already control euint64 Value already exists onchain; verify FHE.isSenderAllowed
User wraps public tokens into confidential plaintext amount Wrap amount is public ERC20 input, not a confidential user input
Internal rebalancing between contract storage slots euint64 Contract operates on its own stored handles

Use externalEuint64 + inputProof when the value originates off-chain in this call. Use bare euint64 when the caller is expected to pass an existing onchain handle, and verify sender access with FHE.isSenderAllowed when that handle comes from an untrusted caller.

Common Input Types

Type External Type Bits Plaintext Equivalent Typical Use Case
ebool externalEbool 2 bool Encrypted flags
euint8 externalEuint8 8 uint8 Small counters, percentages
euint16 externalEuint16 16 uint16 Low-range amounts
euint32 externalEuint32 32 uint32 Medium values
euint64 externalEuint64 64 uint64 Token balances, standard amounts
euint128 externalEuint128 128 uint128 Large amounts, high-precision intermediates
eaddress externalEaddress 160 address Encrypted addresses
euint256 externalEuint256 256 uint256 Full-width opaque integers

These are the input types currently documented in the official Zama guides and examples. euint64 is the most commonly used type for token balances and transfer amounts in ERC7984 flows.

Multiple Encrypted Inputs

When multiple encrypted values are produced in the same frontend encryption call, they share a single inputProof:

function bid(
    externalEuint64 encryptedPrice,
    externalEuint64 encryptedQuantity,
    bytes calldata inputProof
) external {
    euint64 price = FHE.fromExternal(encryptedPrice, inputProof);
    euint64 quantity = FHE.fromExternal(encryptedQuantity, inputProof);
    // ...
}

Frontend encrypts multiple values in one call:

const encrypted = await encrypt.mutateAsync({
  values: [
    { value: price, type: "euint64" },
    { value: quantity, type: "euint64" },
  ],
  contractAddress,
  userAddress,
});
// encrypted.handles[0] is price, encrypted.handles[1] is quantity
// encrypted.inputProof covers both

Anti-Patterns

Anti-Pattern 1: Accept Plaintext Then Encrypt On-Chain

Writing a function that takes uint256 amount and calls FHE.asEuint64(amount). This puts the plaintext onchain in the transaction calldata, destroying confidentiality.

Anti-Pattern 2: Reuse Proofs Across Contracts

Generating a proof for contract A and submitting it to contract B. The proof is bound to a specific contract address. It will revert.

Anti-Pattern 3: Skip ACL When The Imported Handle Persists

Calling FHE.fromExternal and then storing or forwarding the imported handle without granting the required ACL. Same-transaction use works, but future reuse or downstream access breaks.

Anti-Pattern 4: Accept externalEuint64 for Contract-to-Contract Calls

Using the external input type when the caller is always another contract. This forces unnecessary proof generation and adds complexity. Use euint64 for onchain handle passing.

Review Checklist

  • If a function accepts fresh confidential user input, does it use externalE* + inputProof rather than plaintext or a bare onchain handle?
  • If a FHE.fromExternal result is stored or shared, does it get the required ACL grant (allowThis, allow, or allowTransient)?
  • Does the frontend encrypt with the correct contractAddress and userAddress?
  • If a user passes an existing onchain handle, does the contract verify FHE.isSenderAllowed?
  • Are contract-to-contract calls using euint64 instead of external types?
  • Is there any function that accepts plaintext and encrypts it onchain?
  • If multiple values come from one encryption call, are the handles paired with that single inputProof in the same order?

Output Expectations

When applying this skill, structure analysis around:

  1. where the encryption boundary sits (client vs chain)
  2. which functions are user-facing vs contract-facing
  3. whether proof binding matches the actual caller and target
  4. whether persistent ACL is granted when an imported handle is stored or shared

Related Skills

  • skills/fhevm-acl-lifecycle/SKILL.md — when imported handles need persistent or delegated ACL grants
  • skills/fhevm-frontend-integration/SKILL.md — client-side encryption, proof generation, address binding
  • skills/fhevm-cross-contract/SKILL.md — when to take externalEuint64 vs bare euint64
Related skills
Installs
10
First Seen
Apr 14, 2026