fhevm-forge 0.1.1

Foundry scaffold, deployer, gas estimator and linter for Zama FHEVM
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# AGENT.md — FHEVM Development Guide for AI Coding Agents

> This file is embedded in every project generated by `fhevm-forge`.
> Read it completely before writing any code. It encodes hard-won knowledge
> from production FHEVM development including bugs that caused silent failures.

---

## What This Project Is

A confidential smart contract application built on Zama FHEVM (Fully Homomorphic
Encryption Virtual Machine). Contracts are written in Solidity using TFHE encrypted
types. The frontend and agents use `@zama-fhe/relayer-sdk` to encrypt inputs and
decrypt outputs without dealing with the Gateway chain directly.

---

## Architecture: Two Chains, One Abstraction

FHEVM has a two-chain architecture under the hood:

**Host chain** (Sepolia testnet, chain ID 11155111)
- Where your smart contracts live
- Where users hold wallets and submit transactions
- Encrypted state is stored here as opaque ciphertext handles

**Gateway chain** (chain ID 55815)
- Handles FHE key management and decryption
- Runs the KMS (Key Management Service) via threshold MPC
- Developers NEVER interact with this directly

**The Relayer SDK** (`@zama-fhe/relayer-sdk`) abstracts the Gateway chain entirely.
Your code only ever needs a wallet on the host chain. All Gateway interactions
happen through HTTP via the Relayer.

---

## File Map

```
lib/fhevm/
  instance.ts     Singleton SDK instance, environment-aware initialization
  encrypt.ts      All encryption flows (uint8 → uint256, bool, address, batch)
  decrypt.ts      publicDecrypt (contract-initiated) + reencrypt (user-initiated)
  gateway.ts      Callback polling + resolver helpers (resolveHealthCheck, etc.)
  errors.ts       FHE error types with actionable messages
  config.ts       Chain address registry (Sepolia, mainnet, Base, Arbitrum)
  index.ts        Re-exports everything — import from here in app code

lib/hooks/
  useEncrypt.ts        React hook: encrypt inputs before submitting transactions
  useReencrypt.ts      React hook: reveal own encrypted values with wallet signature
  useHealthCheck.ts    React hook: full health check lifecycle (request → resolve)

agent/lib/
  fhevm-agent.ts  Headless agent runtime — no browser/MetaMask required

src/
  *.sol           Solidity contracts using TFHE encrypted types

test/
  *.t.sol         Forge tests using forge-fhevm (runs FHE locally, no Gateway)

script/
  Deploy*.s.sol   Forge deployment scripts (reads chain config from env vars)

abi/
  *.ts            Auto-generated from forge build — never hand-edit
```

---

## SDK Entry Point — Always Use the Singleton

```typescript
import { getFhevmInstance } from "@/lib/fhevm/instance";

// ✅ Correct — singleton, only initializes once per chain
const fhe = await getFhevmInstance("sepolia");

// ❌ Never do this in application code
import { createInstance } from "@zama-fhe/relayer-sdk";
const fhe = await createInstance({ ... }); // creates a new instance every time
```

If the user switches network in their wallet, call `resetFhevmInstance()` then
`getFhevmInstance()` again.

---

## Encryption

Use the helpers in `lib/fhevm/encrypt.ts`. Never construct `createEncryptedInput()`
manually in application code.

```typescript
import { encryptUint64, encryptBatch } from "@/lib/fhevm";

// Single value
const { handles, inputProof } = await encryptUint64(
  2000_000000n,     // $2000 USDC (6 decimal places)
  vaultAddress,     // contract that will receive this
  userAddress       // wallet submitting the transaction
);
// handles[0] is the encrypted handle — pass to the contract
// inputProof proves the ciphertext is well-formed — pass alongside handles[0]

// Multiple values — one SDK call, one inputProof covers all
const { handles, inputProof } = await encryptBatch([
  { type: "uint64", value: collateralGwei },
  { type: "uint64", value: borrowAmountUsdc },
], vaultAddress, userAddress);
// handles[0] = collateral, handles[1] = borrow amount
// Do NOT call encrypt twice and get two inputProofs
```

**RULE:** The `inputProof` is bound to exactly one `contractAddress` + `userAddress` pair.
Never reuse a proof across different contracts. (Lint rule: FHEVM-005)

---

## Submitting Encrypted Transactions

```typescript
// Pass handle as first arg, inputProof as second arg — always in this order
const tx = await vault.borrow(
  handles[0],    // einput encryptedBorrowAmount
  inputProof,    // bytes calldata inputProof
  { value: ethers.parseEther("1.5") }
);
await tx.wait();
```

