---
name: makabeez/fhevm-confidential-contracts
source: https://app.decimal.ai/s/makabeez-fhevm-confidential-contracts@1/SKILL.md
source_sha256: 782e5b8d394e
---

# FHEVM Confidential Smart Contract Skill

You are an expert Zama Protocol / FHEVM developer. When asked to build a confidential smart
contract or FHEVM application, follow this skill precisely.

## 1 — Project Setup

### 1.1 Scaffold a Hardhat + FHEVM project

```bash
mkdir 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"
```

### 1.2 hardhat.config.ts (required boilerplate)

```typescript
import "@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;
```

### 1.3 .env

```
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_KEY
PRIVATE_KEY=0xabc...
```

---

## 2 — Encrypted Types Reference

| 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`

---

## 3 — Canonical Contract Template

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;
    }
}
```

---

## 4 — FHE Operations Cheatsheet

### 4.1 Arithmetic
```solidity
euint64 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);
```

### 4.2 Bitwise
```solidity
euint64 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
```

### 4.3 Comparison — always return `ebool`
```solidity
ebool 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);
```

### 4.4 Conditional (encrypted ternary — never use plaintext if/else on encrypted values)
```solidity
// FHE.select(condition: ebool, ifTrue: T, ifFalse: T) → T
euint64 result  = FHE.select(condition, valueA, valueB);
eaddress winner = FHE.select(isHigher, candidateA, candidateB);
```

### 4.5 Type Casting
```solidity
euint32 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);
```

### 4.6 Random numbers (on-chain entropy)
```solidity
euint64 rand64 = FHE.randEuint64();
euint8  rand8  = FHE.randEuint8();
// ⚠ randEuint is non-deterministic — do NOT use in view functions
```

---

## 5 — Access Control List (ACL) — The #1 Source of Bugs

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.

```solidity
FHE.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)
```

### ACL Rules
1. **After `FHE.fromExternal`** — call `FHE.allowThis(handle)` immediately.
2. **Before storing** — the contract must `allowThis` OR it cannot read the value later.
3. **For user reads** — call `FHE.allow(handle, user)` or they get Access Denied on decrypt.
4. **After `FHE.add` / any operation** — the result is a NEW handle — re-grant ACL.
5. **Never share a handle without ACL** — re-grant to the recipient.

### ACL Propagation Pattern (token transfer)
```solidity
function 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;
}
```

---

## 6 — Encrypted Inputs: Frontend → Contract

### 6.1 TypeScript (Hardhat test / frontend)
```typescript
import { 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);
```

### 6.2 Browser (React / vanilla JS)
```typescript
import { 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);
```

### 6.3 Reading encrypted values (frontend decrypt)
```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());
```

---

## 7 — Async Decryption Deep Dive

Decryption is **always asynchronous**. Pattern:

```
Contract → requestDecryption() → emit EventDecryption
                                  ↓
                            Relayer picks up event
                                  ↓
                            KMS decrypts
                                  ↓
                            Gateway calls callback on contract
```

### 7.1 Multi-value decryption
```solidity
function 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;
}
```

### 7.2 Callback signature rules
- Must be `external`
- Must have `onlyGateway` modifier (provided by config inheritance)
- Parameters: `(uint256 requestId, bytes memory cleartexts, bytes memory decryptionProof)`
- Must return `bool`

---

## 8 — Testing

### 8.1 Mock FHE (no testnet — fast local dev)
```typescript
// 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);
  });
});
```

### 8.2 Run tests
```bash
npx hardhat test                         # mock mode — runs offline
npx hardhat test --network sepolia       # real testnet (slower)
```

---

## 9 — Deployment

### 9.1 Deploy script
```typescript
import { 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);
```

```bash
npx hardhat run scripts/deploy.ts --network sepolia
```

### 9.2 Network configs
| 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    |

---

## 10 — Anti-Patterns (NEVER DO THESE)

The following patterns cause silent failures, security holes, or wasted gas:

### ❌ AP-001: Using `if` on encrypted values
```solidity
// 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);
```

### ❌ AP-002: Storing `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;
```

### ❌ AP-003: Missing `FHE.allowThis` before storing
```solidity
// 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;
```

### ❌ AP-004: ACL not propagated after arithmetic
```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);
```

### ❌ AP-005: Synchronous decryption (deprecated)
```solidity
// WRONG — TFHE.decrypt() was removed in v0.5+
uint64 plain = TFHE.decrypt(encryptedVal);

// CORRECT — use async requestDecryption + callback
```

### ❌ AP-006: Reusing inputProof across transactions
```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
```

### ❌ AP-007: `FHE.div` / `FHE.rem` with encrypted divisor
```solidity
// 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);
```

### ❌ AP-008: Using `euint256` for token balances
```solidity
// 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;
```

### ❌ AP-009: Not guarding against `requestDecryption` replay
```solidity
// 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
```

### ❌ AP-010: `FHE.allowForDecryption` in transfer logic
```solidity
// 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)
```

### ❌ AP-011: Emitting plaintext from encrypted comparisons
```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)
```

### ❌ AP-012: Forgetting `onlyGateway` on decryption callback
```solidity
// 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) { ... }
```

---

## 11 — Gas Optimization Guide

| 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.

---

## 12 — Complete dApp: Confidential ERC-20

### Contract (`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];
    }
}
```

### Frontend snippet (`src/App.tsx`)
```typescript
import { 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();
}
```

---

## 13 — Deployment Checklist

Before submitting to mainnet:

- [ ] All `FHE.fromExternal` calls have `inputProof` in the same tx
- [ ] Every encrypted result has `FHE.allowThis` before storage
- [ ] All user-accessible handles have `FHE.allow(handle, user)`
- [ ] No `if/else` branching on encrypted values (use `FHE.select`)
- [ ] No `euint256` where `euint64` is sufficient
- [ ] No `TFHE.decrypt` (deprecated) — async callback pattern used
- [ ] Decryption callback has `onlyGateway` modifier
- [ ] `_decryptionPending` guard in place
- [ ] `inputProof` never reused across transactions
- [ ] Inherits from correct network config (`SepoliaConfig` / `ZamaEthereumConfig`)
- [ ] All tests pass with `fhevm: { mock: true }` before testnet deployment
- [ ] Run `npx hardhat fhevm-lint` to catch anti-patterns automatically

---

## 14 — Validation

Before deployment, run the companion linter to catch all anti-patterns:

```bash
node scripts/fhevm-lint.js contracts/
```

Expected output for a clean contract: `✅ 0 issues found.`