The Solidity contract receives `einput encryptedAmount` and `bytes calldata inputProof`,
then calls `TFHE.asEuint64(encryptedAmount, inputProof)` to create the encrypted value.

---

## Public Decryption

Public decryption is for values the CONTRACT wants to reveal globally.
Examples: health factor check results, auction settlement prices, vote tallies.

```typescript
import { publicDecrypt } from "@/lib/fhevm";

// ✅ CORRECT — publicDecrypt lives directly on FhevmInstance (via the lib wrapper)
const { abiEncodedClearValues, decryptionProof, clearValues } =
  await publicDecrypt([handle]);

// ❌ WRONG — getRelayer() does not exist on FhevmInstance (FHEVM-007)
const result = await fhe.getRelayer().publicDecrypt([handle]);
```

After getting the result, the contract resolver needs **3 arguments**, not 1:

```typescript
// ✅ Pass all 3 — borrower + both SDK return values (FHEVM-009 prevention)
await vault.resolveHealthCheck(
  borrower,                     // arg 1: identifying key
  result.abiEncodedClearValues, // arg 2: from publicDecrypt — contract verifies this
  result.decryptionProof        // arg 3: from publicDecrypt — contract verifies this
);

// ❌ WRONG — contract requires 3 args, this will revert
await vault.resolveHealthCheck(borrower);
```

---

## User Reencryption

Reencryption lets a wallet holder read their own encrypted value.
Used for portfolio views, balance displays, personal position data.

```typescript
import { reencryptBatch } from "@/lib/fhevm";

// Read own collateral + debt (user must sign EIP-712 message)
const [collateral, debt] = await reencryptBatch(
  [collateralHandle, debtHandle], // handles from getPositionHandles()
  vaultAddress,
  userAddress,
  signer                          // ethers signer — prompts wallet signature
);

// collateral is in gwei units: (Number(collateral) / 1e9).toFixed(4) + " ETH"
// debt is in USDC units:       "$" + (Number(debt) / 1e6).toFixed(2)
```

The plaintext never travels over the network. Only a re-encrypted ciphertext
under an ephemeral keypair, which is decrypted locally. Users will see a
MetaMask signature prompt — explain this in the UI.

---

## Contract Getter Rules

**RULE: Each concern gets its own getter function. Never bundle unrelated handles.**

```solidity
// ✅ Correct — 2 getters, each with a single responsibility
function getPositionHandles(address borrower)
    external view returns (uint256 collateralHandle, uint256 debtHandle)

function getPendingHealthHandle(address borrower)
    external view returns (bytes32)   // 0 if no check pending

function hasPendingCheck(address borrower)
    external view returns (bool)

function getLoanInfo(address borrower)
    external view returns (bool active, uint256 openedAt)

// ❌ Wrong — bundled tuple causes off-by-one errors in TypeScript
function getPositionHandles(address borrower)
    external view returns (uint256, uint256, uint256) // never do this
```

In TypeScript, always use named destructuring, never positional index access:

```typescript
// ✅ Named destructuring — breaks at compile time if field is missing
const { collateralHandle, debtHandle } = await vault.getPositionHandles(borrower);

// ✅ Separate calls for separate concerns
const healthHandle = await vault.getPendingHealthHandle(borrower);
const isPending    = await vault.hasPendingCheck(borrower);
const { active, openedAt } = await vault.getLoanInfo(borrower);

// ❌ Index access — breaks silently if getter return order changes (FHEVM-008)
const handles = await vault.getPositionHandles(borrower);
const health  = handles[2]; // always undefined — only 2 values returned
```

---

## Solidity Rules for TFHE.allow()

Every `euint*` you create **must** have access explicitly granted.
Forgetting this is the single most common silent failure in FHEVM.
There is no revert — the handle just becomes permanently unusable.

```solidity
// ✅ After creating or reassigning any euint — always call both
euint64 amount = TFHE.asEuint64(encryptedInput, inputProof);
TFHE.allowThis(amount);           // contract can use handle in future txs
TFHE.allow(amount, msg.sender);   // sender can reencrypt and view it

// ✅ When storing in a struct
position.debt = newDebt;
TFHE.allowThis(newDebt);          // still required even when stored in struct
TFHE.allow(newDebt, borrower);

// ✅ When passing to an external contract
TFHE.allow(amount, address(debtToken));  // grant before calling external contract
debtToken.mintDebt(msg.sender, amount);

// ❌ Silent failure — no revert, handle is just permanently unreadable
euint64 amount = TFHE.asEuint64(encryptedInput, inputProof);
positions[msg.sender].debt = amount;  // missing allowThis
```

---

## FHE Operations Cannot Be `view` or `pure`

TFHE operations write to internal FHE state registers. They cannot be `view`.

```solidity
// ❌ Will behave incorrectly — remove view
function isUndercollateralized(address b) public view returns (ebool)

// ✅ Correct — no view modifier
function isUndercollateralized(address b) public returns (ebool)
```

This applies to any function calling: `TFHE.add`, `TFHE.sub`, `TFHE.mul`,
`TFHE.div`, `TFHE.lt`, `TFHE.le`, `TFHE.gt`, `TFHE.ge`, `TFHE.eq`,
`TFHE.select`, `TFHE.and`, `TFHE.or`, `TFHE.not`, `TFHE.asEuint*`.

---

## Gateway Callbacks — Always Use `onlyGateway`

When a contract calls `Gateway.requestDecryption()`, the Zama Gateway fires a
callback transaction 2-5 seconds later (on Sepolia testnet).

The callback function **must** have the `onlyGateway` modifier. Without it,
any address can call the function and inject fake decryption results.

```solidity
// ✅ Protected callback
function _onHealthCheckDecrypted(uint256 requestId, bool result)
    external onlyGateway
{
    // This function can only be called by the Zama Gateway
}

// ❌ Unprotected — any address can call this with arbitrary results
function _onHealthCheckDecrypted(uint256 requestId, bool result)
    external
{
    // CRITICAL VULNERABILITY — remove this function or add onlyGateway
}
```

To use `onlyGateway`, inherit from `GatewayCallbackReceiver`:
```solidity
import "@zama-ai/fhevm/contracts/gateway/GatewayInterface.sol";
contract MyContract is GatewayCallbackReceiver { ... }
```

---

## Gas Costs — FHE Is Expensive

FHE operations cost 5-20x more than equivalent plaintext EVM operations.
The dominant cost is the **coprocessor gas** (off-chain FHE compute), not on-chain gas.

| Operation | On-Chain Gas | Coprocessor Gas | Notes |
|-----------|-------------|-----------------|-------|
| TFHE.add/sub | 8,000 | 65,000 | ~8x plaintext |
| TFHE.mul | 15,000 | 150,000 | ~15x plaintext |
| TFHE.div | 30,000 | 400,000 | Avoid in hot paths |
| TFHE.lt/le/gt/ge/eq | 10,000 | 70,000 | Comparisons are expensive |
| TFHE.select | 12,000 | 90,000 | FHE ternary |
| TFHE.asEuint64 | 6,000 | 50,000 | Per encrypted input |
| TFHE.allow/allowThis | 3,000 | 0 | Cheap — always call these |
| Gateway.requestDecryption | 25,000 | 200,000 | Async, ~2-5 sec latency |

**Optimization tips:**
- Batch multiple TFHE operations in a single function rather than separate transactions
- Use `encryptBatch()` instead of multiple `encryptValue()` calls — one SDK round-trip
- Prefer `euint64` over `euint128`/`euint256` where values fit — cheaper operations
- Cache frequently-used public values as calldata rather than re-reading from storage

Run `fhevm-forge gas` to see a per-operation cost breakdown for your contracts.

---

## Agent Runtime (No Browser Required)

For monitor agents, bidder agents, and server-side scripts, use `FhevmAgent`
from `agent/lib/fhevm-agent.ts`. It wraps the Relayer SDK for headless use.

```typescript
import { FhevmAgent } from "./lib/fhevm-agent";

const agent = new FhevmAgent(
  process.env.SEPOLIA_RPC_URL!,
  process.env.AGENT_PRIVATE_KEY!,
  "sepolia"
);

// Encrypt a bid price
const { handle, inputProof } = await agent.encryptUint64(
  currentAuctionPrice,
  auctionContract.address
);
await auctionContract.connect(agent.wallet).submitBid(handle, inputProof);

// Run full 3-step health check resolve (get handle → publicDecrypt → resolve with 3 args)
const { isUndercollateralized } = await agent.resolveHealthCheck(vault, borrower);
```

Never use browser SDK patterns (window.ethereum, MetaMask prompts) in agent code.

---

## ABI Files

ABIs live in `abi/`. They are auto-generated from `forge build`.

```bash
# Regenerate after changing Solidity contracts
forge build
# Then run the ABI extraction script (if generated by fhevm-forge)
pnpm run abi:generate
```

**Never hand-edit ABI files.** If a contract function is missing from the ABI:
1. Add the function to the Solidity contract
2. Run `forge build`
3. Regenerate the ABI TypeScript file

The ABI must include all four critical getter functions:
- `getLoanInfo`
- `getPositionHandles`
- `hasPendingCheck`
- `getPendingHealthHandle`

---

## Running Tests

```bash
# Solidity tests — runs locally with forge-fhevm mock (no Gateway needed)
forge test

# Run a specific test
forge test --match-test test_borrow_opens_position -vvv

# TypeScript type check
pnpm typecheck

# FHE gas report
fhevm-forge gas

# Static analysis
fhevm-forge lint ./src/
```

In Forge tests, `FHEVMTestBase.setUp()` deploys all FHEVM host contracts
at deterministic local addresses using `setCode`/`setStorageAt`. The
`encryptUint64(value, contractAddress, userAddress)` helper encrypts values
for test use, and `decryptUint64(handle)` decrypts them — both only work in tests.

---

## Deploying

```bash
# Copy .env.example to .env and fill in values
cp .env.example .env

# Deploy to Sepolia (testnet)
fhevm-forge deploy --chains sepolia --contract ConfidentialVault

# Deploy to multiple chains simultaneously
fhevm-forge deploy --chains sepolia,base --contract ConfidentialVault

# Dry run (simulate without broadcasting)
fhevm-forge deploy --chains sepolia --contract ConfidentialVault --dry-run
```

Deployment manifests are written to `deployments/<ContractName>.json`.
Each chain entry includes the contract address, transaction hash, and explorer URL.

---

## Common Errors and Fixes

| Error Message | Cause | Fix |
|---------------|-------|-----|
| `getRelayer is not a function` | Called `fhe.getRelayer()` | Use `fhe.publicDecrypt()` directly (FHEVM-007) |
| `handles[N]` is `undefined` | Accessed index beyond getter return count | Use named destructuring; health handle is separate (FHEVM-008) |
| Contract reverts on resolve | Resolver called with 1 arg | Pass `borrower + abiEncodedClearValues + decryptionProof` (FHEVM-009) |
| `info[5]` undefined | Assumed field at index 5 of 5-element tuple | Use named returns; `hasPendingCheck()` is a separate call |
| State silently corrupted | Missing `TFHE.allowThis()` | Call `TFHE.allowThis()` after every `euint` assignment (FHEVM-001) |
| External contract can't read handle | Missing `TFHE.allow()` | Call `TFHE.allow(handle, externalContract)` before passing (FHEVM-002) |
| Function reverts unexpectedly | FHE op in `view` function | Remove `view` modifier — FHE ops modify state (FHEVM-003) |
| Gateway callback callable by anyone | Missing `onlyGateway` | Add `onlyGateway` to all `_on*Decrypted` callbacks (FHEVM-006) |
| `invalid handle` from SDK | Handle is 0 or uninitialized | Verify `TFHE.allowThis()` was called; check position is active |
| `ACL not authorized` | Address not in TFHE permission set | Call `TFHE.allow(handle, address)` in contract before reading |
| Tests fail locally but pass on Sepolia | `evm_version` not `cancun` | Set `evm_version = "cancun"` in `foundry.toml` |
| Gateway timeout in tests | Tests using real Gateway | Use `forge test` with `FHEVMTestBase` — no real Gateway needed |

---

## Environment Variables

```bash
# Required for deployment
SEPOLIA_RPC_URL=              # Alchemy/Infura Sepolia endpoint
DEPLOYER_PRIVATE_KEY=         # Wallet deploying contracts (no 0x prefix)

# Required for agent operation
MONITOR_AGENT_PRIVATE_KEY=    # Separate key for monitor agent
BIDDER_AGENT_PRIVATE_KEY=     # Separate key for bidder agent

# Required for contract verification
ETHERSCAN_API_KEY=            # From etherscan.io account

# Optional (for multi-chain deployment)
MAINNET_RPC_URL=
BASE_RPC_URL=
ARBITRUM_RPC_URL=
BASESCAN_API_KEY=
ARBISCAN_API_KEY=
```

**Security:** Never commit `.env` to git. It is in `.gitignore` by default.
Use separate private keys for deployer, monitor agent, and bidder agent.

---

## Zama FHEVM Resources

- Documentation: https://docs.zama.ai/fhevm
- Relayer SDK: https://github.com/zama-ai/relayer-sdk
- forge-fhevm: https://github.com/zama-ai/forge-fhevm
- Community: https://community.zama.org
- Discord: https://discord.gg/zama

---

*Generated by fhevm-forge — Foundry scaffold for Zama FHEVM*
*https://github.com/yourusername/fhevm-forge*