# End-to-End Example
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/examples/end-to-end
A complete encrypt to store to decrypt flow using @cofhe/sdk
This example demonstrates the full lifecycle of working with encrypted data: initialize the SDK, encrypt a value, send it to a contract, and decrypt the result, both for UI display and for onchain verification.
## The contract
A simple contract that stores an encrypted `uint64` balance and allows the owner to set and read it.
```solidity contracts/ConfidentialVault.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import '@fhenixprotocol/cofhe-contracts/FHE.sol';
contract ConfidentialVault {
mapping(address => euint64) private _balances;
function deposit(externalEuint64 encryptedAmount, bytes calldata inputProof) external {
euint64 amount = FHE.asEuint64(encryptedAmount, inputProof);
_balances[msg.sender] = FHE.add(_balances[msg.sender], amount);
FHE.allowThis(_balances[msg.sender]);
FHE.allowSender(_balances[msg.sender]);
}
function getBalance() public view returns (euint64) {
return _balances[msg.sender];
}
function publishBalance(
euint64 ctHash,
uint64 plaintext,
bytes calldata signature
) external {
FHE.publishDecryptResult(ctHash, plaintext, signature);
}
}
```
## SDK: full flow
```typescript Web (viem) theme={null}
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web';
import { Encryptable, FheTypes } from '@cofhe/sdk';
import { chains } from '@cofhe/sdk/chains';
import { createPublicClient, createWalletClient, http, custom } from 'viem';
import { sepolia } from 'viem/chains';
// 1. Initialize
const config = createCofheConfig({
supportedChains: [chains.sepolia],
});
const client = createCofheClient(config);
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
const walletClient = createWalletClient({
chain: sepolia,
transport: custom(window.ethereum),
});
await client.connect(publicClient, walletClient);
// 2. Create an ACP
await client.acp.getOrCreateSelfACP();
// 3. Encrypt and deposit
const [amountHash, signature] = await client
.encryptInputs([Encryptable.uint64(100n)])
.setConsumingContract(contract.address)
.onStep((step, ctx) => {
if (ctx?.isStart) console.log(`Encrypting: ${step}...`);
})
.execute();
await contract.deposit(amountHash, signature);
// 4. Decrypt for UI display
const ctHash = await contract.getBalance();
const balance = await client
.decryptForView(ctHash, FheTypes.Uint64)
.execute();
console.log('Balance:', balance); // 100n
// 5. Decrypt for onchain verification
const { decryptedValue, signature } = await client
.decryptForTx(ctHash)
.withACP()
.execute();
await contract.publishBalance(ctHash, decryptedValue, signature);
```
```typescript Node (ethers v6) theme={null}
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/node';
import { Encryptable, FheTypes } from '@cofhe/sdk';
import { Ethers6Adapter } from '@cofhe/sdk/adapters';
import { chains } from '@cofhe/sdk/chains';
import { ethers } from 'ethers';
// 1. Initialize
const provider = new ethers.JsonRpcProvider('https://rpc.sepolia.org');
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const config = createCofheConfig({
supportedChains: [chains.sepolia],
});
const client = createCofheClient(config);
const { publicClient, walletClient } = await Ethers6Adapter(provider, wallet);
await client.connect(publicClient, walletClient);
// 2. Create an ACP
await client.acp.getOrCreateSelfACP();
// 3. Encrypt and deposit
const [amountHash, signature] = await client
.encryptInputs([Encryptable.uint64(100n)])
.setConsumingContract(contract.address)
.execute();
await contract.deposit(amountHash, signature);
// 4. Decrypt for UI display
const ctHash = await contract.getBalance();
const balance = await client
.decryptForView(ctHash, FheTypes.Uint64)
.execute();
console.log('Balance:', balance); // 100n
// 5. Decrypt for onchain verification
const { decryptedValue, signature } = await client
.decryptForTx(ctHash)
.withACP()
.execute();
await contract.publishBalance(ctHash, decryptedValue, signature);
```
```typescript Hardhat test theme={null}
import hre from 'hardhat';
import { CofheClient, Encryptable, FheTypes } from '@cofhe/sdk';
import { expect } from 'chai';
describe('ConfidentialVault', () => {
let cofheClient: CofheClient;
before(async () => {
const [signer] = await hre.ethers.getSigners();
cofheClient = await hre.cofhe.createClientWithBatteries(signer);
});
it('deposits and reads an encrypted balance', async () => {
const Factory = await hre.ethers.getContractFactory('ConfidentialVault');
const vault = await Factory.deploy();
// Encrypt and deposit
const [amountHash, proof] = await cofheClient
.encryptInputs([Encryptable.uint64(100n)])
.setConsumingContract(await vault.getAddress())
.execute();
await (await vault.deposit(amountHash, proof)).wait();
// Decrypt and verify
const ctHash = await vault.getBalance();
const balance = await cofheClient
.decryptForView(ctHash, FheTypes.Uint64)
.execute();
expect(balance).to.equal(100n);
});
it('publishes a decrypt result onchain', async () => {
const Factory = await hre.ethers.getContractFactory('ConfidentialVault');
const vault = await Factory.deploy();
// Encrypt and deposit
const [amountHash, proof] = await cofheClient
.encryptInputs([Encryptable.uint64(42n)])
.setConsumingContract(await vault.getAddress())
.execute();
await (await vault.deposit(amountHash, proof)).wait();
// Decrypt for onchain verification
const ctHash = await vault.getBalance();
const { decryptedValue, signature } = await cofheClient
.decryptForTx(ctHash)
.withACP()
.execute();
// Publish onchain with the Teecryptor signature
await (await vault.publishBalance(ctHash, decryptedValue, signature)).wait();
});
});
```
# Templates & Starters
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/examples/templates
Ready-to-use project templates for building with @cofhe/sdk
Get started quickly with pre-configured project templates.
## Hardhat Starter
A minimal Hardhat project with `@cofhe/hardhat-plugin` pre-configured, including a sample contract and test.
Clone this template to start building FHE contracts with Hardhat immediately.
```bash theme={null}
git clone https://github.com/FhenixProtocol/cofhe-hardhat-starter.git
cd cofhe-hardhat-starter
npm install
npx hardhat test
```
### What's included
* `@cofhe/hardhat-plugin` and `@cofhe/sdk` pre-installed
* `hardhat.config.ts` with `evmVersion: 'cancun'` and the plugin imported
* A sample FHE contract
* A test demonstrating the encrypt, store, and decrypt flow
* Pre-configured network settings for local development and testnets
## Foundry Starter
A minimal Foundry project with `@cofhe/foundry-plugin` pre-configured, including a sample `Counter` contract and a comprehensive test suite covering plaintext assertions, public-decrypt flows, ACL deny paths, and fuzzing.
Clone this template to start building FHE contracts with Foundry immediately.
```bash theme={null}
git clone https://github.com/FhenixProtocol/cofhe-foundry-starter.git
cd cofhe-foundry-starter
npm install
forge build
forge test -vvv
```
### What's included
* `@cofhe/foundry-plugin`, `@cofhe/mock-contracts`, and `@fhenixprotocol/cofhe-contracts` pre-installed
* `foundry.toml` with `evm_version = "cancun"`, `solc_version = "0.8.25"`, and `code_size_limit = 100000`
* `remappings.txt` configured for the plugin and mocks
* A sample `Counter` contract using FHE
* Tests demonstrating `expectPlaintext`, `decryptForTx_withoutACP`, ACP-based unseal, ACL deny assertions, and fuzz tests
* Deploy scripts for `eth-sepolia`, `arb-sepolia`, and `base-sepolia`
# CofheClient
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/foundry-plugin/cofhe-client
The in-Solidity SDK shim: one client per user, produces encrypted inputs and signed ACPs
`CofheClient` is the Foundry plugin's in-Solidity SDK shim. One client per "user" in your scenario. The client carries a private key and produces encrypted inputs and signed ACPs **as if it were that user's frontend SDK**, with no JS bridge required.
## Creating and connecting
Spin up a client from inside [`CofheTest`](/cofhe/client-sdk/foundry-plugin/cofhe-test) with `createCofheClient()`, then bind it to an address with `connect(pkey)`:
```solidity theme={null}
CofheClient bob = createCofheClient();
bob.connect(0xB0B); // bob.account() == vm.addr(0xB0B)
```
After `connect`, the client knows which address to sign as. All `createExternalEuintN` and `ACP_*` calls use that account automatically; there's no `account` argument to pass.
To act onchain as that user, prank with `client.account()`:
```solidity theme={null}
(externalEuint32 hash, bytes memory proof) = bob.createExternalEuint32(2000, address(counter));
vm.prank(bob.account());
counter.reset(hash, proof);
```
A mismatch between the prank address and the client that produced the input will fail the ZK-verifier signature check. The input was signed for `bob.account()`, not whoever you pranked. Always match the client to the prank.
## Encrypting inputs
The client mirrors the JS SDK's `encryptInputs` API, one method per encrypted Solidity type. Each takes the plaintext plus the contract that will consume it, and returns the handle and its proof as a pair:
| Method | Returns |
| - | - |
| `createExternalEbool(bool, address)` | `(externalEbool, bytes)` |
| `createExternalEuint8(uint8, address)` | `(externalEuint8, bytes)` |
| `createExternalEuint16(uint16, address)` | `(externalEuint16, bytes)` |
| `createExternalEuint32(uint32, address)` | `(externalEuint32, bytes)` |
| `createExternalEuint64(uint64, address)` | `(externalEuint64, bytes)` |
| `createExternalEuint128(uint128, address)` | `(externalEuint128, bytes)` |
| `createExternalEaddress(address, address)` | `(externalEaddress, bytes)` |
The second argument is the consuming contract. The verifier binds it into the signature, so a proof made for one contract will not verify in another.
```solidity theme={null}
(externalEuint32 hash, bytes memory proof) = bob.createExternalEuint32(42, address(counter));
vm.prank(bob.account());
counter.reset(hash, proof);
```
For several values under one signature, use `createEncryptedInputsBatch`.
## Decrypting
The plugin exposes both decryption flows the SDK supports:
| Method | Returns | Use for |
| - | - | - |
| `decryptForTx_withoutACP(ctHash)` | `(bytes32 ctHash, uint256 plaintext, bytes signature)` | Globally-allowed (`FHE.allowPublic`) ciphertexts. Pass `signature` to `FHE.publishDecryptResult`. |
| `decryptForTx_withACP(ctHash, acp)` | `(bytes32, uint256, bytes)` | ACL-gated `decryptForTx` flow. |
| `decryptForView(ctHash, acp)` | `uint256 plaintext` | Offchain seal/unseal flow. **Reverts on deny**, so use the mock directly to assert deny. |
### Public-decrypt 3-step flow with `decryptForTx_withoutACP`
Mirrors the production flow when a contract calls `FHE.publishDecryptResult`:
```solidity theme={null}
// Step 1: contract grants public decrypt permission
vm.prank(bob.account());
counter.allowCounterPublicly(); // calls FHE.allowPublic(handle)
// Step 2: SDK fetches plaintext + threshold-network signature
bytes32 ctHash = euint32.unwrap(counter.count());
(, uint256 plaintext, bytes memory sig) = bob.decryptForTx_withoutACP(ctHash);
// Step 3: contract verifies signature and stores plaintext
counter.revealCounter(uint32(plaintext), sig);
```
The same shape runs unmodified against real CoFHE on testnet. The mock signature is produced by the same `MockThresholdNetworkSigner` that `FHE.verifyDecryptResult` accepts.
### ACP-based unseal with `decryptForView`
```solidity theme={null}
ACP memory bobAcp = bob.ACP_createSelf();
uint256 value = bob.decryptForView(ctHash, bobAcp);
assertEq(value, 42);
```
`decryptForView` reverts when the caller isn't on the ACL. To **assert** the deny path (e.g. "Alice should NOT be able to decrypt Bob's value"), drop down to the mock directly. See [Testing: Deny path](/cofhe/client-sdk/foundry-plugin/testing#deny-path).
## Access Control Permissions
The client signs EIP-712 ACPs against the ACL's domain. Two flavors:
| Method | Purpose |
| - | - |
| `ACP_createSelf()` | Self-ACP for the connected account; sealing key is auto-derived (`keccak(address)`). |
| `ACP_createShared(recipient)` | Issuer half of a shared ACP (no sealing key; the recipient adds it on import). |
| `ACP_exportShared(acp)` | Strip sensitive fields to produce `SharedACPExport` (safe to transmit out-of-band). |
| `ACP_importShared(export)` | Recipient-side completion: adds sealing key and recipient signature. Reverts unless `export.recipient == account()`. |
| `createSealingKey(seed)` | Custom sealing key. Rarely needed: `ACP_createSelf` derives one for you. |
### Self-ACP (most common)
```solidity theme={null}
ACP memory bobAcp = bob.ACP_createSelf();
uint256 plaintext = bob.decryptForView(ctHash, bobAcp);
```
`ACP_createSelf` builds the EIP-712 typed-data, derives a sealing key from the connected account, and signs, all in one call.
### Shared ACPs (issuer to recipient)
```solidity theme={null}
// Bob (issuer) creates an ACP shared to Alice (recipient)
ACP memory shared = bob.ACP_createShared(alice.account());
// Bob exports it (strips bob's sealing key) for transmission
SharedACPExport memory exported = bob.ACP_exportShared(shared);
// Alice imports it — adds her sealing key and recipient signature
ACP memory aliceImported = alice.ACP_importShared(exported);
```
`ACP_importShared` reverts unless the calling client's `account()` matches `export.recipient`, preventing Alice from importing an ACP shared to someone else.
## Common pitfalls
`vm.prank(bob.account())` while the input came from `alice.createExternalEuintN(...)` fails ZK verification. The input was signed for Alice's address, not Bob's. Match the client to the prank.
`euint32.unwrap(counter.count())` returns the *current* handle. Storing it in a local then asserting after a write reads the old handle.
```solidity theme={null}
bytes32 oldHandle = euint32.unwrap(counter.count());
counter.increment();
// ❌ oldHandle still references the pre-increment handle
expectPlaintext(oldHandle, uint32(0)); // passes by accident
expectPlaintext(counter.count(), uint32(1)); // ✅ fetch the new handle
```
Re-fetch after each state change.
The `pkey` passed to `connect` must derive the address used as `acp.issuer`. If you call `bob.ACP_createSelf()` after `bob.connect(0xB0B)`, the issuer is `vm.addr(0xB0B)`. Trying to forge an issuer mismatch will fail signature verification.
Useful default: most tests want a hard failure when the caller isn't permitted. To assert "Alice cannot decrypt", call the mock's `querySealOutput` directly:
```solidity theme={null}
(bool allowed, string memory err, ) = mockThresholdNetwork.querySealOutput(
uint256(ctHash), block.chainid, aliceAcp
);
assertFalse(allowed);
assertEq(err, "NotAllowed");
```
## Next steps
* [Testing](/cofhe/client-sdk/foundry-plugin/testing): full test-writing patterns.
* [CofheTest](/cofhe/client-sdk/foundry-plugin/cofhe-test): the test base contract that creates clients.
# CofheTest
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/foundry-plugin/cofhe-test
The CofheTest abstract base contract: deploys CoFHE mocks and exposes plaintext assertions
`CofheTest` is the abstract test base shipped by `@cofhe/foundry-plugin`. It already inherits `forge-std/Test`, so inherit `CofheTest` directly; inheriting both is a redeclaration error.
```solidity theme={null}
import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol";
contract MyTest is CofheTest {
function setUp() public {
deployMocks();
// ... your contract deploys
}
}
```
## `deployMocks()`
Call once in `setUp()`. Deploys the full CoFHE mock stack and wires the contracts together:
| Mock | Role | Address |
| - | - | - |
| MockCoFHEAddressBook | Resolves the TaskManager, at the address `FHE.sol` reads | `0xC0F4…196b` (fixed) |
| `MockTaskManager` | Manages FHE operations; stores plaintext values onchain | `0x…5000` (fixed) |
| `MockACL` | Access control list for encrypted handles | (deployed) |
| `MockZkVerifier` | Verifies encrypted-input ZK proofs | `0x…5001` (fixed) |
| `MockZkVerifierSigner` | Signs encrypted inputs (for the mock verifier) | (funded with 10 ether) |
| `MockThresholdNetwork` | Handles decryption requests | `0x…5002` (fixed) |
| `MockThresholdNetworkSigner` | Signs decrypt-result outputs | (deployed) |
After `deployMocks()` runs, the following public state vars are available on `CofheTest`:
```solidity theme={null}
mockAddressBook
mockTaskManager
mockAcl
mockZkVerifier
mockZkVerifierSigner
mockThresholdNetwork
mockThresholdNetworkSigner
```
Reach into them when you need behavior `expectPlaintext` doesn't cover (e.g. permission-denied assertions: see [Testing](/cofhe/client-sdk/foundry-plugin/testing#deny-path)).
## `createCofheClient()`
Returns a fresh [`CofheClient`](/cofhe/client-sdk/foundry-plugin/cofhe-client). You typically follow it with `connect(pkey)`:
```solidity theme={null}
CofheClient bob = createCofheClient();
bob.connect(0xB0B);
```
One client per scenario address. Connecting with a deterministic plaintext private key keeps test output debuggable.
## Reading plaintext values
Because `MockTaskManager` stores plaintext values onchain, you can read the underlying plaintext of any encrypted handle directly, with no ACP needed.
### `getPlaintext(handle)`
Returns the onchain plaintext from `MockTaskManager.mockStorage`. Typed overloads exist for every encrypted Solidity type:
```solidity theme={null}
bytes32 plaintext = getPlaintext(handle); // raw
bool flag = getPlaintext(eboolHandle);
uint8 v8 = getPlaintext(euint8Handle);
uint16 v16 = getPlaintext(euint16Handle);
uint32 v32 = getPlaintext(euint32Handle);
uint64 v64 = getPlaintext(euint64Handle);
uint128 v128 = getPlaintext(euint128Handle);
address addr = getPlaintext(eaddressHandle);
```
Reverts if the handle isn't in mock storage.
### `expectPlaintext(handle, value)` and `(handle, value, "msg")`
Assertion variant, with typed overloads for the same type set. Faster than `decryptForView` (no SDK round-trip, no ACP needed). Use it whenever you only care about the value, not the SDK code path.
```solidity theme={null}
expectPlaintext(counter.count(), uint32(42));
expectPlaintext(counter.count(), uint32(42), "after increment");
```
Prefer `expectPlaintext` over `decryptForView` for state assertions. Reserve the SDK path for tests where the SDK behavior itself is under test.
## Logging
The mocks log every FHE operation to the console when log mode is on. Toggle it locally with:
| Function | Effect |
| - | - |
| `enableLogs()` | Calls `mockTaskManager.setLogOps(true)` |
| `disableLogs()` | Calls `mockTaskManager.setLogOps(false)` |
Logging is verbose, so enable it only around the operation you're investigating.
```solidity theme={null}
function test_DebuggingIncrement() public {
enableLogs();
vm.prank(bob.account());
counter.increment();
disableLogs();
}
```
Each operation prints in a formatted block:
```text theme={null}
├ FHE.add | euint32(4473..3424)[0] + euint32(1157..3648)[1] => euint32(1106..1872)[1]
├ FHE.allowThis | euint32(1106..1872)[1] -> 0x663f..6602
```
## Common pitfalls
Solidity LSPs (Wake, Cursor, etc.) often resolve imports from the monorepo root rather than the package's `node_modules`. The plugin's remappings are package-local, so the LSP may flag valid imports. **`forge` is authoritative.** If `forge build` and `forge test` succeed, the test is correct.
That import path is the **old** API from `@cofhe/mock-contracts@0.4.x`. Migrate to:
```solidity theme={null}
import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol";
```
See the [migration mapping](/cofhe/client-sdk/foundry-plugin/testing#migration-from-the-old-mock-contracts-api) for the full rename table.
The remappings are relative to the foundry package's root. Running `forge test` from a parent monorepo directory won't resolve them; `forge` resolves from the working directory.
## Next steps
* [CofheClient](/cofhe/client-sdk/foundry-plugin/cofhe-client): per-user encrypt, decrypt, and ACP shim.
* [Testing](/cofhe/client-sdk/foundry-plugin/testing): canonical test-writing patterns and migration mapping.
# Getting Started
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/foundry-plugin/getting-started
Set up @cofhe/foundry-plugin for local FHE contract development and testing under Forge
`@cofhe/foundry-plugin` is the Foundry counterpart to [`@cofhe/hardhat-plugin`](/cofhe/client-sdk/hardhat-plugin/getting-started). It provides two abstract Solidity contracts, `CofheTest` (test base, deploys all CoFHE mocks) and `CofheClient` (per-account encrypt, decrypt, and ACP shim), that let you exercise FHE contracts under `forge test` with **no JS SDK required**.
Want to skip the setup? Clone the [cofhe-foundry-starter](https://github.com/FhenixProtocol/cofhe-foundry-starter) template to get a pre-configured project ready to go.
## What the plugin provides
* **`CofheTest`**: abstract test base that inherits `forge-std/Test` and deploys the full CoFHE mock stack (`MockTaskManager`, `MockACL`, `MockZkVerifier`, `MockThresholdNetwork`).
* **`CofheClient`**: in-Solidity SDK shim. One client per "user" in your scenario; each client carries a private key and produces encrypted inputs and signed ACPs as if it were that user's frontend SDK.
* **Plaintext assertions**: `expectPlaintext(handle, value)` reads the onchain plaintext from the mock task manager. Faster than `decryptForView` and needs no ACP.
## Prerequisites
* [Foundry](https://book.getfoundry.sh/getting-started/installation) (`forge`, `cast`, `anvil`)
* Node.js 18+ with npm/pnpm/yarn (for installing the plugin's npm dependencies)
## Installation
The plugin and its dependencies are distributed via npm. Install them as dev dependencies in your Foundry project:
```bash npm theme={null}
npm install -D @cofhe/foundry-plugin@^0.8.0 @cofhe/mock-contracts@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0 @openzeppelin/contracts
```
```bash pnpm theme={null}
pnpm add -D @cofhe/foundry-plugin@^0.8.0 @cofhe/mock-contracts@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0 @openzeppelin/contracts
```
```bash yarn theme={null}
yarn add -D @cofhe/foundry-plugin@^0.8.0 @cofhe/mock-contracts@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0 @openzeppelin/contracts
```
```bash theme={null}
forge install foundry-rs/forge-std
```
The CoFHE contracts and mocks have specific Solidity / EVM requirements. Set them in `foundry.toml`:
```toml foundry.toml theme={null}
[profile.default]
src = "src"
out = "out"
test = "test"
script = "script"
libs = ["node_modules"]
solc_version = "0.8.25" # cofhe-contracts target
auto_detect_remappings = false
code_size_limit = 100000 # mocks exceed 24 KB
```
* `code_size_limit = 100000` is required, the mock contracts exceed the EIP-170 24 KB ceiling.
* `solc_version = "0.8.25"` matches the compiler used by `@fhenixprotocol/cofhe-contracts`.
`evm_version = "cancun"` is no longer required as of `@cofhe/mock-contracts@0.8.0`. `MockACL` was migrated off transient storage (`tstore`/`tload`) to block-number-based storage, and the pragma was lowered to `>=0.8.19`. Set it only if your own contracts need cancun-specific opcodes.
```text remappings.txt theme={null}
forge-std/=node_modules/forge-std/src/
hardhat/=node_modules/forge-std/src/
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
@fhenixprotocol/cofhe-contracts/=node_modules/@fhenixprotocol/cofhe-contracts/
@cofhe/mock-contracts/=node_modules/@cofhe/mock-contracts/
@cofhe/foundry-plugin/=node_modules/@cofhe/foundry-plugin/
```
Two non-obvious shapes worth calling out:
* **`@cofhe/mock-contracts/` points at the package root**, not `…/contracts/`. The plugin's own imports already include the `contracts/` segment (e.g. `@cofhe/mock-contracts/contracts/MockTaskManager.sol`). Pointing at `…/contracts/` will break those imports.
* **`hardhat/=node_modules/forge-std/src/` is load-bearing.** `MockCoFHE.sol` inside the mock contracts imports `hardhat/console.sol`; this alias resolves it to forge-std's compatible `console.sol`. Drop it and `forge build` errors with `Source "hardhat/console.sol" not found`.
## Verify the setup
```bash theme={null}
forge build
forge test
```
If `forge build` succeeds, your remappings and `foundry.toml` are correct. From here, write a test that inherits `CofheTest`:
```solidity test/MyTest.t.sol theme={null}
import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol";
contract MyTest is CofheTest {
function setUp() public {
deployMocks();
// ... your contract deploys
}
}
```
## Version pinning
The plugin and `@cofhe/mock-contracts` pin `@fhenixprotocol/cofhe-contracts` *exactly* (no caret). Keep the three CoFHE packages aligned, otherwise `npm install` may resolve `cofhe-contracts` to a newer version that the mocks don't implement, producing `MockTaskManager should be marked as abstract` at compile time.
Known-aligned tuple as of writing:
| Package | Version |
| - | - |
| `@cofhe/foundry-plugin` | `0.8.0` |
| `@cofhe/mock-contracts` | `0.8.0` |
| `@fhenixprotocol/cofhe-contracts` | `1.0.0` |
See the [Compatibility](/cofhe/get-started/introduction/compatibility) page for the canonical table.
## Mocks vs production
The mocks are the same `@cofhe/mock-contracts` package the [Hardhat plugin](/cofhe/client-sdk/hardhat-plugin/mock-contracts) uses. Behaviorally:
* Plaintext lives onchain in `MockTaskManager.mockStorage` (so `expectPlaintext` and `getPlaintext` work).
* No real ZK proving; encrypted inputs are signed by `MockZkVerifierSigner`.
* Decryption is synchronous. `decryptForTx_withoutACP` returns the result immediately.
* Mock signatures are accepted by the same `FHE.verifyDecryptResult` your contract uses on testnet.
The same test code runs unchanged against real CoFHE on a deployed network.
## Next steps
* [CofheTest](/cofhe/client-sdk/foundry-plugin/cofhe-test): the test base contract: `deployMocks`, `expectPlaintext`, `getPlaintext`, log toggles.
* [CofheClient](/cofhe/client-sdk/foundry-plugin/cofhe-client): per-user shim: `createExternalEuintN`, `decryptForTx_withoutACP`, `decryptForView`, ACPs.
* [Testing](/cofhe/client-sdk/foundry-plugin/testing): canonical test patterns and the migration mapping from the old `@cofhe/mock-contracts/foundry/CoFheTest.sol` API.
# Testing
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/foundry-plugin/testing
Canonical test-writing patterns for FHE contracts under Foundry
This page shows the load-bearing patterns for writing FHE contract tests under Foundry with `@cofhe/foundry-plugin`. Hardhat counterpart: [Hardhat Plugin: Testing](/cofhe/client-sdk/hardhat-plugin/testing).
## Skeleton
```solidity test/Counter.t.sol theme={null}
import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol";
import { CofheClient } from "@cofhe/foundry-plugin/contracts/CofheClient.sol";
import { externalEuint32 } from "@fhenixprotocol/cofhe-contracts/FHE.sol";
import { Counter } from "../src/Counter.sol";
contract CounterTest is CofheTest {
Counter public counter;
CofheClient bob;
CofheClient alice;
uint256 constant BOB_PKEY = 0xB0B;
uint256 constant ALICE_PKEY = 0xA11CE;
function setUp() public {
deployMocks();
bob = createCofheClient(); bob.connect(BOB_PKEY);
alice = createCofheClient(); alice.connect(ALICE_PKEY);
vm.prank(bob.account());
counter = new Counter();
}
function test_Increments() public {
vm.prank(bob.account());
counter.increment();
expectPlaintext(counter.count(), uint32(1));
}
}
```
That's the load-bearing shape. Everything below is what to add as the contract grows more complex.
## Rules
### 1. Inherit `CofheTest`, not `Test`
`CofheTest` already inherits `forge-std/Test` and exposes the mock state vars (`mockTaskManager`, `mockAcl`, `mockThresholdNetwork`, …) you'll occasionally reach into. Inheriting both is a redeclaration error.
### 2. One `CofheClient` per scenario address
Each user with their own ACP and encrypted inputs gets their own client. Connect with a deterministic plaintext private key. Don't recycle real keys; these are visible in test output.
```solidity theme={null}
CofheClient bob = createCofheClient();
bob.connect(0xB0B); // bob.account() == vm.addr(0xB0B)
```
You don't pass `account` to `createExternalEuintN`; the client is bound at `connect`.
### 3. `vm.prank(client.account())` to act onchain as that user
```solidity theme={null}
vm.prank(bob.account());
(externalEuint32 hash, bytes memory proof) = bob.createExternalEuint32(2000, address(counter));
counter.reset(hash, proof);
```
Mismatching the prank address and the client that produced the input fails the ZK-verifier signature check.
### 4. Assert with `expectPlaintext` whenever possible
```solidity theme={null}
expectPlaintext(counter.count(), uint32(2000)); // typed overload
```
Faster than `decryptForView` and needs no ACP. Reserve the SDK path for tests where the SDK behavior itself is under test.
### 5. Test the public-decrypt 3-step flow with `decryptForTx_withoutACP`
When the contract calls `FHE.publishDecryptResult`:
```solidity theme={null}
// Step 1: contract grants public decrypt permission
vm.prank(bob.account());
counter.allowCounterPublicly(); // FHE.allowPublic(handle)
// Step 2: SDK fetches plaintext + threshold-network signature
bytes32 ctHash = euint32.unwrap(counter.count());
(, uint256 plaintext, bytes memory sig) = bob.decryptForTx_withoutACP(ctHash);
// Step 3: contract verifies signature and stores plaintext
counter.revealCounter(uint32(plaintext), sig);
assertEq(counter.getDecryptedValue(), plaintext);
```
Same shape runs unmodified against real CoFHE on testnet. The mock signature is produced by the same `MockThresholdNetworkSigner` that `FHE.verifyDecryptResult` accepts.
### 6. ACP-based unseal: `decryptForView` for the success path
```solidity theme={null}
import { ACP } from "@cofhe/mock-contracts/contracts/Permissioned.sol";
ACP memory bobAcp = bob.ACP_createSelf();
uint256 value = bob.decryptForView(ctHash, bobAcp);
assertEq(value, expected);
```
`ACP_createSelf` builds the EIP-712 typed-data, derives a sealing key from the connected account, and signs, with no manual `signPermissionSelf` boilerplate.
7. Deny path: when the caller is not on the ACL
`decryptForView` reverts when the caller isn't on the ACL. To assert the deny path, drop down to the mock directly:
```solidity theme={null}
ACP memory aliceAcp = alice.ACP_createSelf();
(bool allowed, string memory err, ) = mockThresholdNetwork.querySealOutput(
uint256(ctHash), block.chainid, aliceAcp
);
assertFalse(allowed, "Alice should NOT be allowed");
assertEq(err, "NotAllowed");
```
`mockThresholdNetwork` is a public field on `CofheTest`.
### 8. Fuzz tests inherit normally
```solidity theme={null}
function testFuzz_Reset(uint32 v) public {
(externalEuint32 enc, bytes memory proof) = bob.createExternalEuint32(v, address(counter));
vm.prank(bob.account());
counter.reset(enc);
expectPlaintext(counter.count(), v);
}
```
`createExternalEuintN` accepts the full `uintN` range, so no shaping needed.
## Migration from the old `mock-contracts` API
If you're upgrading from `@cofhe/mock-contracts@0.4.x` (where `CoFheTest` lived inside the mocks package), here's the rename table:
| Old API (mock-contracts ≤ 0.4) | New API (`@cofhe/foundry-plugin`) |
| - | - |
| `import { CoFheTest } from "@cofhe/mock-contracts/foundry/CoFheTest.sol"` | `import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol"` |
| `is Test, CoFheTest` | `is CofheTest` (Test already inherited) |
| `assertHashValue(handle, value)` | `expectPlaintext(handle, value)` |
| `mockStorage(ctHash)` | `getPlaintext(ctHash)` |
| `createInEuint32(v, bob)` | `bob.createExternalEuint32(v, address(target))` |
| `createPermissionSelf(bob)` + `signPermissionSelf(perm, bobKey)` | `bob.ACP_createSelf()` (auto-signs) |
| `createSealingKey(seed)` | `bob.createSealingKey(seed)` (rarely needed: `ACP_createSelf` derives one) |
| `queryDecrypt(hash, chainId, permit)` | `bob.decryptForView(hash, acp)` (reverts on deny) |
| Same, asserting deny | `mockThresholdNetwork.querySealOutput(hash, block.chainid, acp)` |
| `querySealOutput` + `unseal` | `bob.decryptForView` (does both) |
| `decryptForTxWithoutPermit(ct)` returns `(allowed, error, plaintext)` | `bob.decryptForTx_withoutACP(ct)` returns `(ctHash, plaintext, signature)` |
## Common pitfalls
LSPs like Wake/Cursor often resolve from the monorepo root; the plugin's remappings are package-local. **`forge` is the truth.** If `forge build` and `forge test` succeed, the test is correct.
Tests pass on the first op, then a second op reverts with `ACLNotAllowed` because the contract itself isn't on the ACL. Toggle `enableLogs()` to see the missing grant. Every op prints a line showing whether `allowThis`/`allow` was called.
`vm.prank(bob.account())` while the input came from `alice.createExternalEuintN(...)` fails ZK verification. Always match the client to the prank.
`euint32.unwrap(counter.count())` returns the *current* handle. Storing it in a local then asserting after a write reads the old handle:
```solidity theme={null}
bytes32 oldHandle = euint32.unwrap(counter.count());
counter.increment();
expectPlaintext(oldHandle, uint32(0)); // passes by accident
expectPlaintext(counter.count(), uint32(1)); // ✅ re-fetch
```
Re-fetch after each state change.
The `pkey` passed to `connect` must derive the address used as `acp.issuer`. `bob.ACP_createSelf()` after `bob.connect(0xB0B)` produces `issuer == vm.addr(0xB0B)`. Trying to forge a mismatch fails signature verification.
## Related
* [CofheTest](/cofhe/client-sdk/foundry-plugin/cofhe-test): the test base contract API.
* [CofheClient](/cofhe/client-sdk/foundry-plugin/cofhe-client): the per-user shim API.
* [Testing under Hardhat](/cofhe/client-sdk/hardhat-plugin/testing): the same patterns for Hardhat.
# Access Control Permissions (ACP)
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/guides/acps
Create and manage the EIP-712 signatures that authorize decryption
An Access Control Permission (ACP) is an EIP-712 signature that authorizes decryption of confidential data. The `issuer` field identifies who is reading the data, and that address must already have been granted access onchain with `FHE.allow(handle, address)`. When you use an ACP, CoFHE validates it against the ACL contract to confirm the issuer really holds that access.
Every ACP carries a sealing keypair. The public key goes to CoFHE so it can seal the result to the holder. The private key stays on the client and unseals the value when it comes back.
ACPs were called permits before `0.7`. The name changed so they are not confused with an ERC-2612 permit, which is a different thing entirely. If you are upgrading, see [migrating to 0.7](/cofhe/client-sdk/introduction/migrating-to-0-7). This page uses "ACP" throughout.
## When you need one
* `decryptForView` always requires an ACP.
* `decryptForTx` depends on the contract's ACL policy for that handle. If the policy lets anyone decrypt, use `.withoutACP()`. If it restricts decryption, use `.withACP(...)`.
## Prerequisites
[Create and connect a client](/cofhe/client-sdk/guides/client-setup). ACPs are scoped to a chainId and account pair.
## Quick start
`client.acp` is the recommended API. It signs with the connected wallet and manages the store for you. `ACPUtils` is the lower-level alternative when you want direct control over signing and storage. Note the namespace is singular: `client.acp`, not `client.acps`.
```typescript client.acp (recommended) theme={null}
await client.connect(publicClient, walletClient);
// Returns the active self ACP if there is one, otherwise creates and signs it.
const acp = await client.acp.getOrCreateSelfACP();
```
```typescript ACPUtils theme={null}
import { ACPUtils, setACP, setActiveACPHash } from '@cofhe/sdk/acps';
const acp = await ACPUtils.createSelfAndSign(
{ issuer: walletClient.account.address },
publicClient,
walletClient
);
const chainId = await publicClient.getChainId();
const account = walletClient.account.address;
setACP(chainId, account, acp);
setActiveACPHash(chainId, account, acp.hash);
```
After this the active ACP is picked up automatically by `decryptForView(...).execute()` and by `decryptForTx(...).withACP().execute()`.
## The three types
| Type | Who signs | Use for |
| - | - | - |
| `self` | Issuer only | Decrypting your own data. The common case. |
| `sharing` | Issuer only | A shareable offer the issuer creates for a recipient |
| `recipient` | Recipient, carrying the issuer signature | The imported ACP after the recipient signs it |
`expiration` is a unix timestamp in seconds and defaults to 7 days from creation. The client-wide `defaultACPExpiration` config key is a separate setting that defaults to 30 days. Creating an ACP through `client.acp.*` stores it and makes it active.
## Creating a self ACP
A self ACP lets you decrypt data that was allowed to your own address.
```typescript theme={null}
await client.connect(publicClient, walletClient);
const acp = await client.acp.createSelf({
issuer: walletClient.account.address,
name: 'My self ACP',
});
acp.type; // 'self'
acp.hash; // deterministic hash
```
`createSelf` always creates a new one. `getOrCreateSelfACP()` reuses the active ACP when there is one, which is what most applications want.
## Narrowing what an ACP can read
`0.7` adds a scope to every ACP. An unscoped ACP covers everything the issuer can read, which is rarely what you want to hand to someone else.
| Scope | Value | Covers |
| - | - | - |
| `Global` | `0` | Every value the issuer can read |
| `Contract` | `1` | The issuer's values readable by the listed `contracts` |
| `Handles` | `2` | Only the listed `handles`, as bytes32 hex strings |
```typescript theme={null}
const acp = await client.acp.createSharing({
issuer: walletClient.account.address,
recipient,
contracts: [auctionAddress], // scope narrows to this contract
name: 'Auction results',
});
```
A scope only ever narrows the issuer's existing access. It cannot grant access the issuer does not already hold, so scoping is not a way to delegate something you were never allowed to read. It also does not retroactively narrow ACPs you already issued.
Handles are bytes32 hex strings here, not bigints. If you are carrying handle values around as bigints, convert before putting them in an ACP.
## Sharing with another account
An issuer can delegate their ACL access to a recipient, who can then decrypt the issuer's data without holding their own `FHE.allow` grant. There are two routes: pass the offer yourself, or post it onchain.
### Passing the offer yourself
```typescript theme={null}
const sharingAcp = await client.acp.createSharing({
issuer: walletClient.account.address,
recipient,
name: 'Share with recipient',
});
```
```typescript theme={null}
import { ACPUtils } from '@cofhe/sdk/acps';
const exported = ACPUtils.export(sharingAcp);
```
The exported JSON holds no sensitive data and can travel over any channel.
`ACPUtils.export` throws unless the ACP is a signed sharing ACP. In `0.6` the equivalent call serialized anything you gave it. A call that used to always work, such as one made during a render, now always throws on a self ACP. Gate it on `acp.type === 'sharing'`.
Never share the output of `serialize(acp)`. That is for local persistence and contains the sealing private key.
```typescript theme={null}
const recipientAcp = await client.acp.importShared(exported);
recipientAcp.type; // 'recipient'
```
Importing generates a fresh sealing key for the recipient.
### Sharing onchain
Passing the offer yourself means building a channel for it: an API, a message, a QR code. `0.7` adds an alternative. The issuer posts the signed offer to a sharing registry, and the recipient discovers it by reading the chain.
```typescript theme={null}
// Issuer: post the signed sharing ACP
await client.acp.shareOnChain(sharingAcp);
// Recipient: discover and import
const shares = await client.acp.getIncomingShares();
const recipientAcp = await client.acp.importFromChain(shares[0]);
```
Both sides need a connected wallet. Only `getIncomingShares` is a read; the rest send transactions.
| Method | Who calls it | What it does |
| - | - | - |
| `shareOnChain(acp)` | Issuer | Posts a signed sharing ACP to the registry. Returns `{ txHash, shareId }` |
| `getIncomingShares()` | Recipient | Reads the importable shares addressed to the connected account |
| `importFromChain(share)` | Recipient | Fills in the recipient's sealing key, signs, stores, and makes it the active ACP |
| `cancelShare(shareId)` | Issuer | Retracts a pending share that has not been imported yet |
| `dismissShare(shareId)` | Recipient | Declines a share, or clears an entry the recipient no longer wants |
`getIncomingShares()` returns only shares that are still importable, so expired and revoked ones are filtered out. Each entry carries a `shareId` alongside the shared ACP fields, and that id is what `cancelShare` and `dismissShare` take.
`cancelShare` and `dismissShare` resolve to the same registry call. They are named for who is acting and why: the issuer retracting an offer, or the recipient declining one.
The registry address resolves from the chain's ACL. Override it per chain with the `sharingRegistry` key in the `acp` config block:
```typescript theme={null}
createCofheConfig({
supportedChains: [chains.sepolia],
acp: {
sharingRegistry: { 11155111: '0x…' },
},
});
```
Posting onchain makes the offer's existence public. The registry records that this issuer shared with this recipient, and when. The encrypted values stay confidential, but the relationship does not, which the offline route avoids.
## Revoking access
An issuer can revoke an ACP they created, which matters when a sharing ACP has left their control.
```typescript theme={null}
await client.acp.revokeACP(acp.hash);
await client.acp.revokeAllACPs();
const revoked = await client.acp.isACPRevoked(acp.hash);
```
Revocation is checked when the ACP is used, so it applies to copies the issuer no longer holds.
## Managing stored ACPs
The SDK keeps every stored ACP and one active ACP hash per chainId and account.
```typescript theme={null}
const acps = client.acp.getACPs();
Object.keys(acps); // ACP hashes
const active = client.acp.getActiveACP();
client.acp.selectActiveACP(someACPHash);
client.acp.removeACP(acpHash);
client.acp.removeActiveACP();
```
## Validating
`ACPUtils.validate` enforces the full check: schema, signed, and not expired. The decrypt flows call it for you and surface failures as typed errors, so validate manually only when you want to inspect or filter ACPs first.
| Function | Checks | On failure |
| - | - | - |
| `ACPUtils.validate(acp)` | Schema, signed, and not expired | Throws |
| `ACPUtils.validateSchema(acp)` | Schema only | Throws on schema failure, ignores expiry and signatures |
Use `validateSchema` on an ACP that arrived over the wire, when you want to reject a malformed payload before caring about expiry.
For inspection without exception handling, `ValidationUtils` returns a typed result:
```typescript theme={null}
import { ValidationUtils } from '@cofhe/sdk/acps';
const result = ValidationUtils.isValid(acp);
result.valid; // boolean
result.error; // 'invalid-schema' | 'expired' | 'not-signed' | null
```
| Function | Returns | Use for |
| - | - | - |
| `ValidationUtils.isValid(acp)` | `ValidationResult` | The full check without throwing |
| `ValidationUtils.isSignedAndNotExpired(acp)` | `ValidationResult` | Skipping the schema parse when you already validated shape |
| `ValidationUtils.isSigned(acp)` | `boolean` | Whether it carries a signature on the appropriate side |
| `ValidationUtils.isExpired(acp)` | `boolean` | Comparing `acp.expiration` to now |
Match on `result.error` to render a precise message:
```typescript theme={null}
switch (ValidationUtils.isValid(acp).error) {
case 'expired': return 'This ACP has expired. Please sign again.';
case 'not-signed': return 'ACP is awaiting signature.';
case 'invalid-schema': return 'Imported ACP is malformed.';
case null: return null;
}
```
## Persistence and security
* ACPs are stored per chainId and account. On the web the store is `localStorage` under the key `cofhesdk-acps`.
* A stored ACP contains the sealing private key. Treat it as a secret, and never hand a serialized ACP to another user.
* To share access, use `ACPUtils.export`, which strips the sensitive fields.
ACPs stored by `0.6` are dropped on upgrade. They were signed with EIP-712 types the upgraded ACL no longer accepts, so they cannot verify and are discarded when the store loads. Your users are prompted to sign again. Nothing needs migrating, but it looks like data loss if you are not expecting it.
# Client Setup
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/guides/client-setup
Configure and connect the @cofhe/sdk client with viem, Ethers, or wagmi
This page covers the core SDK lifecycle:
1. Create config (`createCofheConfig`)
2. Create client (`createCofheClient`)
3. Connect (`client.connect`)
4. Manage connection (change account / disconnect)
## 1. Create config
Import `createCofheConfig` from the entrypoint that matches your runtime:
* Browser apps: `@cofhe/sdk/web`
* Node.js scripts/backends: `@cofhe/sdk/node`
The only required field is `supportedChains`.
```typescript theme={null}
import { createCofheConfig } from '@cofhe/sdk/web';
import { chains } from '@cofhe/sdk/chains';
const config = createCofheConfig({
supportedChains: [chains.sepolia],
// Optional knobs
// defaultACPExpiration: 60 * 60 * 24 * 30,
// useWorkers: true,
});
```
## 2. Create the client
```typescript theme={null}
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web';
import { chains } from '@cofhe/sdk/chains';
const config = createCofheConfig({ supportedChains: [chains.sepolia] });
const cofheClient = createCofheClient(config);
cofheClient.connected; // false
cofheClient.connecting; // false
```
## 3. Connect
The SDK connects to CoFHE using viem clients:
* `PublicClient`: read-only chain access
* `WalletClient`: signing + sending transactions
The wallet client must carry an account. `connect` reads the connected address from `walletClient.account`, and falls back to `eth_accounts` on the transport. A wallet client created with `http()` and no `account` has neither, so `connect` throws `PublicWalletGetAddressesFailed` and the client stays disconnected.
In the browser, take the account from the injected wallet. In Node.js, build it from a private key.
```typescript Browser wallet theme={null}
import { createPublicClient, createWalletClient, custom, http } from 'viem';
import { sepolia } from 'viem/chains';
const [account] = await window.ethereum.request({
method: 'eth_requestAccounts',
});
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
const walletClient = createWalletClient({
chain: sepolia,
transport: custom(window.ethereum),
account,
});
await cofheClient.connect(publicClient, walletClient);
cofheClient.connected; // true
cofheClient.connection.account; // the address selected in the wallet
```
```typescript Node.js theme={null}
import { createPublicClient, createWalletClient, http } from 'viem';
import { sepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY');
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
const walletClient = createWalletClient({
chain: sepolia,
transport: http(),
account,
});
await cofheClient.connect(publicClient, walletClient);
cofheClient.connected; // true
cofheClient.connection.account; // account.address
```
### Using adapters
If you use Ethers, `@cofhe/sdk/adapters` converts an Ethers provider and signer into viem-shaped clients, and derives the account from the signer. wagmi already gives you viem clients, so pass them to `connect` directly. The wallet client from `useWalletClient` carries the connected account.
```typescript Ethers v6 theme={null}
import { Ethers6Adapter } from '@cofhe/sdk/adapters';
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('https://rpc.sepolia.org');
const signer = new ethers.Wallet('0xYOUR_PRIVATE_KEY', provider);
const { publicClient, walletClient } = await Ethers6Adapter(
provider,
signer
);
await cofheClient.connect(publicClient, walletClient);
```
```typescript Ethers v5 theme={null}
import { Ethers5Adapter } from '@cofhe/sdk/adapters';
import { ethers } from 'ethers5';
const provider = new ethers.providers.JsonRpcProvider(
'https://rpc.sepolia.org'
);
const signer = new ethers.Wallet('0xYOUR_PRIVATE_KEY').connect(provider);
const { publicClient, walletClient } = await Ethers5Adapter(
provider,
signer
);
await cofheClient.connect(publicClient, walletClient);
```
```tsx Wagmi theme={null}
import { useEffect } from 'react';
import { usePublicClient, useWalletClient } from 'wagmi';
function useConnectCofhe() {
const publicClient = usePublicClient();
const { data: walletClient } = useWalletClient();
useEffect(() => {
// useWalletClient resolves only after the user connects a wallet
if (!publicClient || !walletClient) return;
cofheClient.connect(publicClient, walletClient);
}, [publicClient, walletClient]);
}
```
## 4. Managing connections
### Reconnect behavior
Calling `connect` again with the same clients is a no-op. Calling it with new clients replaces the connection state.
### Changing connected account
To switch the client's connected account, call `cofheClient.connect()` with a wallet client that carries the new account.
```typescript theme={null}
import { privateKeyToAccount } from 'viem/accounts';
const bob = privateKeyToAccount('0xBOB_PRIVATE_KEY');
const alice = privateKeyToAccount('0xALICE_PRIVATE_KEY');
const bobWalletClient = createWalletClient({
chain: sepolia,
transport: http(),
account: bob,
});
const aliceWalletClient = createWalletClient({
chain: sepolia,
transport: http(),
account: alice,
});
// Connect as Bob
await cofheClient.connect(publicClient, bobWalletClient);
cofheClient.connection.account; // bob.address
// Switch to Alice
await cofheClient.connect(publicClient, aliceWalletClient);
cofheClient.connection.account; // alice.address
```
### Disconnecting
To manually disconnect, call `cofheClient.disconnect()`. This clears the in-memory connection state (clients/account/chainId) and marks the client as disconnected.
It does **not** delete persisted ACPs or stored FHE keys.
```typescript theme={null}
cofheClient.disconnect();
cofheClient.connected; // false
```
# Decrypt to Transact
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/guides/decrypt-to-tx
Decrypt with a verifiable Teecryptor signature for onchain use
Use `decryptForTx` to reveal a confidential (encrypted) value onchain. It returns the plaintext together with a [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) signature, so a contract can verify the reveal when you publish it in a transaction.
Common use cases:
* **Unshield a confidential token**: reveal the encrypted amount you're unshielding so the contract can finalize the public transfer.
* **Finalize a private auction / game move**: bids or moves are submitted encrypted, and the winner is revealed later in a verifiable way.
If you only need to show plaintext in your UI (and you do **not** need an onchain-verifiable signature), use [`decryptForView`](/cofhe/client-sdk/guides/decrypt-to-view) instead.
## Prerequisites
1. [Create and connect a client](/cofhe/client-sdk/guides/client-setup).
2. Know the onchain encrypted handle (`ctHash`) you want to decrypt.
3. Determine whether the contract's ACL policy for this `ctHash` requires a [ACP](/cofhe/client-sdk/guides/acps).
`decryptForTx` does not take a `utype`. It always returns the plaintext as a `bigint` because the result is intended to be passed into a transaction. If you need UI-friendly decoding, use [`decryptForView`](/cofhe/client-sdk/guides/decrypt-to-view).
## ACP: when is it needed?
Often, `decryptForTx` is used to reveal a value that the protocol already considers OK to make public. In those cases, the contract's ACL policy can allow anyone to decrypt, and you can use `.withoutACP()`.
Examples where an ACP is **not** needed:
* **Unshielding**: the amount being unshielded is no longer meant to stay secret.
* **Auction/game reveal**: it doesn't matter who submits the reveal, only that the result is verified.
If the ACL policy restricts decryption, you must use `.withACP(...)`.
## What `decryptForTx` returns
`.execute()` resolves to an object with:
* `ctHash: bigint | string`: the ciphertext handle you decrypted
* `decryptedValue: bigint`: the plaintext value (always a `bigint`)
* `signature: 0x${string}`: the Teecryptor signature as a hex string
## Decrypt (choose ACP mode)
```typescript No ACP theme={null}
const decryptResult = await client
.decryptForTx(ctHash)
.withoutACP()
.execute();
decryptResult.decryptedValue;
decryptResult.signature;
```
```typescript Active ACP theme={null}
const decryptResult = await client
.decryptForTx(ctHash)
.withACP()
.execute();
```
```typescript Explicit ACP theme={null}
const acp = await client.acp.getOrCreateSelfACP();
const decryptResult = await client
.decryptForTx(ctHash)
.withACP(acp)
.execute();
```
After decrypting, see [Writing Decrypt Result to Contract](/cofhe/client-sdk/guides/writing-decrypt-result) for how to publish or verify the result onchain.
## Builder API
### `.execute()` (required, call last)
Runs the decryption and returns `{ ctHash, decryptedValue, signature }`.
### `.withACP(...)` (required unless using `.withoutACP()`)
* `.withACP()`: uses the active ACP
* `.withACP(acpHash)`: fetches a stored ACP by hash
* `.withACP(acp)`: uses the provided ACP object
### `.withoutACP()` (required unless using `.withACP(...)`)
Decrypt via global allowance (no ACP). Only works if the contract's ACL policy allows anyone to decrypt that `ctHash`.
### `.setAccount(address)` (optional)
Overrides the account used to resolve the active/stored ACP.
### `.setChainId(chainId)` (optional)
Overrides the chain used to resolve the Teecryptor URL and ACPs.
### `.onPoll(callback)` (optional)
Register a callback that fires once per poll attempt while `decryptForTx` waits for Teecryptor to return the plaintext. Useful for surfacing progress in a UI.
```typescript theme={null}
const decryptResult = await client
.decryptForTx(ctHash)
.withoutACP()
.onPoll(({ operation, requestId, attemptIndex, elapsedMs, intervalMs, timeoutMs }) => {
console.log(`[${operation}] attempt ${attemptIndex} after ${elapsedMs}ms (next in ${intervalMs}ms, budget ${timeoutMs}ms)`);
})
.execute();
```
The callback receives:
| Field | Type | Description |
| - | - | - |
| `operation` | `'decrypt' \| 'sealoutput'` | Which decryption flow is polling. For `decryptForTx` this is always `'decrypt'`. |
| `requestId` | `string` | The Teecryptor request id. **May be the empty string** during submit-time retries, see `.set404RetryTimeout(...)` below. |
| `attemptIndex` | `number` | Zero-based poll attempt counter. |
| `elapsedMs` | `number` | Time since the first submit attempt. |
| `intervalMs` | `number` | Delay until the next poll. |
| `timeoutMs` | `number` | Overall budget shared by submit-retries and status-polling. |
### `.set404RetryTimeout(timeoutMs)` (optional)
Configures how long `decryptForTx` keeps retrying when Teecryptor's submit endpoint responds with `404 Not Found` before a `requestId` is available. This typically happens on slower backends where the ciphertext isn't visible yet at submit time. Defaults to `10_000` ms.
```typescript theme={null}
const decryptResult = await client
.decryptForTx(ctHash)
.withoutACP()
.set404RetryTimeout(20_000) // give a slower backend more time
.execute();
```
Pass `0` to disable submit-time retries (`404` becomes a hard failure). Submit retries share the same overall timeout budget as status polling, so a larger value here trades poll time for submit-recovery time.
## Common pitfalls
* **ACP mode must be selected**: you must call exactly one of `.withACP(...)` or `.withoutACP()` before `.execute()`.
* **Wrong chain/account**: ACPs are scoped to `chainId + account`. If you get an ACL/ACP error, double-check the connected chain and account.
# Decrypt to View
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/guides/decrypt-to-view
Reveal encrypted values locally for UI display using ACPs
Use `decryptForView` to reveal a confidential (encrypted) value locally in your app so you can display it in the UI.
Unlike [`decryptForTx`](/cofhe/client-sdk/guides/decrypt-to-tx), this flow does **not** return an onchain-verifiable signature, and it is **not** meant to be published onchain.
## Flow
1. Read the encrypted handle (`ctHash`) from your contract.
2. Ensure you have an ACP that authorizes decryption of that value.
3. Call `decryptForView(ctHash, utype).execute()` to get the plaintext.
`decryptForView` always decrypts using an ACP (there is no `.withoutACP()` mode). If your protocol intends for the plaintext to become publicly visible onchain, use [`decryptForTx`](/cofhe/client-sdk/guides/decrypt-to-tx) instead.
## Prerequisites
1. [Create and connect a client](/cofhe/client-sdk/guides/client-setup).
2. Know the encrypted handle (`ctHash`) and the encrypted type (`utype`).
3. Have a [ACP](/cofhe/client-sdk/guides/acps) available for the connected `chainId + account`.
**Getting `ctHash`**: In most apps, `ctHash` comes from reading a stored encrypted value, an event arg, or a return value from a `view` call.
**Providing `utype`**: `utype` must match the ciphertext's underlying FHE type. The SDK uses it to convert the decrypted `bigint` into a convenient JS type.
Supported `utype`s:
* `FheTypes.Bool` to returns a `boolean`
* `FheTypes.Uint160` (address) to returns a checksummed `0x...` string
* `FheTypes.Uint8 | Uint16 | Uint32 | Uint64 | Uint128` to returns a `bigint`
## ACP setup
If you don't have an ACP yet, create one once after connecting:
```typescript theme={null}
await client.connect(publicClient, walletClient);
// Creates an ACP if needed, stores it, and selects it as the active ACP.
await client.acp.getOrCreateSelfACP();
```
## Decrypt for UI
Choose the pattern that matches how your app manages ACPs:
```typescript Active ACP theme={null}
await client.connect(publicClient, walletClient);
await client.acp.getOrCreateSelfACP();
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint32)
.execute();
```
```typescript ACP object theme={null}
const acp = await client.acp.getOrCreateSelfACP();
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint64)
.withACP(acp)
.execute();
```
```typescript ACP hash theme={null}
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint8)
.withACP(acpHash)
.execute();
```
## What `decryptForView` returns
Running `.execute()` resolves to a scalar JS value:
* Integer utypes (`Uint8`, `Uint16`, `Uint32`, `Uint64`, `Uint128`): a `bigint`
* `FheTypes.Bool`: a `boolean`
* `FheTypes.Uint160` (address): a checksummed `0x...` address string
## Builder API
### `.execute()` (required, call last)
Runs the decryption and returns a UI-friendly scalar value.
### `.withACP(...)` (optional)
Select which ACP to use:
* `.withACP()`: uses the active ACP
* `.withACP(acpHash)`: fetches a stored ACP by hash
* `.withACP(acp)`: uses the provided ACP object
If you don't call `.withACP(...)`, the active ACP is used by default.
### `.setAccount(address)` (optional)
Overrides the account used to resolve the active/stored ACP.
### `.setChainId(chainId)` (optional)
Overrides the chain used to resolve the [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) URL and ACPs.
### `.onPoll(callback)` (optional)
Register a callback that fires once per poll attempt while `decryptForView` waits for Teecryptor to return the sealed plaintext. Useful for surfacing decrypt progress in a UI.
```typescript theme={null}
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint64)
.onPoll(({ operation, requestId, attemptIndex, elapsedMs, intervalMs, timeoutMs }) => {
console.log(`[${operation}] attempt ${attemptIndex} after ${elapsedMs}ms (next in ${intervalMs}ms, budget ${timeoutMs}ms)`);
})
.execute();
```
The callback receives:
| Field | Type | Description |
| - | - | - |
| `operation` | `'decrypt' \| 'sealoutput'` | Which decryption flow is polling. For `decryptForView` this is `'sealoutput'`. |
| `requestId` | `string` | The Teecryptor request id. **May be the empty string** during submit-time retries, see `.set404RetryTimeout(...)` below. |
| `attemptIndex` | `number` | Zero-based poll attempt counter. |
| `elapsedMs` | `number` | Time since the first submit attempt. |
| `intervalMs` | `number` | Delay until the next poll. |
| `timeoutMs` | `number` | Overall budget shared by submit-retries and status-polling. |
### `.set404RetryTimeout(timeoutMs)` (optional)
Configures how long `decryptForView` keeps retrying when Teecryptor's submit endpoint responds with `404 Not Found` before a `requestId` is available. This typically happens on slower backends where the ciphertext isn't visible yet at submit time. Defaults to `10_000` ms.
```typescript theme={null}
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint32)
.set404RetryTimeout(20_000) // give a slower backend more time
.execute();
```
Pass `0` to disable submit-time retries (`404` becomes a hard failure). Submit retries share the same overall timeout budget as status polling.
## Common UI patterns
```typescript Format bigint for display theme={null}
import { formatUnits } from 'viem';
const decimals = 6;
const display = formatUnits(amount, decimals);
```
```typescript Bigint → number (range-check) theme={null}
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
const asNumber = amount <= MAX_SAFE ? Number(amount) : undefined;
```
```typescript Booleans & addresses theme={null}
const statusLabel = decryptedIsAllowed ? 'Allowed' : 'Not allowed';
const shortOwner = `${decryptedOwner.slice(0, 6)}…${decryptedOwner.slice(-4)}`;
```
## Common pitfalls
* **Missing ACP**: `decryptForView` will fail if there is no active ACP for the current `chainId + account`.
* **Wrong `utype`**: you must pass the correct FHE type for the ciphertext.
* **Wrong chain/account**: ACPs are scoped to `chainId + account`. If the user switches wallets or networks, create/select the correct ACP.
# Encrypting Inputs
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/guides/encrypting-inputs
Encrypt plaintext values with a batch ZK proof for use in FHE-enabled smart contracts
`encryptInputs` encrypts plaintext values into FHE ciphertexts you can pass into a confidential contract call. Values must be encrypted before they go onchain, or there is nothing confidential about them.
One call produces one batch: a ciphertext handle for each value, plus a single signature covering the whole batch.
## Prerequisites
1. [Create and connect a client](/cofhe/client-sdk/guides/client-setup).
2. Know which encrypted type each value needs. It must match the Solidity parameter your contract declares, for example `externalEuint32` against `externalEuint64`.
3. Know the address of the contract that will consume the inputs. You have to declare it before signing.
## Basic usage
```typescript theme={null}
import { Encryptable } from '@cofhe/sdk';
await cofheClient.connect(publicClient, walletClient);
const [ageHash, flagHash, addressHash, signature] = await cofheClient
.encryptInputs([
Encryptable.uint32(42n),
Encryptable.bool(true),
Encryptable.address('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'),
])
.setConsumingContract(contractAddress)
.execute();
```
The result holds one handle per input, in the order you passed them, followed by the batch signature. It has `inputs.length + 1` elements, so code that assumes the result matches the input count is off by one.
## Declaring the consuming contract
`setConsumingContract` is required. The verifier binds the target contract into the signed digest, so a batch signed for one contract cannot be replayed into another.
Omitting it is a compile error in TypeScript: `encryptInputs()` returns a builder with no `execute()` method, and you get `Property 'execute' does not exist on type 'EncryptInputsBuilderUnset'`. JavaScript callers get no type check and hit a `ConsumingContractUninitialized` throw instead.
The consuming contract is the contract that runs `FHE.asEuint*`, which is not always the contract you call. If your app calls `vault.deposit(...)` and the vault is what converts the value, the consuming contract is the vault. Naming the wrong one compiles, typechecks, and reverts at runtime when the digest is recomputed onchain. Trace the value to the `FHE.asEuint*` call and use that address.
## Using the result in a transaction
The handle and its proof are a pair. In Solidity the proof parameter follows the handle it authenticates:
```solidity Solidity theme={null}
function confidentialTransfer(
address to,
externalEuint64 amount,
bytes calldata inputProof
) external {
euint64 value = FHE.asEuint64(amount, inputProof);
// ...
}
```
```typescript TypeScript theme={null}
const [amountHash, signature] = await cofheClient
.encryptInputs([Encryptable.uint64(amount)])
.setConsumingContract(tokenAddress)
.execute();
await contract.confidentialTransfer(recipient, amountHash, signature);
```
For more than one encrypted value, the handles stay adjacent and share the one signature:
```typescript theme={null}
const [amountHash, feeHash, signature] = await cofheClient
.encryptInputs([Encryptable.uint32(amount), Encryptable.uint32(fee)])
.setConsumingContract(tokenAddress)
.execute();
await contract.transfer(recipient, [amountHash, feeHash], signature);
```
The signature does not have to be the last parameter, and encrypted parameters must be adjacent to each other because they share one signature. See [migrating to 0.7](/cofhe/client-sdk/introduction/migrating-to-0-7) for the Solidity side.
## Builder API
### `.setConsumingContract(address)` (required)
The contract that will pass these values into `FHE.asEuint*`. Returns the builder that has `execute()`.
### `.execute()` (required, call last)
Runs the encryption pipeline and returns the handles followed by the batch signature.
### `.setAccount(address)` (optional)
Override the address that owns the encrypted inputs. Only that address can use them onchain. Defaults to the connected wallet account.
```typescript theme={null}
const result = await cofheClient
.encryptInputs([Encryptable.uint64(10n)])
.setAccount('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
.setConsumingContract(contractAddress)
.execute();
```
### `.setChainId(chainId)` (optional)
Override the chain the inputs will be used on. Defaults to the connected chain.
### `.setSecurityZone(zone)` (optional)
Override the security zone the batch is encrypted under. Defaults to zone `0`.
### `.setUseWorker(boolean)` (optional)
When `true`, the default, ZK proof generation runs in a Web Worker so it does not block the main thread. No effect in Node.js.
### `.onStep(callback)` (optional)
Fires at the start and end of each encryption step, which is useful for a progress indicator.
```typescript theme={null}
const result = await cofheClient
.encryptInputs([Encryptable.uint64(10n)])
.setConsumingContract(contractAddress)
.onStep((step, ctx) => {
if (ctx?.isStart) console.log(`Starting: ${step}`);
if (ctx?.isEnd) console.log(`Done: ${step} (${ctx.duration}ms)`);
})
.execute();
```
Setters can be called in any order, as long as they come before `.execute()`.
#### The encryption flow
`.execute()` runs five sequential steps:
| Step | Description |
| - | - |
| `InitTfhe` | Lazy-initializes the TFHE WASM module. Does nothing after the first call. |
| `FetchKeys` | Fetches, or loads from cache, the FHE public key and CRS for the target chain. |
| `Pack` | Packs the plaintext values into a ZK list ready for proving. |
| `Prove` | Generates the ZK proof of knowledge. Uses a Web Worker when available. |
| `Verify` | Sends the proof to the CoFHE verifier and returns the batch. |
## Creating the inputs
Use the `Encryptable` factory to build the items. Each function takes the plaintext value and an optional security zone.
| Factory | Data type | Solidity parameter |
| - | - | - |
| `Encryptable.bool(value)` | `boolean` | `externalEbool` |
| `Encryptable.uint8(value)` | `bigint \| string` | `externalEuint8` |
| `Encryptable.uint16(value)` | `bigint \| string` | `externalEuint16` |
| `Encryptable.uint32(value)` | `bigint \| string` | `externalEuint32` |
| `Encryptable.uint64(value)` | `bigint \| string` | `externalEuint64` |
| `Encryptable.uint128(value)` | `bigint \| string` | `externalEuint128` |
| `Encryptable.address(value)` | `bigint \| string` | `externalEaddress` |
There is also a generic form:
```typescript theme={null}
Encryptable.create('uint32', 42n);
Encryptable.create('bool', false);
Encryptable.create('address', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045');
```
A single `encryptInputs` call may encrypt at most **2048 bits** of plaintext in total. Going over throws `ZkPackFailed`.
## What replaced the per-item types
`0.7` removed the per-item input structs. `EncryptedItemInput`, `EncryptedUint64Input`, and the rest of that family no longer exist, and neither does `asHashPlusProof()`, because its output is what `execute()` always returns now.
A value that used to be one of those types is now a plain hash, typed `` `0x${string}` ``. The whole result is ``readonly `0x${string}`[]``.
These also break on your own helpers. A fixture typed `(encAmount: EncryptedUint64Input)` fails at its own definition, not at the call site. Spread the pair instead, or infer it with `Awaited>`.
## Common pitfalls
* **Forgetting the consuming contract**, or naming the contract you call rather than the one that converts the value. The second one fails at runtime, not at compile time.
* **Destructuring the wrong length.** The result carries a trailing signature, so a wrong-length destructure typechecks and then fails at runtime.
* **Wrong `Encryptable` type.** `Encryptable.uint32(...)` has to match the `externalEuint32` your function declares.
* **Wrong account or chain.** Inputs are authorized for one account and chain. Overriding either can make them unusable for the transaction you intended.
* **Reusing one encryption against two contracts.** Not possible with a single batch, because the signature binds to one consuming contract. Encrypt once per target.
* **Bit limit exceeded.** At most 2048 bits per call, otherwise `ZkPackFailed`.
# Error Handling
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/guides/error-handling
Handle errors from @cofhe/sdk operations using typed CofheError objects
All SDK operations return values directly and throw typed `CofheError` objects on failure. This replaces the `Result` wrapper pattern used by `cofhejs`.
## Catching errors
```typescript theme={null}
import { isCofheError, CofheErrorCode, Encryptable } from '@cofhe/sdk';
try {
const [encrypted] = await client
.encryptInputs([Encryptable.uint32(42n)])
.execute();
} catch (err) {
if (isCofheError(err)) {
console.error(err.code); // CofheErrorCode enum value
console.error(err.message); // human-readable description
}
}
```
## What a CofheError carries
Two fields are always set:
* `code: CofheErrorCode`: the enum value identifying the error type
* `message: string`: a human-readable description of what went wrong
Four more are declared optional on the class. The property always exists; its value is `undefined` unless the code that threw supplied it, so narrow before using it:
* `hint?: string`: an actionable suggestion for fixing the error. Set by most SDK throw sites, but not all
* `context?: Record`: the state that produced the error. For a `ZkPackFailed` from oversized inputs this holds `totalBits`, `maxBits`, and the offending `items`
* `cause?: Error`: the inner error being wrapped. When set, its message is also appended to `message` as `| Caused by: ...`
* `apiErrorCode?: string`: the raw backend error string. Set only on errors built from a decryption backend response
Because the four are optional, guard each one rather than assuming it is there:
```typescript theme={null}
try {
await client.encryptInputs([Encryptable.uint32(42n)]).execute();
} catch (err) {
if (isCofheError(err)) {
console.error(err.code, err.message);
if (err.hint) console.error('Hint:', err.hint);
if (err.context) console.error('Context:', err.context);
}
}
```
Use `isCofheError(err)` to check if a caught error is a `CofheError`.
## Common error codes
| Error code | When it occurs |
| - | - |
| `ZkPackFailed` | `encryptInputs` exceeded the 2048-bit total plaintext limit, or an item had an invalid `utype` |
| `ConsumingContractUninitialized` | `execute()` was called without `setConsumingContract(...)` |
| `ACPNotFound` | No ACP found for the given `chainId + account` |
| `ACPInvalid` | The ACP signature is invalid |
| `ACPMalformed` | The decryption backend could not parse the submitted ACP |
| `ACPExpired` | The ACP is past its expiration |
| `ACPRevoked` | The issuer revoked this ACP |
| `ACPDenied` | The ACP does not cover the requested handle |
| `ACPRequired` | The flow needs an ACP and none was supplied |
| `DecryptFailed` | The decryption request was rejected, or the response came back malformed |
| `NotConnected` | Attempted an operation before calling `client.connect(...)` |
The first two codes above are raised locally by the SDK. The `ACP*` codes other than `ACPNotFound` are stable codes returned by the decryption backend and mapped onto the enum, so those errors also carry `apiErrorCode`.
**Migrating from 0.6.x:** the `Permit*` codes are gone. `PermitNotFound` is now `ACPNotFound`, `InvalidPermitData` is `InvalidACPData`, `InvalidPermitDomain` is `InvalidACPDomain`, and `CannotRemoveLastPermit` is `CannotRemoveLastACP`. The set also expanded: expiry, revocation, and scope denial each have their own code, so you no longer have to infer which one applied from the message.
## Error handling patterns
### Encryption errors
```typescript theme={null}
import { isCofheError, CofheErrorCode, Encryptable } from '@cofhe/sdk';
try {
const encrypted = await client
.encryptInputs([Encryptable.uint128(veryLargeValue)])
.setConsumingContract(contractAddress)
.execute();
} catch (err) {
if (isCofheError(err) && err.code === CofheErrorCode.ZkPackFailed) {
console.error('Input too large, split into multiple calls');
}
}
```
### Decryption errors
```typescript theme={null}
import { isCofheError, CofheErrorCode, FheTypes } from '@cofhe/sdk';
try {
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint32)
.execute();
} catch (err) {
if (isCofheError(err) && err.code === CofheErrorCode.ACPNotFound) {
// Create an ACP and retry
await client.acp.getOrCreateSelfACP();
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint32)
.execute();
}
}
```
### Distinguishing why an ACP is invalid
The decrypt flows call `ACPUtils.validate(acp)` internally before submitting the request to the decryption backend. That helper enforces **schema + signed + not-expired** all at once, so when it fails the recovery path depends on **which** check tripped.
Use the non-throwing `ValidationUtils.isValid` helper from `@cofhe/sdk/acps` to pre-flight the active ACP and route based on the typed reason, which avoids the thrown error path entirely:
```typescript theme={null}
import { FheTypes } from '@cofhe/sdk';
import { ValidationUtils } from '@cofhe/sdk/acps';
const active = client.acp.getActiveACP();
const result = active
? ValidationUtils.isValid(active)
: { valid: false, error: 'not-signed' as const };
if (!result.valid) {
switch (result.error) {
case 'expired':
await client.acp.getOrCreateSelfACP(); // create a fresh one
break;
case 'not-signed':
await client.acp.getOrCreateSelfACP(); // prompt the wallet to sign
break;
case 'invalid-schema':
client.acp.removeActiveACP(); // stored payload is malformed
break;
}
}
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint32)
.execute();
```
`ValidationResult.error` is the typed union `'invalid-schema' | 'expired' | 'not-signed' | null`. See [validating ACPs](/cofhe/client-sdk/guides/acps#validating) for the full helper surface.
If you prefer the throwing path: `ACPUtils.validate(acp)` raises plain `Error`s with messages `ACP is expired` / `ACP is not signed` (or a Zod schema error). These are not wrapped in `CofheError`, so use `err.message` rather than an error code to branch.
# Writing Decrypt Result to Contract
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/guides/writing-decrypt-result
Submit a decryptForTx result onchain for verification or publishing
This page covers the "decrypt to write tx" flow after you run [`decryptForTx`](/cofhe/client-sdk/guides/decrypt-to-tx): you take `{ ctHash, decryptedValue, signature }` and submit a transaction that your contract can verify.
There are two common patterns:
* **Publish**: call `FHE.publishDecryptResult(ctHash, plaintext, signature)` so other contracts/users can reference the published result.
* **Verify-only**: call `FHE.verifyDecryptResult(ctHash, plaintext, signature)` inside your contract without publishing globally.
## Prerequisites
1. Run [`decryptForTx`](/cofhe/client-sdk/guides/decrypt-to-tx) and get a result object with `ctHash`, `decryptedValue`, and `signature`.
2. Ensure the Solidity parameter type matches your encrypted type.
`decryptedValue` is a `bigint`. If your Solidity function expects a smaller integer type (e.g. `uint32`), make sure the value is within range.
## Publish the decrypt result onchain
The intended consumer of `decryptForTx` is an onchain verifier such as `FHE.publishDecryptResult(...)`. In practice, you publish the result by calling a function on **your contract** that invokes `FHE.publishDecryptResult` internally.
```solidity Solidity theme={null}
import '@fhenixprotocol/cofhe-contracts/FHE.sol';
// Example wrapper (adjust the handle and plaintext types to match your encrypted type).
function publishDecryptResult(
euint32 handle,
uint32 plaintext,
bytes calldata signature
) external {
FHE.publishDecryptResult(handle, plaintext, signature);
}
```
```typescript TypeScript theme={null}
// `decryptResult` is returned by `decryptForTx(...).execute()`
const tx = await myContract.publishDecryptResult(
decryptResult.ctHash,
decryptResult.decryptedValue,
decryptResult.signature
);
await tx.wait();
```
## Verify a decrypt result signature (without publishing)
Some protocols don't need (or don't want) to publish the decrypt result globally, they only need to verify that the provided plaintext and signature match a specific handle (`ctHash`).
For example, an "unshield" flow can accept `(ctHash, plaintext, signature)` and only proceed if the signature is valid:
```solidity theme={null}
import '@fhenixprotocol/cofhe-contracts/FHE.sol';
function unshield(
bytes32 ctHash,
uint32 plaintext,
bytes calldata signature
) external {
require(
FHE.verifyDecryptResult(ctHash, plaintext, signature),
'Invalid decrypt signature'
);
// ...continue with protocol logic...
}
```
If you prefer to publish the result (so it can be reused elsewhere), use `FHE.publishDecryptResult(...)` instead.
# Writing Encrypted Data to Contract
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/guides/writing-encrypted-data
Encrypt plaintext values and pass them directly into a contract call
This page covers the "encrypt to write tx" flow: encrypt plaintext values and pass them straight into a contract call.
`encryptInputs` returns one ciphertext handle per value, followed by a single signature covering the batch. Each handle is an `externalEuintXX`, and the signature is the `bytes` proof that follows it in the function signature. The onchain CoFHE library verifies the proof before the contract can use the ciphertext.
## Flow
1. Ensure your contract function accepts an `externalEuintXX` handle plus a `bytes` proof.
2. Encrypt the plaintext values with [`encryptInputs`](/cofhe/client-sdk/guides/encrypting-inputs), naming the contract that will consume them.
3. Send a transaction, passing the handle and the proof.
## Prerequisites
1. [Create and connect a client](/cofhe/client-sdk/guides/client-setup).
2. Your contract function must accept `externalEuintXX` parameters with a `bytes` proof.
The encrypted type you choose in TypeScript must match the Solidity parameter type:
* `Encryptable.uint32(...)` to `externalEuint32`
* `Encryptable.bool(...)` to `externalEbool`
* `Encryptable.address(...)` to `externalEaddress`
## Example: encrypt and call a contract
```solidity Solidity theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import '@fhenixprotocol/cofhe-contracts/FHE.sol';
contract EncryptedCounter {
euint32 public count;
function setCount(externalEuint32 inCount, bytes calldata inputProof) external {
count = FHE.asEuint32(inCount, inputProof);
FHE.allowThis(count);
FHE.allowSender(count);
}
}
```
```typescript TypeScript (viem) theme={null}
import { Encryptable } from '@cofhe/sdk';
import { parseAbi } from 'viem';
import { sepolia } from 'viem/chains';
// externalEuint32 is a bytes32 value type on the wire
const encryptedCounterAbi = parseAbi([
'function setCount(bytes32 inCount, bytes inputProof)',
]);
// 1) Encrypt right before sending the transaction
const [countHash, signature] = await cofheClient
.encryptInputs([Encryptable.uint32(42n)])
.setConsumingContract(encryptedCounterAddress)
.execute();
// 2) Pass the handle and its proof
const hash = await walletClient.writeContract({
chain: sepolia,
account,
address: encryptedCounterAddress,
abi: encryptedCounterAbi,
functionName: 'setCount',
args: [countHash, signature],
});
await publicClient.waitForTransactionReceipt({ hash });
```
```typescript TypeScript (ethers) theme={null}
import { Encryptable } from '@cofhe/sdk';
// 1) Encrypt
const [countHash, signature] = await cofheClient
.encryptInputs([Encryptable.uint32(42n)])
.setConsumingContract(encryptedCounterAddress)
.execute();
// 2) Send the transaction
const tx = await contract.setCount(countHash, signature);
await tx.wait();
```
## Common pitfalls
* **Wrong `Encryptable` type**: the factory must match the Solidity parameter, `externalEuint32` against `externalEuint64`.
* **Wrong account / chain**: encrypted inputs are authorized for a specific `account + chainId`. If you encrypt under the wrong wallet/network, the contract call may revert.
* **Stale ABI shape**: the old `(ctHash, securityZone, utype, signature)` tuple is gone. A hand-written ABI takes `bytes32` for the handle and `bytes` for the proof.
* **Missing or wrong consuming contract**: name the contract that runs `FHE.asEuint*`. Getting it wrong compiles and reverts at runtime.
# Client
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/hardhat-3-plugin/client
Create and connect a CofheClient in a Hardhat 3 test
Every network connection carries a `cofhe` object with three methods for building a `CofheClient`. Use `createClientWithBatteries` in most tests, and the manual path when you need to change the client config.
The examples on this page run inside an `async describe` callback that opens a connection first:
```typescript theme={null}
import { network } from 'hardhat';
const { viem, cofhe } = await network.create();
const publicClient = await viem.getPublicClient();
const [walletClient] = await viem.getWalletClients();
```
## Batteries included
`cofhe.createClientWithBatteries(walletClient?)` returns a client that is ready to encrypt and decrypt. It:
1. Creates a config for the Hardhat mock environment, with `supportedChains: [hardhat]` and a zero `mocks.encryptDelay`.
2. Creates the client and connects it with the connection's public client and your wallet client.
3. Signs a self ACP (Access Control Permission) for the wallet's address and makes it the active ACP.
```typescript theme={null}
const client = await cofhe.createClientWithBatteries(walletClient);
client.connected; // true
```
Called with no argument, it signs with the first account of the connection:
```typescript theme={null}
const client = await cofhe.createClientWithBatteries();
```
Because the self ACP exists from the start, `decryptForView` and `decryptForTx().withACP()` work without any further setup.
## Manual setup
Build the client step by step when a test needs a different config, for example a non-zero `encryptDelay`.
```typescript theme={null}
const config = await cofhe.createConfig({ mocks: { encryptDelay: 100 } });
```
`cofhe.createConfig` wraps `createCofheConfig` from `@cofhe/sdk/node`. It sets `environment: 'hardhat'` and `supportedChains: [hardhat]`, defaults `mocks.encryptDelay` to `0`, and merges your overrides on top.
```typescript theme={null}
const client = cofhe.createClient(config);
```
The client is not connected yet: `client.connected` is `false`.
```typescript theme={null}
await client.connect(publicClient, walletClient);
```
`connect` takes viem clients directly, so the clients from `@nomicfoundation/hardhat-viem` pass straight in.
A manually built client also creates a self ACP when it connects, so `decryptForView` works without extra setup here too.
## Several signers
Create one client per wallet. Each client signs its own encrypted inputs and holds its own ACP.
```typescript theme={null}
const [alice, bob] = await viem.getWalletClients();
const aliceClient = await cofhe.createClientWithBatteries(alice);
const bobClient = await cofhe.createClientWithBatteries(bob);
```
An encrypted input is bound to the account that encrypted it. Send the transaction from that same wallet, or the contract reverts with `InvalidSigner`, which viem reports as the selector `0x7ba5ffb5`.
## API summary
| Method | Returns | Description |
| - | - | - |
| createClientWithBatteries | `Promise` | Connected client with a self ACP. Takes an optional `WalletClient` and defaults to the first account of the connection. |
| createConfig | `Promise` | Config for the Hardhat mock environment. Takes optional overrides. |
| createClient | `CofheClient` | Unconnected client built from a config. |
## Using the client
Once connected, the client is the standard SDK client. See:
* [Encrypting inputs](/cofhe/client-sdk/guides/encrypting-inputs)
* [Decrypt to view](/cofhe/client-sdk/guides/decrypt-to-view)
* [Decrypt to transact](/cofhe/client-sdk/guides/decrypt-to-tx)
* [Access Control Permissions](/cofhe/client-sdk/guides/acps)
# Getting Started
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/hardhat-3-plugin/getting-started
Install and configure @cofhe/hardhat-3-plugin for local FHE contract development with Hardhat 3
`@cofhe/hardhat-3-plugin` runs the CoFHE mock contracts inside Hardhat 3's simulated network and attaches a `cofhe` object to every network connection. Use it to compile, test, and debug FHE contracts locally without the offchain CoFHE services.
This plugin is for Hardhat 3. For a Hardhat 2 project, use [`@cofhe/hardhat-plugin`](/cofhe/client-sdk/hardhat-plugin/getting-started). The two are not interchangeable.
## What the plugin provides
* **Mock contracts** deployed on every new connection to the simulated network, standing in for the CoFHE coprocessor.
* **`cofhe` on the connection object**, next to the `viem` object from `@nomicfoundation/hardhat-viem`.
* **`cofhe.createClientWithBatteries()`**, which returns a connected SDK client with a self ACP (Access Control Permission) already signed.
* **Mock helpers** for reading plaintext values, toggling operation logs, and calling the mock contracts directly with viem.
* **Network presets** for `localcofhe`, `eth-sepolia`, and `arb-sepolia`.
## Install the plugin
These steps assume a Hardhat 3 project that already uses `@nomicfoundation/hardhat-viem` and `@nomicfoundation/hardhat-node-test-runner`.
`@cofhe/sdk` pins `viem` to an exact version and declares an optional peer dependency on Hardhat 2. Both collide with a Hardhat 3 project. Add these entries to `package.json` before you install:
```json npm (package.json) theme={null}
{
"overrides": {
"@cofhe/sdk": {
"hardhat": "$hardhat",
"viem": "$viem"
},
"@cofhe/hardhat-3-plugin": {
"viem": "$viem"
}
}
}
```
```yaml pnpm (pnpm-workspace.yaml) theme={null}
overrides:
viem: 2.57.1
```
```json yarn (package.json) theme={null}
{
"resolutions": {
"viem": "2.57.1"
}
}
```
For pnpm and yarn, use the `viem` version your project already declares.
```bash npm theme={null}
npm install --save-dev @cofhe/hardhat-3-plugin@^0.8.0 @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
```bash pnpm theme={null}
pnpm add -D @cofhe/hardhat-3-plugin@^0.8.0 @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
```bash yarn theme={null}
yarn add -D @cofhe/hardhat-3-plugin@^0.8.0 @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
Add the plugin to the `plugins` array, and list the mock contract sources in `npmFilesToBuild`:
```typescript hardhat.config.ts theme={null}
import { defineConfig } from 'hardhat/config';
import cofhePlugin from '@cofhe/hardhat-3-plugin';
import hardhatViem from '@nomicfoundation/hardhat-viem';
import hardhatNodeTestRunner from '@nomicfoundation/hardhat-node-test-runner';
export default defineConfig({
plugins: [cofhePlugin, hardhatViem, hardhatNodeTestRunner],
solidity: {
version: '0.8.28',
npmFilesToBuild: [
'@cofhe/mock-contracts/contracts/MockTaskManager.sol',
'@cofhe/mock-contracts/contracts/MockACL.sol',
'@cofhe/mock-contracts/contracts/ACPTimestampRevoker.sol',
'@cofhe/mock-contracts/contracts/ACPShareRegistry.sol',
'@cofhe/mock-contracts/contracts/MockZkVerifier.sol',
'@cofhe/mock-contracts/contracts/MockThresholdNetwork.sol',
],
},
});
```
The plugin reads the mock bytecode from your project's build artifacts. `npmFilesToBuild` makes the mocks part of that build, so the compile step of `npx hardhat test` keeps their artifacts in place.
Run `npx hardhat test`. The output prints `cofhe-hardhat-3-plugin :: mocks deployed` once for each connection your tests open.
## Why the overrides are needed
Each entry fixes a specific failure:
* **`hardhat`** (npm only): `@cofhe/sdk` declares `hardhat@^2` as an optional peer, so npm refuses the install with `ERESOLVE` once Hardhat 3 is in the tree. pnpm and yarn warn and continue.
* **`viem`**: `@cofhe/sdk` and the plugin pin `viem` 2.38.6, while `@nomicfoundation/hardhat-viem` requires 2.47.6 or later. Without the override, two copies of viem are installed. Tests still run, but TypeScript rejects every hardhat-viem client you pass to the plugin with error `TS2345`.
Without `npmFilesToBuild`, `@nomicfoundation/hardhat-node-test-runner` 3.0.17 and later remove the mock artifacts before the tests start. Every `network.create()` then fails with `HHE1000: Artifact for contract "MockTaskManager" not found`.
## Configuration
The plugin adds an optional `cofhe` key to the Hardhat config. Every option has a default, so you can leave the key out.
```typescript hardhat.config.ts theme={null}
export default defineConfig({
plugins: [cofhePlugin, hardhatViem, hardhatNodeTestRunner],
cofhe: {
gasWarning: true,
mocksDeployVerbosity: 'vv',
},
});
```
| Option | Type | Default | Effect |
| - | - | - | - |
| gasWarning | `boolean` | `false` | Prints a note after each mock deployment that mock FHE operations cost more gas than they do on a live network. |
| mocksDeployVerbosity | `'' \| 'v' \| 'vv'` | `'v'` | Output while the mocks deploy. `''` prints nothing, `'v'` prints one summary line, `'vv'` prints each contract and its address. |
The config type also accepts `cofhe.logMocks`, but no plugin code reads it. Setting it has no effect on FHE operation logs. Use the helpers on the [Logging](/cofhe/client-sdk/hardhat-3-plugin/logging) page instead.
## When the mocks deploy
The plugin hooks into Hardhat's connection lifecycle. Every call to `network.create()` returns a new simulated chain, and the plugin deploys the full mock stack on it before your code gets the connection.
```typescript test/Counter.test.ts theme={null}
import { describe, it } from 'node:test';
import { network } from 'hardhat';
describe('Counter', async () => {
const { viem, cofhe } = await network.create();
it('has the mocks deployed', async () => {
console.log(cofhe.mocks.MockTaskManager.address);
});
});
```
`network.connect()` behaves the same way, but Hardhat 3.4.0 deprecated it in favor of `network.create()`. `network.getOrCreate()` reuses an existing connection, and its mocks, when one is open.
The plugin checks whether the network answers Hardhat's `hardhat_metadata` RPC method before it deploys anything. On a testnet it skips the deployment, so the same connection code works against a live network. There is no environment variable to turn deployment off.
## Pre-configured networks
The plugin adds these networks unless your config already defines a network with the same name.
| Network | URL | Chain ID |
| - | - | - |
| `localcofhe` | `http://127.0.0.1:42069` | `420105` |
| `eth-sepolia` | `SEPOLIA_RPC_URL`, or the public Ethereum Sepolia RPC | `11155111` |
| `arb-sepolia` | `ARBITRUM_SEPOLIA_RPC_URL`, or the public Arbitrum Sepolia RPC | `421614` |
The testnet presets sign with the key in the `PRIVATE_KEY` environment variable.
## Differences from the Hardhat 2 plugin
| | `@cofhe/hardhat-plugin` (Hardhat 2) | `@cofhe/hardhat-3-plugin` |
| - | - | - |
| Entry point | `hre.cofhe` | `cofhe` on each network connection |
| Mocks deploy | Before `npx hardhat test` and `npx hardhat node` | On every new network connection |
| Test runner | Mocha | Node.js `node:test` |
| Signers | ethers `HardhatEthersSigner` | viem `WalletClient` |
| Registration | Side-effect `import` | `plugins` array |
| Mock contract access | Typed ethers contracts from `getMockTaskManager()` and similar | viem `{ address, abi }` descriptors |
| Skip mock deployment | `COFHE_SKIP_MOCKS_DEPLOY=1` | Automatic on networks that are not Hardhat |
| Tasks | `task:cofhe-mocks:deploy`, `task:cofhe-mocks:setlogops`, `task:cofhe:usefaucet` | `cofhe:set-log-ops`, `cofhe:faucet` |
The mock contracts themselves are the same package, `@cofhe/mock-contracts`, in both plugins.
## Next steps
* [Client](/cofhe/client-sdk/hardhat-3-plugin/client): create and connect an SDK client in a test.
* [Mock contracts](/cofhe/client-sdk/hardhat-3-plugin/mock-contracts): read plaintext values and call the mocks directly.
* [Logging](/cofhe/client-sdk/hardhat-3-plugin/logging): print the FHE operations your contracts perform.
* [Testing](/cofhe/client-sdk/hardhat-3-plugin/testing): a complete test file and the patterns around it.
# Logging
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/hardhat-3-plugin/logging
Print the FHE operations your contracts perform in Hardhat 3 test output
The mock `MockTaskManager` can print every FHE operation it runs. Under Hardhat 3 this logging is off until you turn it on, so wrap the code you want to inspect in `withLogs`.
## What gets logged
Each FHE operation prints one line with the operation name, its inputs, and its result. Each handle is shortened and followed, in brackets, by the plaintext the mock stores for it:
```text theme={null}
┌──────────────────┬──────────────────────────────────────────────────
│ [COFHE-MOCKS] │ "counter.increment()" logs:
├──────────────────┴──────────────────────────────────────────────────
├ FHE.asEuint32 | uint32(1) => euint32(1157..3648)[1]
├ FHE.asEuint32 | uint32(0) => euint32(4473..3424)[0]
├ FHE.add | euint32(4473..3424)[0] + euint32(1157..3648)[1] => euint32(1106..1872)[1]
├ FHE.allowThis | euint32(1106..1872)[1] -> 0x0dcd1bf9a1b36ce34237eeafef220932846bcd82
├ FHE.allow | euint32(1106..1872)[1] -> 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
└─────────────────────────────────────────────────────────────────────
```
## Log one block with `withLogs`
`cofhe.mocks.withLogs(name, fn)` turns logging on, runs `fn`, and prints `name` as the header of the block:
```typescript theme={null}
await cofhe.mocks.withLogs('counter.increment()', async () => {
await counter.write.increment();
});
```
When `fn` returns or throws, `withLogs` closes the block and turns logging back off if it was off before.
## Turn logging on and off
`enableLogs(label?)` and `disableLogs()` leave logging on across several calls:
```typescript theme={null}
await cofhe.mocks.enableLogs('counter.increment()');
await counter.write.increment();
await counter.write.increment();
await cofhe.mocks.disableLogs();
```
The optional label prints a header immediately, so you can find the section in the output.
## Why logging starts off
The logging switch is the `logOps` flag in `MockTaskManager` storage. The contract declares it `true`, but that default is set by the constructor. The plugin places `MockTaskManager` with `hardhat_setCode`, which skips the constructor, so the flag starts as `false` on every new connection.
Because the flag lives onchain, each helper above is a transaction on the current chain, and every new connection starts with logging off again.
The config type accepts `cofhe.logMocks`, but no plugin code reads it. Setting it to `true` does not turn logging on.
The `cofhe:set-log-ops` task also flips the flag, but it opens a connection of its own. Against the in-process network that is a separate chain, so the task does not affect your tests. Use the helpers above instead.
# Mock Contracts
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/hardhat-3-plugin/mock-contracts
Read plaintext values and call the CoFHE mock contracts in Hardhat 3 tests
The plugin deploys mock versions of the CoFHE contracts on every simulated chain. The mocks compute FHE operations onchain and keep each result's plaintext in storage, so a test can read any encrypted value without the offchain services.
## What the mocks deploy
| Contract | Address | Role |
| - | - | - |
| MockCoFHEAddressBook | `0xC0F4e00E531a2B086492Ae3DCC1515038307196b` | Resolves the TaskManager, at the same address `FHE.sol` reads on real networks |
| `MockTaskManager` | `0x0000000000000000000000000000000000005000` | Runs FHE operations and stores each result's plaintext |
| `MockACL` | Assigned at deployment | Access control for encrypted handles |
| `ACPTimestampRevoker` | Assigned at deployment | Default revoker for ACPs |
| `ACPShareRegistry` | Assigned at deployment | Onchain hand-off for shared ACPs |
| `MockZkVerifier` | `0x0000000000000000000000000000000000005001` | Verifies encrypted inputs |
| `MockThresholdNetwork` | `0x0000000000000000000000000000000000005002` | Answers decryption requests |
The plugin writes the fixed-address contracts' bytecode directly with `hardhat_setCode`. It deploys `MockACL` and the two ACP contracts as normal transactions, because they need their constructors to run.
The SDK detects the mocks by checking for code at the `MockZkVerifier` address. When it finds them, it skips ZK proof generation and lets the mocks verify inputs.
## Call a mock directly
`cofhe.mocks` exposes four of the mocks as viem `{ address, abi }` descriptors. Spread one into `readContract` or `writeContract`:
```typescript theme={null}
import { network } from 'hardhat';
const { viem, cofhe } = await network.create();
const publicClient = await viem.getPublicClient();
const aclAddress = await publicClient.readContract({
...cofhe.mocks.MockTaskManager,
functionName: 'acl',
});
```
| Descriptor | Contract |
| - | - |
| `cofhe.mocks.MockTaskManager` | `MockTaskManager` |
| `cofhe.mocks.MockACL` | `MockACL` |
| `cofhe.mocks.MockZkVerifier` | `MockZkVerifier` |
| `cofhe.mocks.MockThresholdNetwork` | `MockThresholdNetwork` |
The ABIs are typed, so viem checks `functionName` and `args` at compile time.
## Read plaintext values
Because `MockTaskManager` stores plaintexts, you can assert on encrypted state without an ACP or a decryption request. Both helpers accept the handle as a hex string, which is what viem returns for an `euint32` getter, or as a `bigint`.
### `getPlaintext(ctHash)`
Returns the stored plaintext as a `bigint`:
```typescript theme={null}
const counter = await viem.deployContract('Counter');
await counter.write.increment();
const count = await cofhe.mocks.getPlaintext(await counter.read.count());
// count === 1n
```
`getPlaintext` returns `0n` for a handle that has no stored value, so a typo in the handle reads as zero.
### `expectPlaintext(ctHash, expectedValue)`
Throws when the stored plaintext differs from `expectedValue`, or when the handle has no stored value:
```typescript theme={null}
await cofhe.mocks.expectPlaintext(await counter.read.count(), 1n);
```
A mismatch throws an `Error` whose message names both values:
```text theme={null}
mock_expectPlaintext: expected 2, got 1 for ctHash 0xf4b9...0400
```
On a network without the mocks, `getPlaintext` throws, but `expectPlaintext` prints a message and returns without asserting anything. A test that relies on `expectPlaintext` passes silently against a testnet.
## Redeploy the mocks
`cofhe.mocks.deployMocks(options?)` runs the deployment again on the current chain:
```typescript theme={null}
await cofhe.mocks.deployMocks({ gasWarning: false, mocksDeployVerbosity: '' });
```
| Option | Type | Default |
| - | - | - |
| `gasWarning` | `boolean` | `true` |
| `mocksDeployVerbosity` | `'' \| 'v' \| 'vv'` | `'v'` |
These defaults apply to the call itself. `deployMocks` does not read the `cofhe` block of your Hardhat config, so a bare call prints the gas warning even when the config turns it off.
A redeploy is not a reset. It deploys a new `MockACL` and points `MockTaskManager` at it, but `cofhe.mocks.MockACL` keeps the old address, and handles created before the redeploy lose their permissions. The next FHE operation on one of those handles reverts with `ACLNotAllowed` (selector `0x4d13139e`). For a clean state, open a new connection with `network.create()` instead.
# Testing
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/hardhat-3-plugin/testing
A complete Hardhat 3 test for an FHE contract, and the patterns around it
This page builds one test file for an encrypted counter. It then covers the patterns you add as a contract grows: plaintext assertions, ACPs (Access Control Permissions), and decryption for a transaction. It assumes the setup from [Getting Started](/cofhe/client-sdk/hardhat-3-plugin/getting-started).
## The contract
The examples use this contract. It accepts an encrypted input, increments the stored value, and can publish a decryption result onchain.
```solidity contracts/Counter.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import '@fhenixprotocol/cofhe-contracts/FHE.sol';
contract Counter {
euint32 public count;
function setCount(externalEuint32 inValue, bytes calldata inputProof) external {
count = FHE.asEuint32(inValue, inputProof);
FHE.allowThis(count);
FHE.allowSender(count);
}
function increment() external {
count = FHE.add(count, FHE.asEuint32(1));
FHE.allowThis(count);
FHE.allowSender(count);
}
function revealCount() external {
FHE.allowPublic(count);
}
function publishCount(uint32 plaintext, bytes calldata signature) external {
FHE.publishDecryptResult(count, plaintext, signature);
}
}
```
## Encrypt, store, and decrypt
The core test loop encrypts a value, sends it to the contract, and decrypts the stored handle:
```typescript test/Counter.test.ts theme={null}
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { network } from 'hardhat';
import { Encryptable, FheTypes } from '@cofhe/sdk';
describe('Counter', async () => {
const { viem, cofhe } = await network.create();
const [walletClient] = await viem.getWalletClients();
it('stores and decrypts an encrypted count', async () => {
const counter = await viem.deployContract('Counter');
const client = await cofhe.createClientWithBatteries(walletClient);
// The signature is bound to the contract that consumes the input
const [countHash, signature] = await client
.encryptInputs([Encryptable.uint32(42n)])
.setConsumingContract(counter.address)
.execute();
await counter.write.setCount([countHash, signature]);
const ctHash = await counter.read.count();
const count = await client.decryptForView(ctHash, FheTypes.Uint32).execute();
assert.equal(count, 42n);
});
});
```
Run it:
```bash theme={null}
npx hardhat test
```
The output lists the `stores and decrypts an encrypted count` test under `Counter`, and the summary reads `1 passing (1 nodejs)`.
The `describe` callback is `async` so that it can `await network.create()` at the top. Every `it` inside it shares that connection and its mocks, without a `before` hook.
## Start each test from a clean chain
A connection is one chain, so tests in the same `describe` see each other's state. When a test needs a clean chain, open a new connection inside it:
```typescript theme={null}
it('starts from a clean chain', async () => {
const { viem, cofhe } = await network.create();
const counter = await viem.deployContract('Counter');
// ...
});
```
Each `network.create()` call starts a new simulated chain and deploys a new set of mocks. Contracts from other connections do not exist on it.
## Assert on plaintext
Reading the plaintext from the mocks is faster than a decryption and needs no ACP:
```typescript theme={null}
await counter.write.increment();
await cofhe.mocks.expectPlaintext(await counter.read.count(), 43n);
```
Use `decryptForView` when the SDK's own behavior is under test. See [Mock contracts](/cofhe/client-sdk/hardhat-3-plugin/mock-contracts) for both plaintext helpers.
## Named ACPs
`createClientWithBatteries` creates and selects a self ACP. For a test that needs a second ACP, create it and select it:
```typescript theme={null}
import { ACPUtils } from '@cofhe/sdk/acps';
const acp = await client.acp.createSelf({
issuer: walletClient.account.address,
name: 'Counter test ACP',
});
client.acp.selectActiveACP(ACPUtils.getHash(acp));
```
For several signers, create one client per wallet, as shown on the [Client](/cofhe/client-sdk/hardhat-3-plugin/client#several-signers) page.
## Decrypt for a transaction
[`decryptForTx`](/cofhe/client-sdk/guides/decrypt-to-tx) resolves to `{ ctHash, decryptedValue, signature }`. Choose the ACP mode before `execute()`.
### Public handles
After the contract calls `FHE.allowPublic`, anyone can decrypt the handle without an ACP:
```typescript theme={null}
await counter.write.increment();
await counter.write.revealCount();
const result = await client
.decryptForTx(await counter.read.count())
.withoutACP()
.execute();
assert.equal(result.decryptedValue, 1n);
```
On a handle that is not public, `withoutACP()` rejects with `mocks decryptForTx call failed: NotAllowed`.
### Handles restricted by the ACL
Pass an ACP, or call `withACP()` with no argument to use the active one:
```typescript Active ACP theme={null}
const result = await client
.decryptForTx(ctHash)
.withACP()
.execute();
```
```typescript Explicit ACP theme={null}
const result = await client
.decryptForTx(ctHash)
.withACP(acp)
.execute();
```
### Publish the result onchain
Send the value and the signature to the contract, which verifies and stores them with `FHE.publishDecryptResult`:
```typescript theme={null}
await counter.write.publishCount([Number(result.decryptedValue), result.signature]);
```
`decryptedValue` is a `bigint`. viem expects a `number` for a `uint32` argument, so convert it first.
## Common pitfalls
`HHE1000: Artifact for contract "MockTaskManager" not found` means the mock contracts are missing from `solidity.npmFilesToBuild`. Add them as shown in [Getting Started](/cofhe/client-sdk/hardhat-3-plugin/getting-started#install-the-plugin).
Error `TS2345` on `createClientWithBatteries(walletClient)` or `client.connect(...)` means two copies of viem are installed. Add the `viem` override from [Getting Started](/cofhe/client-sdk/hardhat-3-plugin/getting-started#install-the-plugin) and reinstall from a clean `node_modules`.
The input signature binds the encrypting account and the consuming contract. The call reverts when a different wallet sends the transaction, or when `setConsumingContract` named a different contract. viem reports the revert by its selector, `0x7ba5ffb5`, which is `InvalidSigner(address,address)`.
The SDK types only offer `execute()` after `setConsumingContract()`, so TypeScript reports `Property 'execute' does not exist`. At runtime, the same omission throws `CONSUMING_CONTRACT_UNINITIALIZED`.
The contract did not call `FHE.allowThis` on a stored handle, so its next transaction cannot use it. viem reports the revert by its selector, `0x4d13139e`, which is `ACLNotAllowed(uint256,address)`. Wrap the call in [`withLogs`](/cofhe/client-sdk/hardhat-3-plugin/logging) to see which grants each operation made.
## Related
* [Hardhat 2 plugin testing](/cofhe/client-sdk/hardhat-plugin/testing): the same patterns with Mocha and ethers.
* [Decrypt to transact](/cofhe/client-sdk/guides/decrypt-to-tx): the full `decryptForTx` guide.
# Client
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/hardhat-plugin/client
Create and connect a CofheClient in Hardhat tests
The plugin extends `hre` with `hre.cofhe`, providing three ways to create and connect a `CofheClient` in your Hardhat tests.
## Batteries included (recommended)
`hre.cofhe.createClientWithBatteries(signer?)` is the one-liner that handles everything:
1. Creates a CoFHE config with `environment: 'hardhat'` and `supportedChains: [hardhat]`
2. Creates a `CofheClient`
3. Connects it using the provided Hardhat signer (defaults to the first signer)
4. Generates a self-usage ACP for the signer
```typescript theme={null}
import hre from 'hardhat';
const [signer] = await hre.ethers.getSigners();
const cofheClient = await hre.cofhe.createClientWithBatteries(signer);
cofheClient.connected; // true
```
`createClientWithBatteries` generates a self-ACP automatically, so most test operations (encrypting inputs, decrypting for view, decrypting for tx) work immediately without any extra setup.
If `signer` is omitted, the first signer from `hre.ethers.getSigners()` is used.
## Manual setup
For more control (custom config options, multiple signers, or adjusting `encryptDelay`) you can set up the client step by step.
```typescript theme={null}
import hre from 'hardhat';
import { hardhat } from '@cofhe/sdk/chains';
const config = await hre.cofhe.createConfig({
supportedChains: [hardhat],
});
```
`hre.cofhe.createConfig` wraps `createCofheConfig` from `@cofhe/sdk/node` with two Hardhat-specific additions:
* Sets `environment: 'hardhat'` automatically
* Defaults `mocks.encryptDelay` to `0` so tests run without artificial wait times
```typescript theme={null}
const cofheClient = hre.cofhe.createClient(config);
```
```typescript theme={null}
await hre.cofhe.connectWithHardhatSigner(cofheClient, signer);
```
`connectWithHardhatSigner` uses `HardhatSignerAdapter` under the hood to convert a `HardhatEthersSigner` into the viem `PublicClient` + `WalletClient` pair that the SDK expects.
## Low-level adapter
If you need direct access to the underlying viem clients, call the adapter directly:
```typescript theme={null}
import hre from 'hardhat';
const [signer] = await hre.ethers.getSigners();
const { publicClient, walletClient } =
await hre.cofhe.hardhatSignerAdapter(signer);
await cofheClient.connect(publicClient, walletClient);
```
## Using the client
Once connected, the client works identically to the standard SDK client. See:
* [Encrypting Inputs](/cofhe/client-sdk/guides/encrypting-inputs)
* [Decrypt to View](/cofhe/client-sdk/guides/decrypt-to-view)
* [Decrypt to Transact](/cofhe/client-sdk/guides/decrypt-to-tx)
* [Access Control Permissions](/cofhe/client-sdk/guides/acps)
# Getting Started
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/hardhat-plugin/getting-started
Set up @cofhe/hardhat-plugin for local FHE contract development and testing
`@cofhe/hardhat-plugin` extends Hardhat with everything you need to build and test FHE-enabled contracts locally: mock contracts, a pre-configured CoFHE client, and test utilities.
## What the plugin provides
* **Mock contracts** deployed automatically on every `npx hardhat test` / `npx hardhat node` run, simulating the full CoFHE coprocessor stack on the Hardhat network.
* **`hre.cofhe`**: a namespaced API for creating and connecting CoFHE clients with Hardhat signers.
* **`hre.cofhe.mocks`**: utilities for reading raw plaintext values and interacting with mock contracts directly in tests.
* **Pre-configured networks**: `localcofhe`, `eth-sepolia`, and `arb-sepolia` are injected automatically.
## Installation
```bash npm theme={null}
npm install @cofhe/hardhat-plugin @cofhe/sdk @cofhe/mock-contracts @fhenixprotocol/cofhe-contracts
```
```bash pnpm theme={null}
pnpm add @cofhe/hardhat-plugin @cofhe/sdk @cofhe/mock-contracts @fhenixprotocol/cofhe-contracts
```
```bash yarn theme={null}
yarn add @cofhe/hardhat-plugin @cofhe/sdk @cofhe/mock-contracts @fhenixprotocol/cofhe-contracts
```
```typescript hardhat.config.ts theme={null}
import '@cofhe/hardhat-plugin';
export default {
solidity: '0.8.28',
};
```
That's it. The plugin automatically deploys the mock contracts before every test run.
## Configuration
The plugin adds an optional `cofhe` key to your Hardhat config:
```typescript hardhat.config.ts theme={null}
import '@cofhe/hardhat-plugin';
export default {
solidity: '0.8.28',
cofhe: {
gasWarning: true, // warn when mock gas usage is high (default: false)
mocksDeployVerbosity: 'v', // mock deployment log verbosity (default: 'v')
},
};
```
`mocksDeployVerbosity` controls how much output the plugin prints while deploying the mock contracts at the start of each `npx hardhat test` / `npx hardhat node` run:
| Value | Output |
| - | - |
| `''` | Silent, no deploy lines. |
| `'v'` | Single summary line per mock (default since `0.5.0`). |
| `'vv'` | Full per-contract deployment log, including each contract's deployed address. |
This is separate from **runtime** FHE-op logging, the entries printed when your tests actually call `FHE.*`. That is controlled onchain rather than from this config; see [Logging](/cofhe/client-sdk/hardhat-plugin/logging).
## Pre-configured networks
The following networks are injected automatically. You can override any of them by defining the same key under `networks` in your config.
| Network | URL | Chain ID |
| - | - | - |
| `localcofhe` | `http://127.0.0.1:42069` | `420105` |
| `eth-sepolia` | Ethereum Sepolia public RPC | `11155111` |
| `arb-sepolia` | Arbitrum Sepolia public RPC | `421614` |
For testnets, set `PRIVATE_KEY` (and optionally `SEPOLIA_RPC_URL` / `ARBITRUM_SEPOLIA_RPC_URL`) in your environment.
## Auto-deployment
Mock contracts are deployed automatically before:
* `npx hardhat test`
* `npx hardhat node`
To skip auto-deployment (e.g., when running tests only against an external RPC):
```bash theme={null}
COFHE_SKIP_MOCKS_DEPLOY=1 npx hardhat test
```
## Next steps
* [Client](/cofhe/client-sdk/hardhat-plugin/client): create and connect a CoFHE client in tests.
* [Mock Contracts](/cofhe/client-sdk/hardhat-plugin/mock-contracts): read plaintext values and interact with mock contracts.
* [Logging](/cofhe/client-sdk/hardhat-plugin/logging): inspect FHE operations in test output.
* [Testing](/cofhe/client-sdk/hardhat-plugin/testing): end-to-end test patterns.
# Logging
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/hardhat-plugin/logging
Inspect FHE operations in your Hardhat test output
The mock contracts log every FHE operation to the console. This shows exactly which operations your contracts perform during tests.
## What gets logged
Each FHE operation emits a formatted log entry showing the operation name, the input and output operand hashes (truncated), and the security zone:
```text theme={null}
┌──────────────────┬──────────────────────────────────────────────────
│ [COFHE-MOCKS] │ "counter.increment()" logs:
├──────────────────┴──────────────────────────────────────────────────
├ FHE.add | euint32(4473..3424)[0] + euint32(1157..3648)[1] => euint32(1106..1872)[1]
├ FHE.allowThis | euint32(1106..1872)[1] -> 0x663f..6602
├ FHE.allow | euint32(1106..1872)[1] -> 0x3c44..93bc
└─────────────────────────────────────────────────────────────────────
```
## `withLogs(name, fn)` (recommended)
Wraps a block of code with logging enabled and prints a labeled box around the output. The `name` appears as the header so you can identify which call produced which operations.
```typescript theme={null}
import hre from 'hardhat';
await hre.cofhe.mocks.withLogs('counter.increment()', async () => {
await counter.increment();
});
```
`withLogs` enables logging before the closure runs and disables it after, so only operations from within that block appear in the output.
## `enableLogs()` / `disableLogs()` (manual)
For finer-grained control, you can enable and disable logging manually:
```typescript theme={null}
import hre from 'hardhat';
await hre.cofhe.mocks.enableLogs('counter.increment()');
await counter.increment();
await hre.cofhe.mocks.disableLogs();
```
`enableLogs` accepts an optional label string. If provided, it prints a labeled header immediately, useful when you want to mark a section of output at a known point.
## Default logging behavior
Logging is **enabled by default**: `MockTaskManager` ships with its `logOps` flag set to `true`, so every FHE operation prints from the moment the mocks are deployed.
The flag lives onchain, not in your Hardhat config. Every control below is a transaction that flips `logOps` on the deployed `MockTaskManager`, which is why the setting resets each time the mocks are redeployed.
To silence logs for a whole test suite, disable them once in a global setup hook:
```typescript test/setup.ts theme={null}
import hre from 'hardhat';
before(async () => {
await hre.cofhe.mocks.disableLogs();
});
```
Or toggle logging for a specific run using the Hardhat task:
```bash theme={null}
npx hardhat task:cofhe-mocks:setlogops --enable true
npx hardhat task:cofhe-mocks:setlogops --enable false
```
There is no Hardhat config option for this. The plugin's config type still accepts a `cofhe.logMocks` boolean, but no plugin code reads it. Setting it has no effect on FHE-op logging. Use `disableLogs()` or the `setlogops` task instead.
# Mock Contracts
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/hardhat-plugin/mock-contracts
Interact with mock CoFHE contracts and read plaintext values in tests
The plugin deploys a suite of mock contracts that simulate the full CoFHE coprocessor stack on the Hardhat network. This lets you develop and test FHE contracts without running the offchain FHE engine.
## What the mocks simulate
| Contract | Role |
| - | - |
| MockCoFHEAddressBook | Resolves the TaskManager, at the same address `FHE.sol` reads on real networks |
| `MockTaskManager` | Manages FHE operations; stores plaintext values onchain for testing |
| `MockACL` | Access control for encrypted handles |
| `MockZkVerifier` | Simulates ZK proof verification for encrypted inputs |
| `MockThresholdNetwork` | Handles decryption requests |
The SDK automatically detects when it's running against the mock environment (by checking bytecode at the `MockZkVerifier` fixed address) and adapts its behavior accordingly. ZK proof generation is skipped and verification is handled by the mock contracts.
## Auto-deployment
Mock contracts are deployed automatically before every `npx hardhat test` and `npx hardhat node` run.
To skip auto-deployment:
```bash theme={null}
COFHE_SKIP_MOCKS_DEPLOY=1 npx hardhat test
```
You only need `COFHE_SKIP_MOCKS_DEPLOY=1` if your tests exclusively target an external RPC and don't use the in-process Hardhat network at all.
You can also deploy mock contracts manually via the Hardhat task:
```bash theme={null}
npx hardhat task:cofhe-mocks:deploy
```
The task takes no parameters. To control how much it prints, set `mocksDeployVerbosity` in your Hardhat config (`''` for silent, `'v'` for a summary line, `'vv'` for full per-contract logs).
Or programmatically from a test or script:
```typescript theme={null}
import hre from 'hardhat';
await hre.cofhe.mocks.deployMocks();
```
## Accessing mock contracts
`hre.cofhe.mocks` exposes typed accessors for each mock contract:
```typescript theme={null}
import hre from 'hardhat';
const taskManager = await hre.cofhe.mocks.getMockTaskManager();
const acl = await hre.cofhe.mocks.getMockACL();
const thresholdNetwork = await hre.cofhe.mocks.getMockThresholdNetwork();
const zkVerifier = await hre.cofhe.mocks.getMockZkVerifier();
```
## Reading plaintext values
Because `MockTaskManager` stores plaintext values onchain, you can read the underlying plaintext of any encrypted handle directly in tests, no ACP needed.
### `getPlaintext(ctHash)`
Returns the plaintext `bigint` for a given ciphertext hash:
```typescript theme={null}
import hre from 'hardhat';
import { expect } from 'chai';
// Your own contract, deployed from your own artifacts
const counter = await hre.ethers.deployContract('Counter');
await counter.increment();
// A getter on your contract that returns the encrypted handle
const ctHash = await counter.countHash();
const plaintext = await hre.cofhe.mocks.getPlaintext(ctHash);
expect(plaintext).to.equal(1n);
```
### `expectPlaintext(ctHash, expectedValue)`
Assertion shorthand. Wraps `getPlaintext` with a Chai `expect`:
```typescript theme={null}
import hre from 'hardhat';
const counter = await hre.ethers.deployContract('Counter');
await counter.increment();
const ctHash = await counter.countHash();
await hre.cofhe.mocks.expectPlaintext(ctHash, 1n);
```
`getPlaintext` and `expectPlaintext` only work on the Hardhat network (where `MockTaskManager` stores plaintexts). They will throw on `localcofhe` or any real network.
# Testing
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/hardhat-plugin/testing
Common patterns for writing Hardhat tests with the CoFHE plugin
This page shows the common patterns for writing Hardhat tests with the CoFHE plugin.
## Test setup
Use `hre.cofhe.createClientWithBatteries` in a `before` hook. It creates and connects a fully configured `CofheClient`, including a self-ACP, so the client is ready for every test in the suite:
```typescript theme={null}
import hre from 'hardhat';
import { CofheClient } from '@cofhe/sdk';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
let cofheClient: CofheClient;
let signer: HardhatEthersSigner;
before(async () => {
[signer] = await hre.ethers.getSigners();
cofheClient = await hre.cofhe.createClientWithBatteries(signer);
});
```
See [Client](/cofhe/client-sdk/hardhat-plugin/client) for manual setup options.
## Encrypt to store to decrypt
The core test loop: encrypt a value, pass it to a contract, then decrypt the stored handle.
```typescript theme={null}
import { Encryptable, FheTypes } from '@cofhe/sdk';
import { expect } from 'chai';
// 1. Encrypt the input, bound to the contract that will consume it
const [valueHash, signature] = await cofheClient
.encryptInputs([Encryptable.uint32(100n)])
.setConsumingContract(await testContract.getAddress())
.execute();
// 2. Send to contract
const tx = await testContract.setValue(valueHash, signature);
await tx.wait();
// 3. Read the stored handle
const ctHash = await testContract.storedValue();
// 4. Decrypt for display
const decrypted = await cofheClient
.decryptForView(ctHash, FheTypes.Uint32)
.execute();
expect(decrypted).to.equal(100n);
```
## Reading plaintext directly
In tests you can bypass the normal decrypt flow and read the raw plaintext stored by the mock contracts. This is useful for asserting contract state without needing an ACP:
```typescript theme={null}
import hre from 'hardhat';
// Get raw plaintext value
const plaintext = await hre.cofhe.mocks.getPlaintext(ctHash);
// Or use the assertion shorthand
await hre.cofhe.mocks.expectPlaintext(ctHash, 100n);
```
See [Mock Contracts](/cofhe/client-sdk/hardhat-plugin/mock-contracts) for details.
## Access Control Permissions
`createClientWithBatteries` pre-generates a self-ACP, so `decryptForView` and `decryptForTx().withACP()` work immediately. For tests that need named ACPs or multiple signers, create them explicitly:
```typescript theme={null}
import { ACPUtils } from '@cofhe/sdk/acps';
const acp = await cofheClient.acp.createSelf({
issuer: signer.address,
name: 'My Test ACP',
});
// Select it as the active ACP
const acpHash = ACPUtils.getHash(acp);
cofheClient.acp.selectActiveACP(acpHash);
```
Alternatively, create a separate client for each signer:
```typescript theme={null}
import hre from 'hardhat';
const [bob, alice] = await hre.ethers.getSigners();
const bobClient = await hre.cofhe.createClientWithBatteries(bob);
const aliceClient = await hre.cofhe.createClientWithBatteries(alice);
```
## `decryptForTx` patterns
[`decryptForTx`](/cofhe/client-sdk/guides/decrypt-to-tx) returns a `{ ctHash, decryptedValue, signature }` tuple for onchain submission. The ACP mode must be selected explicitly.
### Globally allowed values (`.withoutACP()`)
When a contract calls `FHE.allowPublic(handle)`, anyone can decrypt without an ACP:
```typescript theme={null}
import { expect } from 'chai';
const result = await cofheClient
.decryptForTx(publicCtHash)
.withoutACP()
.execute();
expect(result.decryptedValue).to.equal(55n);
```
### Access-controlled values (`.withACP()`)
For handles restricted by ACL policy, supply an ACP:
```typescript Explicit ACP theme={null}
const result = await cofheClient
.decryptForTx(ctHash)
.withACP(acp)
.execute();
expect(result.decryptedValue).to.equal(99n);
```
```typescript Active ACP theme={null}
// resolves the active ACP automatically
const result = await cofheClient
.decryptForTx(ctHash)
.withACP()
.execute();
expect(result.decryptedValue).to.equal(99n);
```
### Submitting the result onchain
Pass the result directly to your contract:
```typescript theme={null}
await myContract.revealValue(
result.ctHash,
result.decryptedValue,
result.signature
);
```
For a full walkthrough of `decryptForTx`, see [Decrypt to Transact](/cofhe/client-sdk/guides/decrypt-to-tx).
# Installation
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/introduction/installation
Install and configure @cofhe/sdk for your project
## Prerequisites
* Node.js 18+
* TypeScript 5+
* viem 2.38.6+
## Install packages
```bash npm theme={null}
npm install @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
```bash pnpm theme={null}
pnpm add @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
```bash yarn theme={null}
yarn add @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
| Package | Version | Purpose |
| - | - | - |
| `@cofhe/sdk` | `^0.8.0` | Client-side encryption, decryption, and ACP management |
| `@fhenixprotocol/cofhe-contracts` | `^1.0.0` | `FHE.sol`, the Solidity library imported by your contracts |
`@cofhe/sdk` `0.7.x` pairs with `@fhenixprotocol/cofhe-contracts` `0.2.0`. Keep every `@cofhe/*` package on the same version. They are released together and pin each other exactly. The [compatibility page](/cofhe/get-started/introduction/compatibility) carries the full table.
## For Hardhat projects
If you are using Hardhat for development and testing, also install the plugin:
```bash npm theme={null}
npm install @cofhe/hardhat-plugin@^0.8.0 @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
```bash pnpm theme={null}
pnpm add @cofhe/hardhat-plugin@^0.8.0 @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
```bash yarn theme={null}
yarn add @cofhe/hardhat-plugin@^0.8.0 @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
| Package | Version | Purpose |
| - | - | - |
| `@cofhe/hardhat-plugin` | `^0.8.0` | Extends Hardhat with `hre.cofhe`, deploys mock contracts automatically |
| `@cofhe/sdk` | `^0.8.0` | Client-side encryption, decryption, and ACP management |
| `@fhenixprotocol/cofhe-contracts` | `^1.0.0` | `FHE.sol`, the Solidity library imported by your contracts |
See the [Hardhat Plugin Getting Started](/cofhe/client-sdk/hardhat-plugin/getting-started) guide for configuration details.
## For Foundry projects
If you are using Foundry for development and testing, install the Foundry plugin and its peer packages as dev dependencies:
```bash npm theme={null}
npm install -D @cofhe/foundry-plugin@^0.8.0 @cofhe/mock-contracts@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0 @openzeppelin/contracts
forge install foundry-rs/forge-std
```
```bash pnpm theme={null}
pnpm add -D @cofhe/foundry-plugin@^0.8.0 @cofhe/mock-contracts@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0 @openzeppelin/contracts
forge install foundry-rs/forge-std
```
```bash yarn theme={null}
yarn add -D @cofhe/foundry-plugin@^0.8.0 @cofhe/mock-contracts@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0 @openzeppelin/contracts
forge install foundry-rs/forge-std
```
| Package | Version | Purpose |
| - | - | - |
| `@cofhe/foundry-plugin` | `^0.8.0` | `CofheTest` and `CofheClient`, the Solidity test base and per-user SDK shim |
| `@cofhe/mock-contracts` | `^0.8.0` | Mock CoFHE contracts used by the Foundry plugin |
| `@fhenixprotocol/cofhe-contracts` | `^1.0.0` | `FHE.sol`, the Solidity library imported by your contracts |
The Foundry plugin uses Solidity-only abstractions, so tests need no `@cofhe/sdk` (JS).
See the [Foundry Plugin Getting Started](/cofhe/client-sdk/foundry-plugin/getting-started) guide for `foundry.toml` and `remappings.txt` setup.
## Runtime entrypoints
Import from the entrypoint that matches your runtime:
```typescript theme={null}
// Browser apps (React, Next.js, etc.)
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web';
// Node.js scripts, backends, Hardhat tests
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/node';
// Shared types (works in any runtime)
import { Encryptable, FheTypes } from '@cofhe/sdk';
```
## Next steps
* [Quick start](/cofhe/client-sdk/quick-start/javascript): write your first FHE contract and test.
* [Client setup](/cofhe/client-sdk/guides/client-setup): configure and connect the SDK client.
# Mental Model
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/introduction/mental-model
Understand how data flows through FHE-enabled dApps using the @cofhe/sdk
To understand how `@cofhe/sdk` fits into the Fhenix framework, you'll explore a mental model using a Counter smart contract example. This will show you how data flows through FHE-enabled dApps, from encryption to computation to decryption.
## The counter example
Imagine a smart contract called **Counter** where each user has their own private counter. Users can increment their counter and read its value with complete privacy, no one, including the smart contract itself, can see the actual counter values.
### Key concepts
* **Public Key** = A lock that anyone can use to seal data
* **Private Key** = The unique key to unlock sealed data
* **CoFHE** = Fhenix's offchain coprocessor, which runs the FHE operations
* **Ciphertext** = Encrypted data that can be computed on without decryption
When a user wants to add `5` to their counter, your app encrypts the data before it is sent to the contract.
**What happens:**
1. The user's plaintext value `5` is encrypted using `client.encryptInputs([Encryptable.uint32(5n)]).setConsumingContract(address).execute()`
2. The SDK generates a ZK proof and submits the encrypted value to CoFHE for verification
3. The returned ciphertext handle and its batch signature are sent to the smart contract onchain
4. The host chain sees only encrypted data, never the actual value `5`
### The "locked box" analogy
Think of this as placing the value `5` in a box and locking it with the CoFHE coprocessor's public key. The locked box (ciphertext) can be sent to the smart contract, but no one can see what's inside without the private key.
Once the encrypted data reaches the smart contract, FHE magic happens. The smart contract can perform arithmetic operations directly on the ciphertext without ever decrypting it.
**What happens:**
1. The smart contract receives the encrypted value
2. It retrieves the user's encrypted counter from storage (also encrypted)
3. Using FHE operations, it adds the encrypted values together
4. The result is stored as encrypted data
5. **Critical**: At no point does your contract, the host chain, or anyone else see the actual numbers
### FHE computation magic
This is where Fully Homomorphic Encryption shines. The CoFHE coprocessor enables the smart contract to:
* Add encrypted values together
* Compare encrypted values
* Perform other arithmetic operations
* All while the data remains encrypted and private
When a user wants to read their counter value, they use one of the SDK's two decryption methods depending on their goal.
**For UI display (`decryptForView`):**
1. The user reads the encrypted handle (`ctHash`) from the contract
2. An ACP authorizes decryption. Created via `client.acp.getOrCreateSelfACP()`
3. The SDK asks [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) to decrypt the handle and seal the result to the ACP's sealing key
4. The plaintext is returned locally for display
**For onchain use (`decryptForTx`):**
1. The user reads the encrypted handle (`ctHash`) from the contract
2. The SDK requests decryption from Teecryptor
3. The plaintext and a verifiable signature are returned
4. The signature can be verified onchain via `FHE.verifyDecryptResult(...)`
### The "lock exchange" analogy
This is like exchanging locks on the box:
* The box starts locked with the CoFHE coprocessor's lock
* The user sends their own lock to the coprocessor (via the ACP's sealing key)
* The coprocessor removes its lock and applies the user's lock
* The box remains locked throughout, but now only the user can open it
* The data remains private at every step
## The complete flow
Color marks the zone. Your app and the Client SDK run client side, your contract and the host chain onchain, and CoFHE offchain. Hover any component or message in the [explorable version](/cofhe/client-sdk/introduction/mental-model-explorable).
## Key takeaways
1. **Encryption happens client-side**: The SDK encrypts data with ZK proofs before it reaches the host chain
2. **Computation happens onchain**: Smart contracts perform operations on encrypted data via `FHE.sol`
3. **FHE enables privacy-preserving computation**: The host chain never sees plaintext values
4. **ACPs enable access control**: EIP-712 signed ACPs authorize who can decrypt specific data
5. **Two decryption paths**: `decryptForView` for UI display, `decryptForTx` for onchain verification
This architecture ensures that sensitive data remains private throughout its entire lifecycle, from input to computation to output, while still enabling decentralized applications.
# Migrating from cofhejs
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/introduction/migrating-from-cofhejs
Side-by-side migration guide from cofhejs to @cofhe/sdk
`@cofhe/sdk` is the successor to `cofhejs`, redesigned around an explicit, builder-pattern API that gives you full control over encryption, decryption, and ACP management.
## Why migrate?
* **Explicit API**: no more implicit initialization or auto-generated ACPs. Every action is opt-in.
* **Builder pattern**: `encryptInputs`, `decryptForView`, and `decryptForTx` use a chainable builder so you can set overrides (account, chain, callbacks) before calling `.execute()`.
* **`decryptForTx` feature**: `cofhejs` does not provide an API for generating decryption signatures for onchain usage.
* **Deferred key loading**: FHE keys and TFHE WASM are fetched lazily on the first `encryptInputs` call, not during initialization.
* **Better multichain support**: configure multiple chains up front and override per-call.
* **Structured errors**: typed `CofheError` objects with error codes replace the `Result` wrapper.
## Requirements
* Node.js 18+
* TypeScript 5+
* Viem 2+
## Installation
Remove `cofhejs` and install `@cofhe/sdk`:
```bash theme={null}
npm uninstall cofhejs && npm install @cofhe/sdk
```
## 1. Initialization
The single `cofhejs.initializeWithEthers(...)` / `cofhejs.initializeWithViem(...)` call is replaced by a three-step flow: create a config, create a client, then connect. FHE keys and WASM are no longer fetched eagerly during init, they are deferred until the first `encryptInputs` call.
| | `cofhejs` | `@cofhe/sdk` |
| - | - | - |
| **Entry** | `cofhejs.initializeWithEthers(...)` / `cofhejs.initializeWithViem(...)` | `createCofheConfig(...)` to `createCofheClient(config)` to `client.connect(...)` |
| **Key fetching** | Immediate (during init) | Deferred (first `encryptInputs` call) |
| **WASM init** | Immediate (during init) | Deferred (first `encryptInputs` call) |
| **Environment** | `"LOCAL"` / `"TESTNET"` / `"MAINNET"` string | Chain objects via `supportedChains: [chains.sepolia]` |
| **Provider format** | Ethers provider/signer or viem clients | Always viem clients (use adapters for ethers) |
### Before (cofhejs)
```typescript Ethers theme={null}
import { cofhejs } from 'cofhejs/node';
await cofhejs.initializeWithEthers({
ethersProvider: provider,
ethersSigner: signer,
environment: 'TESTNET',
});
```
```typescript Viem theme={null}
import { cofhejs } from 'cofhejs/web';
await cofhejs.initializeWithViem({
viemClient: publicClient,
viemWalletClient: walletClient,
environment: 'TESTNET',
});
```
### After (@cofhe/sdk)
```typescript Web (viem) theme={null}
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web';
import { chains } from '@cofhe/sdk/chains';
const config = createCofheConfig({
supportedChains: [chains.sepolia],
});
const client = createCofheClient(config);
await client.connect(publicClient, walletClient);
```
```typescript Node (viem) theme={null}
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/node';
import { chains } from '@cofhe/sdk/chains';
const config = createCofheConfig({
supportedChains: [chains.sepolia],
});
const client = createCofheClient(config);
await client.connect(publicClient, walletClient);
```
```typescript Ethers v6 (via adapter) theme={null}
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web';
import { Ethers6Adapter } from '@cofhe/sdk/adapters';
import { chains } from '@cofhe/sdk/chains';
const config = createCofheConfig({
supportedChains: [chains.sepolia],
});
const client = createCofheClient(config);
const { publicClient, walletClient } = await Ethers6Adapter(
provider,
signer
);
await client.connect(publicClient, walletClient);
```
## 2. Encrypting inputs
`cofhejs.encrypt(...)` is replaced by a builder: `client.encryptInputs([...]).setConsumingContract(address).execute()`. `setConsumingContract` is required, because the verifier binds that address into the batch signature.
| | `cofhejs` | `@cofhe/sdk` |
| - | - | - |
| **Function** | `cofhejs.encrypt([...], callback)` | `client.encryptInputs([...]).setConsumingContract(addr).execute()` |
| **Return value** | `Result` with `.success` / `.data` / `.error` | Direct value (throws `CofheError` on failure) |
| **Progress callback** | Second argument to `encrypt` | `.onStep(callback)` on the builder |
| **Overrides** | Not available | `.setAccount(...)`, `.setChainId(...)`, `.setUseWorker(...)` |
### Before (cofhejs)
```typescript theme={null}
import { cofhejs, Encryptable } from 'cofhejs/node';
const result = await cofhejs.encrypt(
[Encryptable.uint64(42n), Encryptable.bool(true)],
(state) => console.log(state)
);
if (!result.success) {
console.error(result.error);
return;
}
const [eAmount, eFlag] = result.data;
```
### After (@cofhe/sdk)
```typescript theme={null}
import { Encryptable, EncryptStep } from '@cofhe/sdk';
const [amountHash, flagHash, signature] = await client
.encryptInputs([Encryptable.uint64(42n), Encryptable.bool(true)])
.setConsumingContract(contractAddress)
.onStep((step, ctx) => {
if (ctx?.isStart) console.log(`Starting: ${step}`);
})
.execute();
```
The `Encryptable` factory functions (`Encryptable.uint32(...)`, `Encryptable.bool(...)`, etc.) work the same way in both libraries.
## 3. Decrypting / Unsealing
`cofhejs` has a single `unseal` function. `@cofhe/sdk` splits decryption into two purpose-built methods:
* **`decryptForView`**: returns the plaintext for UI display (no onchain signature).
* **`decryptForTx`**: returns the plaintext **and** a [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) signature for onchain verification.
| | `cofhejs` | `@cofhe/sdk` |
| - | - | - |
| **Function** | `cofhejs.unseal(sealed, type)` | `client.decryptForView(ctHash, type)` or `client.decryptForTx(ctHash)` |
| **ACP handling** | Automatic (uses most recent permit) | Explicit. `.withACP()` / `.withoutACP()` |
| **Return value** | `Result` | Direct value for view; `{ ctHash, decryptedValue, signature }` for tx |
| **Onchain verification** | Not built in | `decryptForTx` returns a signature for `FHE.publishDecryptResult(...)` |
### Before (cofhejs)
```typescript theme={null}
import { cofhejs, FheTypes } from 'cofhejs/node';
const sealedBalance = await contract.getBalance();
const result = await cofhejs.unseal(sealedBalance, FheTypes.Uint64);
if (!result.success) {
console.error(result.error);
return;
}
console.log(result.data); // bigint
```
### After (@cofhe/sdk): viewing in UI
```typescript theme={null}
import { FheTypes } from '@cofhe/sdk';
const ctHash = await contract.getBalance();
const balance = await client
.decryptForView(ctHash, FheTypes.Uint64)
.execute();
```
### After (@cofhe/sdk): publishing onchain
```typescript TypeScript theme={null}
const ctHash = await myContract.getEncryptedAmount();
const { decryptedValue, signature } = await client
.decryptForTx(ctHash)
.withoutACP()
.execute();
await myContract.publishDecryptResult(ctHash, decryptedValue, signature);
```
```solidity MyContract.sol theme={null}
import '@fhenixprotocol/cofhe-contracts/FHE.sol';
contract MyContract {
euint64 private _encryptedAmount;
function publishDecryptResult(
euint64 ctHash,
uint64 plaintext,
bytes calldata signature
) external {
FHE.publishDecryptResult(ctHash, plaintext, signature);
}
}
```
## 4. Access Control Permissions
ACPs, which `cofhejs` called permits, are no longer auto-generated during initialization. All ACP operations are now explicit through `client.acp`.
| | `cofhejs` | `@cofhe/sdk` |
| - | - | - |
| **Auto-generation** | `generatePermit: true` (default) | Never, always explicit |
| **Creation** | `cofhejs.createPermit({ type, issuer })` | `client.acp.createSelf(...)`, `client.acp.createSharing(...)` |
| **Return type** | `Result` | Direct `ACP` object |
| **Active ACP** | Implicitly used by `unseal` | `getOrCreateSelfACP()` sets active; used automatically by decrypt methods |
### Before (cofhejs)
```typescript theme={null}
await cofhejs.initializeWithEthers({
ethersProvider: provider,
ethersSigner: signer,
environment: 'TESTNET',
// generatePermit: true ← default
});
const result = await cofhejs.createPermit({
type: 'self',
issuer: wallet.address,
});
```
### After (@cofhe/sdk)
```typescript theme={null}
// Create a self ACP (prompts for wallet signature)
const acp = await client.acp.createSelf({
issuer: account,
name: 'My dApp ACP',
});
// Or use the convenience method that creates one only if needed
const acp2 = await client.acp.getOrCreateSelfACP();
// Use with decryptForView (the active ACP is used automatically)
const value = await client
.decryptForView(ctHash, FheTypes.Uint32)
.execute();
```
## 5. Error handling
### Before (cofhejs)
```typescript theme={null}
const result = await cofhejs.encrypt([Encryptable.uint32(42n)]);
if (!result.success) {
console.error('Failed:', result.error); // string
return;
}
const encrypted = result.data;
```
### After (@cofhe/sdk)
```typescript theme={null}
import { isCofheError, CofheErrorCode } from '@cofhe/sdk';
try {
const encrypted = await client
.encryptInputs([Encryptable.uint32(42n)])
.setConsumingContract(contractAddress)
.execute();
} catch (err) {
if (isCofheError(err)) {
console.error(err.code); // CofheErrorCode enum
console.error(err.message); // human-readable message
}
}
```
## 6. Import path changes
| `cofhejs` | `@cofhe/sdk` |
| - | - |
| `cofhejs/node` | `@cofhe/sdk/node` |
| `cofhejs/web` | `@cofhe/sdk/web` |
| N/A | `@cofhe/sdk` (core types, `Encryptable`, `FheTypes`) |
| N/A | `@cofhe/sdk/acps` |
| N/A | `@cofhe/sdk/adapters` |
| N/A | `@cofhe/sdk/chains` |
## 7. Type renames
`cofhejs` had one input type per encrypted value. `@cofhe/sdk` `0.7` has none. `encryptInputs` returns plain ciphertext handles, and the signature that authenticates them covers the whole batch.
| `cofhejs` | `@cofhe/sdk` |
| - | - |
| `CoFheInItem`, `CoFheInBool`, `CoFheInUint8` through `CoFheInAddress` | A ciphertext handle, typed `` `0x${string}` `` |
The `EncryptedItemInput` family that `0.5` and `0.6` used as the replacement was itself removed in `0.7`, along with `asHashPlusProof()`.
# Migrating to 0.7
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/introduction/migrating-to-0-7
Upgrade from 0.6 to 0.7 or later: batch inputs, ACPs, and the new encrypted types
The `0.7` release renames permits to ACPs, replaces per-ciphertext input signatures with one signature per batch, and binds every encrypted input to the contract that will consume it. It also moves you to `@fhenixprotocol/cofhe-contracts` `0.2.0`, which deletes the `InEuintXX` structs. Most projects need Solidity changes, not only TypeScript changes.
There are no deprecation shims. Old names are removed, so the compiler finds most of the work for you.
Some of this migration produces no compile error at all. The [silent changes](#silent-changes) section lists every case where the old code still builds and then misbehaves at runtime. Read it before you declare the upgrade finished.
## Three packages move together
This is one migration across three independently versioned packages. Bumping a subset is the most common way to end up with errors that look like SDK bugs.
| Package | Target | Notes |
| - | - | - |
| `@cofhe/*` (every package) | `0.8.0` | Keep them all on one version |
| `@fhenixprotocol/cofhe-contracts` | `1.0.0` | Use the stable release, not a beta |
| `fhenix-confidential-contracts` | `0.4.0` | Only if you use FHERC20 or confidential tokens |
`fhenix-confidential-contracts` `0.4.0` depends on an exact `@fhenixprotocol/cofhe-contracts@1.0.0`. Leaving a beta pin in place resolves two copies of `FHE.sol`, and `sharedEuint64` from one is not the same type as `sharedEuint64` from the other. The resulting errors read like nonsense, so check the pin first.
If you are coming from `cofhejs` rather than `@cofhe/sdk`, follow [migrating from cofhejs](/cofhe/client-sdk/introduction/migrating-from-cofhejs) first. If you are on `0.5.x`, the Solidity work below is identical, because the `InEuintXX` structs did not change between `0.5` and `0.6`.
## Let the skill do the mechanical work
Most of this migration is mechanical, and the parts that are not deserve a conversation rather than a find and replace. Fhenix publishes an agent skill that drives the whole thing. It detects what your project uses, works in dependency order, shows you a diff before touching anything, and reports what it could not decide for you.
The skill uses the open [Agent Skill](https://agentskills.io/) format, which Claude Code, Cursor, GitHub Copilot, VS Code, Codex, Gemini CLI, OpenCode, Roo, Kiro, and Goose all read. Drop the folder wherever your agent looks for skills, commonly `.claude/skills/` or `.cursor/skills/`:
```bash theme={null}
mkdir -p .claude/skills
curl -L https://github.com/FhenixProtocol/cofhesdk/archive/refs/heads/master.tar.gz \
| tar -xz --strip-components=2 -C .claude/skills \
'*/skills/cofhe-migrate-0-6-to-0-7'
```
You can also [browse the skill on GitHub](https://github.com/FhenixProtocol/cofhesdk/tree/master/skills/cofhe-migrate-0-6-to-0-7) and copy the directory by hand.
Then ask your agent to run it:
```text theme={null}
Migrate this project to @cofhe/sdk 0.8.0
```
It proposes each change as a diff and waits for your approval. Tell it to apply everything if you would rather review at the end.
It stops and asks on the decisions that are genuinely yours:
* A function taking two or more encrypted parameters.
* Encrypted inputs produced in one place and consumed in another.
* A contract you do not control on the other side of a handoff.
* Persisted `EncryptedItemInput` records. They carry per-item signatures, which cannot be rebuilt from a batch signature.
## Work contract-first
If you are doing this by hand, the order matters. The ABI you land on decides what every call site has to look like, so going backwards means rewriting the same call sites twice.
Resolve versions from the registry rather than copying them from a guide. Update the contract dependencies at the same time as the JavaScript ones.
Move off the deleted `InEuintXX` structs, then decide which values cross a contract boundary.
These throw at client construction, so they surface the moment you boot.
Sweeping the renames first makes every remaining TypeScript error a genuine shape problem rather than a missing identifier.
The batch result and the consuming contract land on the same lines, so do them together.
## Contracts: inputs from a user
`0.2.0` deletes the `InEuintXX` structs, the `FHE.asEuint32(InEuint32)` overloads, the `Utils.inputFromEuint32` helpers, and `ITaskManager.verifyInput`. An encrypted value arriving from a user is now an `externalEuintXX` handle plus a `bytes` proof.
```solidity theme={null}
// Before
function setValue(InEuint32 memory inValue) public {
_setStoredValue(FHE.asEuint32(inValue)); // the struct carried its own signature
}
```
```solidity theme={null}
// After
function setValue(externalEuint32 inValue, bytes memory proof) public {
_setStoredValue(FHE.asEuint32(inValue, proof));
}
```
The signature moves out of the struct into a `bytes` parameter that immediately follows the handle it authenticates. This changes the ABI, so a contract coming from `InEuintXX` needs a redeploy.
A contract that already takes `(externalEuint32, bytes)` for a single encrypted value needs no change and no redeploy. `FHE.asEuint32(handle, proof)` verifies that input as a batch of one, so a `0.7` signature works against the ABI you already deployed.
The proof does not have to be the last parameter. It has to follow the `external*` handle as a pair. Extra plain arguments can come after it, which is how ERC-7984's `confidentialTransferAndCall` is shaped.
### More than one encrypted value
One signature now covers all the handles in the batch, computed over them together. You cannot keep two separate `(handle, proof)` pairs, because verifying one handle against a signature covering two reverts.
You can still keep the parameter names, which most projects prefer to an array:
```solidity theme={null}
function transfer(
address to,
externalEuint32 amount,
externalEuint32 fee,
bytes calldata signature
) public {
externalEuint32[] memory packed = new externalEuint32[](2);
packed[0] = amount;
packed[1] = fee;
euint32[] memory values = FHE.asEuint32s(packed, signature);
// values[0] is amount, values[1] is fee
}
```
The array form, `function transfer(address to, externalEuint32[] calldata values, bytes calldata signature)`, is shorter but collapses named parameters into indices. Either way the encrypted parameters must be adjacent, because they share one signature and there is no other way to tell which `bytes` belongs to them.
For a batch mixing types, such as a `euint32` with an `ebool`, call `ITaskManager.batchVerifyInputs` directly.
### Values that cross a contract boundary
`0.2.0` adds a `sharedEuintXX` type for encrypted values passed between contracts. Both directions count: a value handed over as an argument, and a value returned by a function that is not `view`. A `view` function returning an encrypted value is unaffected, because it never granted anything.
This is the part of the migration the compiler cannot help with. The `0.6` spelling, an `FHE.allowTransient` grant plus a bare `euintXX` parameter, still compiles and still runs while both sides stay on it.
It is also the part with a security consequence. A function taking a bare `euintXX` from outside can be turned into an oracle over every ciphertext the contract holds. FHE operations check the permission of the contract performing them, not of whoever called it. An attacker passes a handle the contract is allowed on, such as one read from its own storage, and gets back a value derived from it.
```solidity theme={null}
// Before: permission granted out of band, and the sharer cannot be verified
FHE.allowTransient(amount, address(token));
token.pull(amount);
```
```solidity theme={null}
// After: the type carries the permission, and the sharer is checked
token.pull(FHE.shareEuint64(amount, address(token)));
```
```solidity theme={null}
// On the receiving side
function pull(sharedEuint64 shared) external {
euint64 amount = FHE.receiveEuint64Param(shared);
...
}
```
Both sides have to move in the same change. `pull(euint64)` and `pull(sharedEuint64)` are both `bytes32` on the wire, so an unmigrated caller compiles against a migrated callee and then reverts at runtime with `NotShared`.
Pick the receive form by how the value reached you. `receiveEuint64Param` checks the sharer against `msg.sender` and suits a value that arrived as an argument. `receiveEuint64FromCall(shared, callee)` checks it against the contract you called, and `callee` must be the address called in that same expression.
Returning an encrypted value works the same way in reverse. Share the result with `msg.sender`, and unwrap it with the `FromCall` form:
```solidity theme={null}
// In Token
function swap(sharedEuint64 shared) external returns (sharedEuint64) {
euint64 amountIn = FHE.receiveEuint64Param(shared);
return FHE.shareEuint64(FHE.div(amountIn, FHE.asEuint64(2)), msg.sender);
}
```
```solidity theme={null}
// In Vault
euint64 out = FHE.receiveEuint64FromCall(token.swap(shared), address(token));
FHE.allowThis(out);
```
`FHE.shareEuint64` reverts with `SenderNotAllowed` unless your contract is itself allowed on the handle. You cannot share what you cannot use.
Sharing is single-use and transaction-scoped, so a share cannot be stored, replayed, or reconstructed from an event. To keep a received value past the transaction, call `FHE.allowThis` on the unwrapped `euintXX`. Anything you derive from it produces a new handle that needs its own `FHE.allowThis` before you store it.
## Config keys
Five keys were renamed. In `0.6` an unknown key was silently dropped and the setting fell back to its default. Both schemas now reject unknown keys and name the replacement, so a stale key throws when you construct the client.
| Before | After |
| - | - |
| `defaultPermitExpiration` | `defaultACPExpiration` |
| `react.shareablePermits` | `react.shareableACPs` |
| `react.autogeneratePermits` | `react.autogenerateACPs` |
| `react.permitExpirationOptions` | `react.acpExpirationOptions` |
| `react.defaultPermitExpirationSeconds` | `react.defaultACPExpirationSeconds` |
If your `0.6` app set any of these, the setting was already being ignored and the default was in force. Behavior can change once it starts applying again. An expiration you believed was one day may have been running at the 30 day default.
## Permits are now ACPs
"Permit" is now "ACP", short for Access Control Permission, so that it is not confused with an ERC-2612 permit. The entry point moves and the client namespace is singular:
```ts theme={null}
// Before
import { PermitUtils, type Permit, type Permission } from '@cofhe/sdk/permits';
const permit = await client.permits.createSelf();
// After
import { ACPUtils, type ACP, type ACPPublic } from '@cofhe/sdk/acps';
const acp = await client.acp.createSelf();
```
Decrypt builders follow the same rename: `.withPermit()` becomes `.withACP()` and `.withoutPermit()` becomes `.withoutACP()`.
React hooks rename the same way. `useCofhePermits` becomes `useCofheACPs`, `useCofheActivePermit` becomes `useCofheActiveACP`, and the rest of the family follows the pattern.
Rename only identifiers that resolve to a `@cofhe/*` import. A blind replace of `Permit` corrupts unrelated code, and the English words `permitted` and `permitting` are not renames. `isPermittedCofheEnvironment` and `isAllowedWithPermission` keep their names.
For the full type table, the new scope model, and what the client gained, see [Access Control Permissions](/cofhe/client-sdk/guides/acps).
Stored permits are dropped. They were signed with retired EIP-712 types and cannot verify against the upgraded ACL, so your users are prompted to sign again on first use. Nothing needs migrating, but it looks like data loss if you are not expecting it.
## Encrypt call sites
Two changes land on the same lines. `execute()` returns a different shape, and you must now declare the consuming contract.
```ts theme={null}
// Before
const [encA, encB] = await client.encryptInputs([a, b]).execute();
await contract.f(encA, encB);
// After
const [handleA, handleB, signature] = await client
.encryptInputs([a, b])
.setConsumingContract(contractAddress)
.execute();
await contract.f([handleA, handleB], signature);
```
The result now holds one handle per input followed by a single signature, so it has `inputs.length + 1` elements. Code that assumed the result matched the input count is off by one.
`setConsumingContract` is required because the verifier binds the target contract into the signed digest, which stops a batch signed for one contract being replayed into another. Omitting it is a compile error in TypeScript, since `encryptInputs()` returns a builder without an `execute()` method.
The consuming contract is the contract that runs `FHE.asEuint*`, which is not always the contract you call. If your app calls `vault.deposit(...)` and the vault is what converts the value, the consuming contract is the vault. Naming the wrong one compiles, typechecks, and reverts at runtime. Trace the value to the `FHE.asEuint*` call.
The per-item input types are gone: `EncryptedItemInput`, `EncryptedUint64Input`, and the rest of that family. A value that used to be one of them is now a handle. These also break on your own helpers, where a fixture typed `(encAmount: EncryptedUint64Input)` fails at its definition rather than at the call site. `asHashPlusProof()` is removed, because its output is what `execute()` always returns now.
## Silent changes
A clean build proves very little in this migration. Each of these compiles and then behaves differently:
| Change | What happens |
| - | - |
| A bare `euintXX` parameter left unmigrated | Compiles, and keeps running while both sides stay on it. Can be a live disclosure path, not a style issue. |
| Only one side moved to `sharedEuintXX` | Both spellings are `bytes32` on the wire, so the call compiles and reverts with `NotShared`. |
| `receiveEuintXXFromCall` naming a trusted address instead of the callee | Checks who created the share rather than who handed it over. Exploitable. |
| A wrong-length destructure of the `execute()` result | Typechecks, then fails at runtime on the trailing signature. |
| A reordered or partial batch | The signature covers the exact ordered set that `execute()` produced. Compiles, then fails verification. |
| `ACPUtils.export()` on a self ACP | `0.6` serialized anything; `0.7` throws unless the ACP is a signed sharing ACP. |
| A transient allowance relied on across transactions | Mock transient storage is now real EIP-1153, so it expires with its own transaction rather than the block. |
| Overload selector strings and ERC-165 interface ids | `InEuint64` was a tuple; `externalEuint64` is a `bytes32`, so ids change. Recompute them. |
| `cofhe.logMocks` in a Hardhat config | Still accepted by the plugin's config type, so it typechecks. No plugin code reads it. Use `hre.cofhe.mocks.disableLogs()` or the `setlogops` task. |
Verify by exercising a round trip, not by compiling. For every bare-handle function you kept, ask whether an arbitrary caller can reach it with a handle the contract is allowed on. If they can, guard it with `FHE.isAllowed(value, msg.sender)`.
## Next steps
* Check every version against the [compatibility page](/cofhe/get-started/introduction/compatibility).
* Read the [Access Control Permissions guide](/cofhe/client-sdk/guides/acps) for the permission model in full.
* Review [encrypting inputs](/cofhe/client-sdk/guides/encrypting-inputs) for the current builder API.
# Overview
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/introduction/overview
Introduction to @cofhe/sdk - the TypeScript client SDK for building FHE-enabled applications on Fhenix
`@cofhe/sdk` is the TypeScript client SDK for [CoFHE](/cofhe/deep-dive/cofhe-components/overview). It handles the client-side work of interacting with FHE-enabled smart contracts: encrypting inputs with ZK proofs, and decrypting ciphertext handles. It also manages the EIP-712 Access Control Permissions (ACPs) that authorize those decryptions. Decryption itself happens in [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor), inside a hardware-attested TEE.
Onchain, contracts use [`FHE.sol`](/cofhe/fhe-library/introduction/overview) to operate on encrypted data. Offchain, this SDK prepares the inputs and reads the outputs.
## What the SDK does
Packs plaintext values, generates a ZKPoK, and submits them to the CoFHE verifier's batch endpoint. Returns one ciphertext handle per input plus a single shared signature, for use in contract calls.
Requests decryption of a ciphertext handle via Teecryptor using an ACP. Returns the plaintext locally, not published onchain.
Requests decryption and returns the plaintext with a Teecryptor signature for onchain verification.
Creates, stores, and manages EIP-712 ACPs that authorize decryption of specific ciphertext handles.
## Entrypoints
| Entrypoint | Contents |
| - | - |
| `@cofhe/sdk` | Core types (`Encryptable`, `FheTypes`, `EncryptStep`, `CofheError`), shared across runtimes |
| `@cofhe/sdk/web` | `createCofheConfig` / `createCofheClient` with browser defaults (IndexedDB storage, TFHE WASM, Web Workers) |
| `@cofhe/sdk/node` | `createCofheConfig` / `createCofheClient` with Node.js defaults (filesystem storage, `node-tfhe`) |
| `@cofhe/sdk/acps` | ACP creation, validation, serialization, and storage utilities |
| `@cofhe/sdk/adapters` | `Ethers5Adapter`, `Ethers6Adapter`, `WagmiAdapter`, `HardhatSignerAdapter` |
| `@cofhe/sdk/chains` | Built-in chain definitions and `getChainById` / `getChainByName` helpers |
```typescript theme={null}
// Browser
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web';
// Node.js
import {
createCofheConfig as createCofheConfigNode,
createCofheClient as createCofheClientNode,
} from '@cofhe/sdk/node';
// Shared
import { Encryptable, FheTypes } from '@cofhe/sdk';
import { chains } from '@cofhe/sdk/chains';
import { Ethers6Adapter } from '@cofhe/sdk/adapters';
```
## Client lifecycle
The SDK follows a three-step lifecycle: **config to client to connect**.
```text theme={null}
createCofheConfig({ supportedChains }) → createCofheClient(config) → client.connect(publicClient, walletClient)
```
```typescript theme={null}
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web';
import { Encryptable, FheTypes } from '@cofhe/sdk';
import { chains } from '@cofhe/sdk/chains';
const config = createCofheConfig({
supportedChains: [chains.sepolia],
});
const client = createCofheClient(config);
await client.connect(publicClient, walletClient);
// Encrypt and send: one handle per input, then the batch signature
const [hash, signature] = await client
.encryptInputs([Encryptable.uint64(100n)])
.setConsumingContract(contract.address)
.execute();
await contract.deposit(hash, signature);
// Decrypt for UI
await client.acp.getOrCreateSelfACP();
const ctHash = await contract.getBalance();
const balance = await client
.decryptForView(ctHash, FheTypes.Uint64)
.execute();
```
## Key concepts
Install the SDK, write a contract, and run your first encrypt to store to decrypt test in minutes.
Encrypt plaintext values with ZK proofs before passing them to your smart contract.
Create and manage the EIP-712 ACPs that authorize decryption of confidential data.
Reveal encrypted values locally for UI display using ACPs.
Decrypt with a verifiable Teecryptor signature for onchain use.
# Quick Start: Foundry
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/quick-start/foundry
Set up a Foundry project with @cofhe/foundry-plugin, write a contract, and test the encrypt to store to decrypt flow
Set up a Foundry project with `@cofhe/foundry-plugin`, write a contract that stores an encrypted `uint32`, and test the encrypt to store to decrypt flow under `forge test`.
Want to skip the setup? Clone the [cofhe-foundry-starter](https://github.com/FhenixProtocol/cofhe-foundry-starter) template to get a pre-configured project with everything ready to go.
## Prerequisites
* [Foundry](https://book.getfoundry.sh/getting-started/installation) (`forge`, `cast`, `anvil`)
* Node.js 18+ with npm/pnpm/yarn (for installing the plugin's npm packages)
## 1. Install dependencies
The plugin and its CoFHE dependencies are distributed via npm. Install them as dev dependencies, then `forge install` for `forge-std`:
```bash npm theme={null}
npm install -D @cofhe/foundry-plugin@^0.8.0 @cofhe/mock-contracts@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0 @openzeppelin/contracts
forge install foundry-rs/forge-std
```
```bash pnpm theme={null}
pnpm add -D @cofhe/foundry-plugin@^0.8.0 @cofhe/mock-contracts@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0 @openzeppelin/contracts
forge install foundry-rs/forge-std
```
```bash yarn theme={null}
yarn add -D @cofhe/foundry-plugin@^0.8.0 @cofhe/mock-contracts@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0 @openzeppelin/contracts
forge install foundry-rs/forge-std
```
## 2. Configure `foundry.toml`
```toml foundry.toml theme={null}
[profile.default]
src = "src"
out = "out"
test = "test"
script = "script"
libs = ["node_modules"]
solc_version = "0.8.25"
auto_detect_remappings = false
code_size_limit = 100000
```
`code_size_limit = 100000` is required, the mock contracts exceed the EIP-170 24 KB limit.
`evm_version = "cancun"` is no longer required as of `@cofhe/mock-contracts@0.8.0`. `MockACL` was migrated off transient storage to block-number-based storage. Set it only if your own contracts need cancun-specific opcodes.
## 3. Configure `remappings.txt`
```text remappings.txt theme={null}
forge-std/=node_modules/forge-std/src/
hardhat/=node_modules/forge-std/src/
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
@fhenixprotocol/cofhe-contracts/=node_modules/@fhenixprotocol/cofhe-contracts/
@cofhe/mock-contracts/=node_modules/@cofhe/mock-contracts/
@cofhe/foundry-plugin/=node_modules/@cofhe/foundry-plugin/
```
The `hardhat/=node_modules/forge-std/src/` line is **load-bearing**. `MockCoFHE.sol` imports `hardhat/console.sol`, and this alias resolves it to forge-std's compatible `console.sol`.
## 4. Write a contract
```solidity src/MyContract.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
contract MyContract {
euint32 public storedValue;
function setValue(externalEuint32 inValue, bytes calldata inputProof) external {
storedValue = FHE.asEuint32(inValue, inputProof);
FHE.allowThis(storedValue);
FHE.allowSender(storedValue);
}
}
```
* `euint32`: an encrypted `uint32` stored onchain as a ciphertext handle.
* `externalEuint32`: the encrypted input handle produced by `CofheClient`, paired with a `bytes` proof.
* `FHE.allowThis` / `FHE.allowSender`: grant the contract and caller permission to read the encrypted value (required by the ACL).
## 5. Write a test
```solidity test/MyContract.t.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;
import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol";
import { CofheClient } from "@cofhe/foundry-plugin/contracts/CofheClient.sol";
import { externalEuint32 } from "@fhenixprotocol/cofhe-contracts/FHE.sol";
import { MyContract } from "../src/MyContract.sol";
contract MyContractTest is CofheTest {
MyContract public myContract;
CofheClient public bob;
uint256 constant BOB_PKEY = 0xB0B;
function setUp() public {
deployMocks();
bob = createCofheClient();
bob.connect(BOB_PKEY);
vm.prank(bob.account());
myContract = new MyContract();
}
function test_StoresAndDecryptsAnEncryptedValue() public {
// 1. Encrypt the input, bound to the contract that will consume it
(externalEuint32 hash, bytes memory proof) = bob.createExternalEuint32(42, address(myContract));
// 2. Send to contract
vm.prank(bob.account());
myContract.setValue(hash, proof);
// 3. Assert the stored plaintext directly (mock-only, no ACP needed)
expectPlaintext(myContract.storedValue(), uint32(42));
}
}
```
## 6. Run
```bash theme={null}
forge test -vvv
```
```text theme={null}
[PASS] test_StoresAndDecryptsAnEncryptedValue() (gas: …)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in …
```
## What just happened?
1. **`deployMocks()`** deployed the full CoFHE coprocessor mock stack (TaskManager, ACL, ZK verifier, threshold network) to the in-process EVM.
2. **`createCofheClient()` + `bob.connect(BOB_PKEY)`** spun up an in-Solidity SDK shim bound to `vm.addr(BOB_PKEY)`.
3. **`bob.createExternalEuint32(42, address(myContract))`** produced a signed `externalEuint32` and its proof, the same shape your contract receives on testnet, signed by `MockZkVerifierSigner`.
4. **`vm.prank(bob.account())` + `setValue(...)`** called the contract as Bob. The contract stored the ciphertext handle and granted ACL access to itself and Bob.
5. **`expectPlaintext(myContract.storedValue(), 42)`** read the onchain plaintext directly from `MockTaskManager.mockStorage`, no ACP, no SDK round-trip.
## Next steps
* [Foundry Plugin to Getting Started](/cofhe/client-sdk/foundry-plugin/getting-started): full plugin configuration and features.
* [CofheTest](/cofhe/client-sdk/foundry-plugin/cofhe-test): `deployMocks`, `expectPlaintext`, log toggles.
* [CofheClient](/cofhe/client-sdk/foundry-plugin/cofhe-client): encrypt inputs, decrypt for view or tx, and ACPs.
* [Testing](/cofhe/client-sdk/foundry-plugin/testing): canonical test patterns for ACL, public-decrypt, and fuzzing.
# Quick Start: Hardhat
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/quick-start/hardhat
Set up a Hardhat project with @cofhe/sdk, write a contract, and test the encrypt to store to decrypt flow
Set up a Hardhat project with `@cofhe/hardhat-plugin`, write a contract that stores an encrypted `uint32`, and test the encrypt to store to decrypt flow.
Want to skip the setup? Clone the [cofhe-hardhat-starter](https://github.com/FhenixProtocol/cofhe-hardhat-starter) template to get a pre-configured project with everything ready to go.
## Prerequisites
* Node.js 18+
* An existing Hardhat project (or run `npx hardhat init` to create one)
* `@nomicfoundation/hardhat-toolbox` or `@nomicfoundation/hardhat-ethers` installed
## 1. Install dependencies
```bash npm theme={null}
npm install @cofhe/hardhat-plugin@^0.8.0 @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
```bash pnpm theme={null}
pnpm add @cofhe/hardhat-plugin@^0.8.0 @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
```bash yarn theme={null}
yarn add @cofhe/hardhat-plugin@^0.8.0 @cofhe/sdk@^0.8.0 @fhenixprotocol/cofhe-contracts@^1.0.0
```
## 2. Configure Hardhat
Import the plugin and set `evmVersion` to `cancun` (required for the transient storage opcodes used by FHE contracts).
```typescript hardhat.config.ts theme={null}
import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
import '@cofhe/hardhat-plugin';
const config: HardhatUserConfig = {
solidity: {
version: '0.8.28',
settings: {
evmVersion: 'cancun',
},
},
};
export default config;
```
Without `evmVersion: 'cancun'`, compilation will fail with errors from `@fhenixprotocol/cofhe-contracts`.
## 3. Write a contract
Create a minimal contract that accepts an encrypted input and stores it onchain.
```solidity contracts/MyContract.sol theme={null}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import '@fhenixprotocol/cofhe-contracts/FHE.sol';
contract MyContract {
euint32 public storedValue;
function setValue(externalEuint32 inValue, bytes calldata inputProof) external {
storedValue = FHE.asEuint32(inValue, inputProof);
FHE.allowThis(storedValue);
FHE.allowSender(storedValue);
}
}
```
* `euint32`: an encrypted `uint32` stored onchain as a ciphertext handle.
* `externalEuint32`: the encrypted input handle produced by the SDK, paired with a `bytes` proof.
* `FHE.allowThis` / `FHE.allowSender`: grant the contract and caller permission to read the encrypted value (required by the ACL).
## 4. Write a test
Use `hre.cofhe.createClientWithBatteries` to get a fully configured SDK client with a self-ACP, then encrypt to send to decrypt.
```typescript test/MyContract.test.ts theme={null}
import hre from 'hardhat';
import { CofheClient, Encryptable, FheTypes } from '@cofhe/sdk';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { expect } from 'chai';
describe('MyContract', () => {
let cofheClient: CofheClient;
let signer: HardhatEthersSigner;
before(async () => {
[signer] = await hre.ethers.getSigners();
cofheClient = await hre.cofhe.createClientWithBatteries(signer);
});
it('stores and decrypts an encrypted value', async () => {
const Factory = await hre.ethers.getContractFactory('MyContract');
const contract = await Factory.deploy();
// 1. Encrypt the input, bound to the contract that will consume it
const [valueHash, signature] = await cofheClient
.encryptInputs([Encryptable.uint32(42n)])
.setConsumingContract(await contract.getAddress())
.execute();
// 2. Send to contract
await (await contract.setValue(valueHash, signature)).wait();
// 3. Read the stored handle and decrypt
const ctHash = await contract.storedValue();
const decrypted = await cofheClient
.decryptForView(ctHash, FheTypes.Uint32)
.execute();
expect(decrypted).to.equal(42n);
});
});
```
## 5. Run
```bash theme={null}
npx hardhat test
```
The plugin deploys mock contracts automatically, no extra setup needed.
```text theme={null}
MyContract
✓ stores and decrypts an encrypted value
1 passing (1s)
```
## What just happened?
1. The **Hardhat plugin** deployed mock versions of the CoFHE coprocessor contracts (TaskManager, ACL, ZK verifier, threshold network) before the test ran.
2. `createClientWithBatteries` created an SDK client connected to the Hardhat network, with a self-ACP ready to go.
3. `encryptInputs` encrypted the plaintext `42` into an FHE ciphertext with a ZK proof (simulated by the mock verifier).
4. The contract stored the ciphertext handle onchain and set ACL permissions.
5. `decryptForView` used the ACP to decrypt the handle back to `42n` locally.
## Next steps
* [Hardhat Plugin](/cofhe/client-sdk/hardhat-plugin/getting-started): full plugin configuration and features.
* [Mock Contracts](/cofhe/client-sdk/hardhat-plugin/mock-contracts): read plaintext directly and assert encrypted state in tests.
* [Logging](/cofhe/client-sdk/hardhat-plugin/logging): inspect every FHE operation your contracts perform.
# Quick Start: JavaScript
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/quick-start/javascript
Get started with @cofhe/sdk in a browser or Node.js app
Connect to an FHE-enabled contract, encrypt a value, send it onchain, and decrypt the result, all from JavaScript.
## Prerequisites
* Node.js 18+
* A deployed FHE contract on a supported network (e.g. Sepolia)
* A wallet with testnet ETH
## 1. Install
```bash npm theme={null}
npm install @cofhe/sdk@^0.8.0 viem
```
```bash pnpm theme={null}
pnpm add @cofhe/sdk@^0.8.0 viem
```
```bash yarn theme={null}
yarn add @cofhe/sdk@^0.8.0 viem
```
## 2. Create and connect the client
Import from `@cofhe/sdk/web` for browser apps or `@cofhe/sdk/node` for Node.js scripts.
```typescript Web (browser) theme={null}
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/web';
import { chains } from '@cofhe/sdk/chains';
import { createPublicClient, createWalletClient, http, custom } from 'viem';
import { sepolia } from 'viem/chains';
// 1. Configure
const config = createCofheConfig({
supportedChains: [chains.sepolia],
});
const client = createCofheClient(config);
// 2. Create viem clients (browser wallet)
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
const walletClient = createWalletClient({
chain: sepolia,
transport: custom(window.ethereum),
});
// 3. Connect
await client.connect(publicClient, walletClient);
```
```typescript Node.js theme={null}
import { createCofheConfig, createCofheClient } from '@cofhe/sdk/node';
import { chains } from '@cofhe/sdk/chains';
import { createPublicClient, createWalletClient, http } from 'viem';
import { sepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
// 1. Configure
const config = createCofheConfig({
supportedChains: [chains.sepolia],
});
const client = createCofheClient(config);
// 2. Create viem clients
const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY');
const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
});
const walletClient = createWalletClient({
chain: sepolia,
transport: http(),
account,
});
// 3. Connect
await client.connect(publicClient, walletClient);
```
If you use Ethers.js instead of viem, see the [Client Setup](/cofhe/client-sdk/guides/client-setup) guide for adapter usage.
## 3. Encrypt and send
```typescript theme={null}
import { Encryptable } from '@cofhe/sdk';
// Encrypt a uint32 value, naming the contract that will consume it
const [valueHash, signature] = await client
.encryptInputs([Encryptable.uint32(42n)])
.setConsumingContract(contract.address)
.execute();
// Pass the handle and its proof to your contract
await contract.setValue(valueHash, signature);
```
## 4. Decrypt for display
```typescript theme={null}
import { FheTypes } from '@cofhe/sdk';
// Create an ACP (one-time per account + chain)
await client.acp.getOrCreateSelfACP();
// Read the encrypted handle from your contract
const ctHash = await contract.storedValue();
// Decrypt locally
const plaintext = await client
.decryptForView(ctHash, FheTypes.Uint32)
.execute();
console.log(plaintext); // 42n
```
## Next steps
* [Client Setup](/cofhe/client-sdk/guides/client-setup): adapters, connection management, and config options.
* [Encrypting Inputs](/cofhe/client-sdk/guides/encrypting-inputs): supported types, builder API, and progress callbacks.
* [Access Control Permissions](/cofhe/client-sdk/guides/acps): create, share, and manage decryption authorization.
* [Decrypt to View](/cofhe/client-sdk/guides/decrypt-to-view): reveal encrypted state in your UI.
* [Decrypt to Transact](/cofhe/client-sdk/guides/decrypt-to-tx): decrypt with a verifiable signature for onchain use.
# Quick Start: React
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/quick-start/react
Get started with @cofhe/sdk in a React application
`@cofhe/react` is currently in development. This page will be updated with a full quick start guide once the React library is available.
In the meantime, you can use `@cofhe/sdk/web` directly in your React app. See the [Client Setup](/cofhe/client-sdk/guides/client-setup) guide for how to configure and connect the SDK with viem or wagmi.
# Foundry Plugin Reference
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/reference/foundry-reference
Complete API reference for @cofhe/foundry-plugin
This page summarizes the public surface of `@cofhe/foundry-plugin`. For task-oriented walkthroughs, see [Getting Started](/cofhe/client-sdk/foundry-plugin/getting-started) and [Testing](/cofhe/client-sdk/foundry-plugin/testing).
## `CofheTest`
Abstract test base. Inherit it instead of `forge-std/Test` (it inherits `Test` for you).
```solidity theme={null}
import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol";
```
### Setup
| Function | Description |
| - | - |
| `deployMocks()` | Deploy `MockTaskManager`, `MockACL`, `MockZkVerifier`, `MockZkVerifierSigner`, `MockThresholdNetwork`, `MockThresholdNetworkSigner`. Wires them via `setACLContract` / `setVerifierSigner` / `setDecryptResultSigner`. Funds the ZK signer with 10 ether. Call once in `setUp()`. |
| `createCofheClient()` | Returns a fresh `CofheClient`. Follow with `client.connect(pkey)`. |
### Plaintext reads
| Function | Description |
| - | - |
| `getPlaintext(bytes32 handle)` | Returns the raw `bytes32` plaintext from `MockTaskManager.mockStorage`. |
| `getPlaintext(ebool)` / `(euint8)` / `(euint16)` / `(euint32)` / `(euint64)` / `(euint128)` / `(eaddress)` | Typed overloads returning `bool` / `uint8`–`uint128` / `address`. |
| `expectPlaintext(handle, value)` | Assertion variant. Typed overloads for the same set. |
| `expectPlaintext(handle, value, "msg")` | With assertion message. |
Reverts if the handle isn't in mock storage.
### Logging
| Function | Description |
| - | - |
| `enableLogs()` | Calls `mockTaskManager.setLogOps(true)`. |
| `disableLogs()` | Calls `mockTaskManager.setLogOps(false)`. |
### Public state
| Field | Type |
| - | - |
| `mockTaskManager` | `MockTaskManager` |
| `mockAcl` | `MockACL` |
| `mockZkVerifier` | `MockZkVerifier` |
| `mockZkVerifierSigner` | `MockZkVerifierSigner` |
| `mockThresholdNetwork` | `MockThresholdNetwork` |
| `mockThresholdNetworkSigner` | `MockThresholdNetworkSigner` |
## `CofheClient`
Per-user shim. One client per scenario address.
```solidity theme={null}
import { CofheClient } from "@cofhe/foundry-plugin/contracts/CofheClient.sol";
```
### Connection
| Function | Description |
| - | - |
| `connect(uint256 pkey)` | Set the connected account to `vm.addr(pkey)`. Required before any `createExternal*` or `ACP_*` call. |
| `account()` | Returns the connected `address`. |
### Encrypt inputs
| Function | Returns |
| - | - |
| `createExternalEbool(bool, address)` | `(externalEbool, bytes)` |
| `createExternalEuint8(uint8, address)` | `(externalEuint8, bytes)` |
| `createExternalEuint16(uint16, address)` | `(externalEuint16, bytes)` |
| `createExternalEuint32(uint32, address)` | `(externalEuint32, bytes)` |
| `createExternalEuint64(uint64, address)` | `(externalEuint64, bytes)` |
| `createExternalEuint128(uint128, address)` | `(externalEuint128, bytes)` |
| `createExternalEaddress(address, address)` | `(externalEaddress, bytes)` |
All produce signed `EncryptedInput` shapes signed for `account()`.
### Decrypt
| Function | Returns | Notes |
| - | - | - |
| `decryptForTx_withoutACP(bytes32 ctHash)` | `(bytes32, uint256, bytes)` | `(ctHash, plaintext, signature)`. Signature consumable by `FHE.publishDecryptResult`. Requires `FHE.allowPublic(handle)` to have been called. |
| `decryptForTx_withACP(bytes32 ctHash, ACP acp)` | `(bytes32, uint256, bytes)` | ACL-gated `decryptForTx`. |
| `decryptForView(bytes32 ctHash, ACP acp)` | `uint256` | Offchain seal/unseal. **Reverts on deny**, to assert deny use `mockThresholdNetwork.querySealOutput(...)`. |
### ACPs
Access Control Permission (ACP) helpers. See [Access Control Permissions](/cofhe/client-sdk/guides/acps).
| Function | Description |
| - | - |
| `ACP_createSelf()` | Self-ACP for the connected account; sealing key auto-derived. |
| `ACP_createShared(address recipient)` | Issuer-side shared ACP. |
| `ACP_exportShared(ACP acp)` | Strip sensitive fields to `SharedACPExport`. |
| `ACP_importShared(SharedACPExport export)` | Recipient-side completion. Reverts unless `export.recipient == account()`. |
| `createSealingKey(bytes32 seed)` | Custom sealing key (rarely needed). |
## Mock storage layout
`MockTaskManager.mockStorage` is the onchain plaintext storage that backs `getPlaintext` / `expectPlaintext`. It only exists in the mock environment. `getPlaintext` reverts on real CoFHE networks.
## `foundry.toml` requirements
```toml theme={null}
[profile.default]
solc_version = "0.8.25" # cofhe-contracts target
auto_detect_remappings = false
code_size_limit = 100000 # mocks exceed 24 KB
libs = ["node_modules"]
```
`evm_version = "cancun"` is no longer required as of `@cofhe/mock-contracts@0.8.0`. `MockACL` was migrated off transient storage to block-number-based storage. Set it only if your own contracts need cancun-specific opcodes.
## `remappings.txt` (canonical shape)
```text theme={null}
forge-std/=node_modules/forge-std/src/
hardhat/=node_modules/forge-std/src/
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
@fhenixprotocol/cofhe-contracts/=node_modules/@fhenixprotocol/cofhe-contracts/
@cofhe/mock-contracts/=node_modules/@cofhe/mock-contracts/
@cofhe/foundry-plugin/=node_modules/@cofhe/foundry-plugin/
```
* `@cofhe/mock-contracts/` points at the **package root**, not `…/contracts/`. The plugin's imports include the `contracts/` segment themselves.
* `hardhat/=node_modules/forge-std/src/` is required so that `MockCoFHE.sol`'s `import "hardhat/console.sol"` resolves to forge-std's compatible console.
## Version pinning
`@cofhe/foundry-plugin` and `@cofhe/mock-contracts` pin `@fhenixprotocol/cofhe-contracts` *exactly*, keep all three CoFHE packages aligned. Known-good tuple as of the latest release:
| Package | Version |
| - | - |
| `@cofhe/foundry-plugin` | `0.8.0` |
| `@cofhe/mock-contracts` | `0.8.0` |
| `@fhenixprotocol/cofhe-contracts` | `1.0.0` |
See the [Compatibility](/cofhe/get-started/introduction/compatibility) page for the canonical table.
# Hardhat Plugin Reference
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/reference/hardhat-reference
Complete API reference for @cofhe/hardhat-plugin
This page is under construction. A full API reference documenting all functions, parameters, and return types will be added here.
## `hre.cofhe`
The plugin extends the Hardhat Runtime Environment with a `cofhe` namespace.
### Client creation
| Method | Description |
| - | - |
| `hre.cofhe.createClientWithBatteries(signer?)` | Creates a fully configured `CofheClient` with a self-ACP. Uses the first signer if none provided. |
| `hre.cofhe.createConfig(options)` | Wraps `createCofheConfig` with Hardhat defaults (`environment: 'hardhat'`, `encryptDelay: 0`). |
| `hre.cofhe.createClient(config)` | Creates a `CofheClient` from a config object. |
| `hre.cofhe.connectWithHardhatSigner(client, signer)` | Connects a client using a `HardhatEthersSigner`. |
| `hre.cofhe.hardhatSignerAdapter(signer)` | Returns `{ publicClient, walletClient }` from a Hardhat signer. |
### `hre.cofhe.mocks`
| Method | Description |
| - | - |
| `hre.cofhe.mocks.deployMocks()` | Programmatically deploy mock contracts. |
| `hre.cofhe.mocks.getPlaintext(ctHash)` | Returns the plaintext `bigint` for a ciphertext hash. |
| `hre.cofhe.mocks.expectPlaintext(ctHash, expected)` | Chai assertion shorthand for `getPlaintext`. |
| `hre.cofhe.mocks.getMockTaskManager()` | Returns the `MockTaskManager` contract instance. |
| `hre.cofhe.mocks.getMockACL()` | Returns the `MockACL` contract instance. |
| `hre.cofhe.mocks.getMockThresholdNetwork()` | Returns the `MockThresholdNetwork` contract instance. |
| `hre.cofhe.mocks.getMockZkVerifier()` | Returns the `MockZkVerifier` contract instance. |
| `hre.cofhe.mocks.withLogs(name, fn)` | Wraps a function with labeled FHE operation logging. |
| `hre.cofhe.mocks.enableLogs(label?)` | Enables FHE operation logging. |
| `hre.cofhe.mocks.disableLogs()` | Disables FHE operation logging. |
## Hardhat config
```typescript hardhat.config.ts theme={null}
import '@cofhe/hardhat-plugin';
export default {
solidity: '0.8.28',
cofhe: {
gasWarning: true, // Warn on high mock gas usage (default: false)
mocksDeployVerbosity: 'v', // Mock deploy log verbosity: '' | 'v' | 'vv' (default: 'v')
},
};
```
## Hardhat tasks
| Task | Description |
| - | - |
| `task:cofhe-mocks:deploy` | Deploy mock contracts. Takes no parameters. |
| `task:cofhe-mocks:setlogops` | Toggle FHE operation logging. Options: `--enable`. |
## Pre-configured networks
| Network | URL | Chain ID |
| - | - | - |
| `localcofhe` | `http://127.0.0.1:42069` | `420105` |
| `eth-sepolia` | Ethereum Sepolia public RPC | `11155111` |
| `arb-sepolia` | Arbitrum Sepolia public RPC | `421614` |
# SDK Reference
Source: https://cofhe-docs.fhenix.zone/cofhe/client-sdk/reference/sdk-reference
Complete API reference for @cofhe/sdk
This page is under construction. A full API reference documenting all functions, parameters, and return types will be added here.
## Entrypoints
| Entrypoint | Contents |
| - | - |
| `@cofhe/sdk` | Core types: `Encryptable`, `FheTypes`, `EncryptStep`, `CofheError`, `CofheErrorCode`, `isCofheError` |
| `@cofhe/sdk/web` | `createCofheConfig`, `createCofheClient` (browser defaults) |
| `@cofhe/sdk/node` | `createCofheConfig`, `createCofheClient` (Node.js defaults) |
| `@cofhe/sdk/acps` | `ACPUtils`, `setACP`, `setActiveACPHash`, `getACP`, `getActiveACPHash` |
| `@cofhe/sdk/adapters` | `Ethers5Adapter`, `Ethers6Adapter`, `WagmiAdapter`, `HardhatSignerAdapter` |
| `@cofhe/sdk/chains` | `chains`, `getChainById`, `getChainByName`, `hardhat` |
## Core types
### `Encryptable`
Factory for creating encryptable input items.
| Method | Input type | Solidity param |
| - | - | - |
| `Encryptable.bool(value)` | `boolean` | `externalEbool` |
| `Encryptable.uint8(value)` | `bigint \| string` | `externalEuint8` |
| `Encryptable.uint16(value)` | `bigint \| string` | `externalEuint16` |
| `Encryptable.uint32(value)` | `bigint \| string` | `externalEuint32` |
| `Encryptable.uint64(value)` | `bigint \| string` | `externalEuint64` |
| `Encryptable.uint128(value)` | `bigint \| string` | `externalEuint128` |
| `Encryptable.address(value)` | `bigint \| string` | `externalEaddress` |
| `Encryptable.create(type, value)` | varies | varies |
### `FheTypes`
Enum of supported FHE types used with `decryptForView`.
| Value | JS return type |
| - | - |
| `FheTypes.Bool` | `boolean` |
| `FheTypes.Uint8` | `bigint` |
| `FheTypes.Uint16` | `bigint` |
| `FheTypes.Uint32` | `bigint` |
| `FheTypes.Uint64` | `bigint` |
| `FheTypes.Uint128` | `bigint` |
| `FheTypes.Uint160` | `string` (checksummed address) |
### The `encryptInputs` result
`execute()` returns one ciphertext handle per input, in order, followed by a single signature covering the whole batch:
```typescript theme={null}
type EncryptInputsResult = readonly `0x${string}`[];
// [ ...hashes, signature ] -> inputs.length + 1 elements
```
The per-item `EncryptedItemInput` struct and its typed variants were removed in `0.7`, along with `asHashPlusProof()`.
### `EncryptStep`
Enum values fired during the encryption pipeline:
| Value | Description |
| - | - |
| `EncryptStep.InitTfhe` | Initialize TFHE WASM module |
| `EncryptStep.FetchKeys` | Fetch FHE public key and CRS |
| `EncryptStep.Pack` | Pack plaintext values |
| `EncryptStep.Prove` | Generate ZK proof |
| `EncryptStep.Verify` | Submit to CoFHE verifier |
# ACL (Access Control Layer)
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/cofhe-components/acl
Onchain contract that manages and enforces access control for ciphertexts, ensuring only authorized contracts and accounts can reference or decrypt them
| Aspect | Description |
| - | - |
| **Type** | Contract deployed on every host chain, UUPS-upgradeable behind a proxy. |
| **Function** | Manages and enforces access control for ciphertexts, for both contracts and user accounts. |
| **Responsibilities** | • Records who may use each handle in FHE operations.
• Records which handles may be decrypted, and by whom.
• Answers the access queries of the [TaskManager](/cofhe/deep-dive/cofhe-components/task-manager) and the decryption component. |
Every encrypted value in CoFHE is guarded by this contract. A handle is useless to anyone the ACL does not list. The TaskManager rejects operations on inputs the caller cannot access, and [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) refuses to decrypt handles without a matching grant.
## Grant tiers
| Tier | Granted by | Scope |
| - | - | - |
| **Transient** | `FHE.allowTransient` | Current transaction only. Stored with EIP-1153 transient storage, so it costs no persistent state. |
| **Persistent** | `FHE.allow(handle, account)` | A specific account (contract or EOA), permanently. |
| **Global** | `FHE.allowGlobal` (alias `FHE.allowPublic`) | Every account. Also marks the handle publicly decryptable. |
| **Decryption** | `TaskManager.allowForDecryption` | Adds the handle to the decryption allowlist and emits `AllowedForDecryption`. |
Every tier above requires the requester to hold access to the handle already. Only the transient tier expires, and the ACL has no revoke. So any holder can extend access to anyone. See [who you trust when you grant access](/cofhe/fhe-library/core-concepts/access-control#who-you-trust-when-you-grant-access).
## Writes go through the TaskManager
All state-mutating entry points require `msg.sender` to be the TaskManager; direct calls revert with `DirectAllowForbidden`. Contracts grant access through the `FHE.sol` helpers (`allow`, `allowThis`, `allowSender`, `allowGlobal`, `allowTransient`), which route through the TaskManager.
## ACPs
Offchain reads are authorized by an [ACP](/cofhe/client-sdk/guides/acps), an Access Control Permission: an EIP-712 body signed by its issuer. Beyond the issuer, expiration, and the sealing key used for sealed outputs, an ACP carries a **scope**: global, limited to specific contracts, or limited to specific handles. A scope only narrows what the issuer could already access; it never grants more.
An ACP can be shared with a recipient, revoked through a revoker contract, and handed over onchain through the ACP share registry.
## Read surface
| Function | Answers |
| - | - |
| `isAllowed(handle, account)` | May this account use the handle? (any tier) |
| `allowedTransient(handle, account)` | Is there a transient grant in this transaction? |
| `persistAllowed(handle, account)` | Is there a persistent grant? |
| `globalAllowed(handle)` | Is the handle globally allowed? |
| `isAllowedForDecryption(handle)` | Is the handle on the decryption allowlist? |
| isAllowedWithPermission | Does this ACP authorize its issuer for the given handle? |
The last two are what the decryption path runs on. For every `decrypt` or `sealoutput` request, Teecryptor queries the ACL through the TaskManager: `isAllowedWithPermission` when an ACP is attached, or `isPubliclyAllowed` (the TaskManager's wrapper over `globalAllowed`) when none is. A future [Threshold Network](/cofhe/deep-dive/research/future-plans) will consume the same interface.
## Upgrades
The contract is UUPS-upgradeable behind a proxy, and storage uses ERC-7201 namespaced slots for upgrade safety.
# CommitmentRegistry
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/cofhe-components/commitment-registry
Registry-chain contract that records FHE computation commitments. Teecryptor verifies ciphertext integrity against it before decrypting.
| Aspect | Description |
| - | - |
| **Type** | UUPS-upgradeable Solidity contract deployed on a dedicated **registry chain**. |
| **Function** | Records `(version, handle) → commitHash` entries for every ciphertext the coprocessor produces or verifies, computed results and encrypted inputs alike. |
| **Responsibilities** | • Provide an authoritative source of ciphertext integrity that [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) checks **before** decrypting.
• Group commitments by an opaque `version` tag so a future tfhe-rs / FHE-parameter upgrade can roll out without invalidating earlier ciphertexts.
• Enforce write-once semantics per `(version, handle)` to prevent commitment replacement.
• Expose paginated enumeration so offchain tooling can audit what has been posted. |
| **Deployment** | One deployment per registry chain, behind an ERC-1967 proxy. The production registry is `0x4B82F827C03fad5739EFEcfcF743E65E732be695` on Arbitrum One, and it serves every host chain. |
## What a commitment is
A commitment is the `keccak256` hash of a ciphertext's canonical stored bytes. The [compute pipeline](/cofhe/deep-dive/cofhe-components/compute-pipeline) produces one for every result it computes, and anchors one for every encrypted input the [ZK Verifier](/cofhe/deep-dive/cofhe-components/zk-verifier) approves. It batches them and writes them to this registry.
## Who writes and who reads
One component writes commitments, and one component reads them. The write happens when a ciphertext is produced; the read happens later, whenever someone asks to decrypt it.
The compute pipeline and Teecryptor run offchain inside CoFHE; the registry itself is onchain. Hover any component or message in the [explorable version](/cofhe/deep-dive/cofhe-components/commitment-registry-explorable).
## Why commitments?
The commitment is a safety check in the decryption flow. Before decrypting anything, [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) confirms that the ciphertext bytes it fetched hash to the commitment anchored onchain. It only ever decrypts a ciphertext the coprocessor actually produced, never a tampered or substituted one.
The registry also makes the coprocessor accountable. Every result it has ever produced is committed publicly, permanently, and write-once, so its computation history is tamper-evident and open to independent audit. Commitments for every host chain land on one dedicated **registry chain**, which gives the decryption path a single place to verify against.
## Storage shape
```solidity theme={null}
mapping(bytes32 version => mapping(bytes32 handle => bytes32 commitHash)) commitments;
mapping(bytes32 version => bytes32[]) handlesByVersion;
mapping(bytes32 version => VersionStatus) versionStatus;
mapping(address => bool) posters;
```
`commitments` is the source-of-truth lookup. `handlesByVersion` is an array kept in parallel so paginated enumeration is `O(limit)` instead of `O(total)`. Storage lives at the ERC-7201 slot derived from `cofhe.storage.CommitmentRegistry`, so the contract is upgrade-safe.
## Version lifecycle
`version` is an opaque `bytes32` tag chosen by the coprocessor when FHE parameters change. The current tag is the number 2 encoded as `bytes32`. Every version moves through a small state machine:
Only an Active version accepts new commitments. Revoked is terminal.
| State | Meaning | Allowed transitions |
| - | - | - |
| `Unset` | Default. No commitments have been posted under this version. | to `Active` |
| `Active` | Posters may write commitments under this version. Teecryptor honors lookups. | to `Deprecated` or `Revoked` |
| `Deprecated` | New commitments rejected. Existing lookups still resolve. Used during a parameter rollover. | to `Revoked` |
| `Revoked` | Hard kill. No further transitions; the version is dead. | none (terminal) |
The admin-only `setVersionStatus(version, newStatus)` enforces these transitions and reverts with `InvalidVersionTransition` otherwise. The transition emits `VersionStatusChanged(version, oldStatus, newStatus)`.
## Write surface
Only accounts holding the **poster** role can write commitments (`postCommitments`, `postCommitmentsSafe`); posts from anyone else revert with `OnlyPosterAllowed(caller)`. Poster management and version transitions are admin-only. In production, the poster role is held by the coprocessor's relayer signer (OpenZeppelin Relayer).
## Writing commitments
```solidity theme={null}
function postCommitments(
bytes32 version,
bytes32[] calldata handles,
bytes32[] calldata commitHashes
) external onlyPoster;
function postCommitmentsSafe(
bytes32 version,
bytes32[] calldata handles,
bytes32[] calldata commitHashes
) external onlyPoster;
```
Both functions batch-write `(version, handle) → commitHash` rows and require:
* `version` is in `Active` state, otherwise the call reverts with `VersionNotActive(version)`.
* `handles.length == commitHashes.length` and `> 0`, otherwise `LengthMismatch` / `EmptyBatch`.
* Each `commitHash != bytes32(0)`, otherwise `ZeroCommitHash(handle)`.
The difference is in **how duplicates are handled**:
| Function | Duplicate handle under same version | Use case |
| - | - | - |
| `postCommitments` | Reverts the whole batch with `CommitmentAlreadyExists(version, handle)`. | Strict integrity: the caller knows it is posting unique data. |
| `postCommitmentsSafe` | Silently skips the handle; emits `CommitmentsPostedSafe(version, newlyPosted, skipped)`. | Idempotent re-flushes (e.g. when the coprocessor's message broker redelivers a commitment batch). |
`postCommitments` emits `CommitmentsPosted(version, batchSize)`. `postCommitmentsSafe` emits `CommitmentsPostedSafe(version, newlyPosted, skipped)` so the offchain caller can tell whether the round did real work.
Both enforce **write-once per (version, handle)**: a commitment can never be overwritten, only superseded by writing the same handle under a new `version`.
## Reading commitments
| Function | Returns | Notes |
| - | - | - |
| `getCommitment(version, handle)` | `bytes32` | `bytes32(0)` means "not posted". |
| `getVersionStatus(version)` | `VersionStatus` | `Unset` if never registered. |
| `getSize(version)` | `uint256` | Number of handles ever committed under `version`. |
| getHandleByIndex(version, index) | `bytes32` | Direct array lookup. Reverts on out-of-range. |
| getHandles(version, offset, limit) | `bytes32[]` | Paginated. Returns an empty array if `offset >= total`; clamps `offset + limit` at `total`. |
| `isPoster(address)` | `bool` | Useful for offchain ops dashboards. |
The paginated `getHandles` is the recommended way to enumerate a version: `getSize` first to compute pages, then `getHandles(version, offset, pageSize)` in a loop.
## Events
| Event | Emitted by | Use |
| - | - | - |
| CommitmentsPosted | `postCommitments` | Confirm a strict batch landed. Carries the version and batch size. |
| CommitmentsPostedSafe | `postCommitmentsSafe` | Reconcile "how many were new" in an idempotent flow. Carries newlyPosted and skipped counts. |
| VersionStatusChanged | `setVersionStatus` | Watch for the `Active` to `Deprecated` transition to know when to stop posting under a version. |
| PosterAdded / PosterRemoved | `addPoster` / `removePoster` | Audit role changes. |
## Source
* Solidity: [`contracts/internal/registry-chain/contracts/commitment-registry/CommitmentRegistry.sol`](https://github.com/FhenixProtocol/cofhe-contracts/blob/master/contracts/internal/registry-chain/contracts/commitment-registry/CommitmentRegistry.sol).
* Offchain: the compute pipeline's blockchain poster batches the commitments and posts them to this registry.
* The pipeline sets the commitment version, and bumps it when the FHE parameters change.
# Compute Pipeline
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/cofhe-components/compute-pipeline
The offchain components that carry an FHE task from onchain event to committed result
| Aspect | Description |
| - | - |
| **Type** | Offchain execution pipeline, built from four components. |
| **Function** | Carries every FHE task from onchain event to committed result. |
| **Responsibilities** | • Listens to TaskManager events on each host chain
• Validates and orders operations
• Executes them with the TFHE library
• Posts a commitment for every result |
The compute pipeline is the computation side of CoFHE. Contracts never call it. It subscribes to what happens onchain, does the encrypted math, and anchors the results.
Four components make up the pipeline. Each one owns a single step, and work moves in one direction.
Color marks the zone. The four pipeline components run offchain inside CoFHE; the TaskManager is on the host chain and the CommitmentRegistry on the registry chain.
## Slim listener
The slim listener watches TaskManager events on every host chain the pipeline serves. `TaskCreated` events bring FHE operations in for execution. `InputVerified` events bring verified encrypted inputs in, so a commitment gets anchored for each.
Delivery is reliable by construction. The listener tracks the last processed block, so a crash or a missed range is re-scanned rather than skipped.
## FheOS
FheOS checks that each operation is well formed and that the inputs it references exist. It holds the operation state for the pipeline.
Operations can arrive before the inputs they depend on have finished computing. FheOS defers such operations and releases them once the missing results land, so out-of-order arrival never produces a wrong answer. Work that is malformed, or that references inputs that never materialize, is set aside for inspection instead of being silently dropped.
## FHE Engine
The FHE Engine runs validated operations against the TFHE library: arithmetic, comparison, select, cast, and random generation on encrypted operands. It reads its operands from the ciphertext store and writes the result back.
The result ciphertext is stored under the handle the TaskManager issued. Any deferred operations waiting on that handle are released as soon as it lands.
Note the name. The FHE Engine is one component inside the compute pipeline, not the pipeline itself.
## Blockchain poster
For every stored result, the pipeline produces a commitment, the `keccak256` hash of the stored ciphertext bytes. The blockchain poster batches those commitments and posts them to the [CommitmentRegistry](/cofhe/deep-dive/cofhe-components/commitment-registry) on the registry chain.
This is the anchor [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) verifies before decrypting anything. Only bytes that hash to a registered commitment ever reach the decryption key.
## Key material
The pipeline computes with the FHE public key material only. Production builds load no decryption key, so a compromised pipeline can corrupt results (which commitment verification would catch) but cannot read them. Decryption capability exists solely inside [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor)'s attested enclave.
# Key Management
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/cofhe-components/key-management
How CoFHE's keys are created in an attested ceremony, split among independent partners, and read back by Teecryptor and the ZK Verifier at boot
CoFHE's security rests on a small set of keys, and none of them is ever held whole by any person or machine outside an attested enclave.
| Key | Used by | Purpose |
| - | - | - |
| FHE network key | [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) | Decrypts ciphertexts inside the enclave |
| Result-signing key | Teecryptor | Signs decrypt results the TaskManager verifies onchain |
| Verifier signing key | [ZK Verifier](/cofhe/deep-dive/cofhe-components/zk-verifier) | Signs verified input batches |
| Public material | Everyone | The network public key and CRS that clients encrypt against |
## The ceremony
The keys are born inside a TEE. A one-shot keygen ceremony runs in its own hardware-attested enclave and generates the network keyset there. The secret material is split with Shamir secret sharing into six shares; any three reconstruct it (3-of-6).
Each share goes to one **partner**: an independent custodian with its own share store. A partner's store accepts writes only from the attested ceremony image, so nobody can slip in a substitute share. The public material is published for clients. The ceremony enclave then terminates; the assembled key is never persisted anywhere.
Color marks the zone. The keygen ceremony and the component enclave (Teecryptor or the ZK Verifier) run inside CoFHE. The partners are independent custodians, so they are not colored as CoFHE's. Hover any component or message in the [explorable version](/cofhe/deep-dive/cofhe-components/key-management-explorable).
## Custody
At rest, the key exists only as shares held by independent partners. No partner can reconstruct anything alone, and no quorum below the threshold can either. Fhenix operates the components, but it cannot assemble the key outside an attested enclave any more than anyone else can.
## Who reads the shares
Two components read key material, and each reads only its own. Teecryptor reads the share carrying the FHE key and the result-signing key. The ZK Verifier reads the share carrying its own signing key. Neither can read the other's, and that separation is enforced by access control on each partner's store, not by trusting the code to behave.
## Release at boot
The reading is done by `cofhe-keys`, a library compiled into the component itself. It is part of the image the partners approve, so the code that reconstructs a key is the same code the attestation covers.
When Teecryptor or the ZK Verifier starts, the TEE hardware produces an attestation of the exact code image it is running.
Every partner independently verifies that attestation and releases its share only to the approved image digest. No partner takes another's word for it.
It gathers shares from every partner it can reach, so one unreachable partner does not stop a boot. It checks each share against its own digest and discards any that fails, then reconstructs from the threshold and validates the result against the published digest. A share that does not verify is dropped rather than used.
The component zeroizes the transit material and never persists the assembled key. Restarting it repeats the whole handshake.
The guarantee this chain yields: from generation through every reconstruction, the key exists only inside hardware-attested enclaves running reviewed code.
# CoFHE Architecture Overview
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/cofhe-components/overview
How CoFHE's onchain contracts and offchain components fit together to run FHE computations for any EVM chain
CoFHE (Coprocessor for Fully Homomorphic Encryption) lets smart contracts compute on encrypted data. Contracts request operations through onchain calls; a set of offchain components executes them and commits to the results. Data stays encrypted until an authorized decryption, and every result can be verified against an onchain commitment.
Color marks the zone, and the boxes group what runs where. Each component is described under [the onchain contracts](#the-onchain-contracts) and [the offchain components](#the-offchain-components) below.
## The onchain contracts
* **FHE.sol**: the Solidity library your contract imports. It exposes arithmetic, comparison, and logical operations on encrypted values, plus access control (`allow`, `allowGlobal`) and decrypt-result verification.
* **CoFHEAddressBook**: deployed at `0xC0F4e00E531a2B086492Ae3DCC1515038307196b` on every supported chain, mainnet and testnet. It maps a TaskManager id to that chain's TaskManager, and `FHE.sol` finds the TaskManager through it with id `1`.
* **[TaskManager](/cofhe/deep-dive/cofhe-components/task-manager)**: the gateway for all FHE operation requests. It validates requests, emits task events for the offchain components, and manages permissions through the ACL.
* **[ACL](/cofhe/deep-dive/cofhe-components/acl)**: the access control contract. It records who may use or decrypt each handle; all writes to it go through the TaskManager.
* **[PlaintextsStorage](/cofhe/deep-dive/cofhe-components/plaintext-storage)**: stores decrypted results. The TaskManager writes them during `publishDecryptResult`, only after ECDSA-verifying the result against `decryptResultSigner`.
* **[CommitmentRegistry](/cofhe/deep-dive/cofhe-components/commitment-registry)**: lives on a dedicated registry chain and records a hash commitment for every ciphertext the coprocessor produces or verifies. Teecryptor verifies integrity against it before decrypting anything.
## The offchain components
* **[Client SDK](/cofhe/client-sdk/introduction/overview)** (`@cofhe/sdk`): the TypeScript library applications use to encrypt inputs, manage [Access Control Permissions](/cofhe/client-sdk/guides/acps) (ACPs), and decrypt outputs.
* **[ZK Verifier](/cofhe/deep-dive/cofhe-components/zk-verifier)**: verifies encrypted inputs in batches, inside a hardware-attested TEE. It checks each zero-knowledge proof, stores the ciphertexts, and signs one approval for the whole batch.
* **[Compute pipeline](/cofhe/deep-dive/cofhe-components/compute-pipeline)**: the execution pipeline. It subscribes to TaskManager events, validates and orders operations, runs them on encrypted data, and posts a commitment for every result.
* **Ciphertext store**: the database that holds every ciphertext, fronted by the CT Server. The ZK Verifier writes verified inputs, the FHE Engine reads operands and writes results, and Teecryptor fetches bytes to decrypt.
* **[Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor)**: the decryption component. It runs inside a hardware-attested TEE, authorizes every request against the onchain ACL, verifies the ciphertext commitment, and returns signed or sealed results.
* **Partners**: independent custodians of the key material. Each holds a Shamir share and releases it only to an attested enclave. See [Key Management](/cofhe/deep-dive/cofhe-components/key-management).
## Data flows
1. **[Encryption request](/cofhe/deep-dive/data-flows/encryption-request-flow)**: an input is encrypted client-side and proven valid with a zero-knowledge proof before it enters the chain.
2. **[FHE operation](/cofhe/deep-dive/data-flows/fhe-operation-request-flow)**: a contract requests a computation; the coprocessor executes it and commits to the result.
3. **[Decryption](/cofhe/deep-dive/data-flows/decryption-request-flow)**: the SDK asks Teecryptor to decrypt a handle, either as a signed plaintext for onchain publication or sealed to an ACP for offchain reads.
## Future plans
Two components run inside hardware-attested TEEs today: Teecryptor, which decrypts, and the ZK Verifier, which checks input proofs. Both move to multi-party computation in one planned step. See [Future Plans](/cofhe/deep-dive/research/future-plans).
# PlaintextsStorage
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/cofhe-components/plaintext-storage
Host-chain contract that stores published decryption results so contracts can read them synchronously
| Aspect | Description |
| - | - |
| **Type** | Internal smart contract on the host chain. |
| **Function** | Stores published decryption results for synchronous reads. |
| **Responsibilities** | • Holds the plaintext result for every published decryption
• Serves reads through the TaskManager's result getters |
Decryption happens offchain, but contracts need the result onchain. When a signed decrypt result is published through `FHE.publishDecryptResult`, the [TaskManager](/cofhe/deep-dive/cofhe-components/task-manager) verifies the signature and writes the plaintext here. Only the TaskManager can write to this contract.
Contracts read results back with `FHE.getDecryptResult(handle)`, which reverts if the result is not yet published, or `FHE.getDecryptResultSafe(handle)`, which returns a ready flag instead. See the [Decryption Flow](/cofhe/deep-dive/data-flows/decryption-request-flow) for the full path.
# TaskManager
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/cofhe-components/task-manager
Onchain entry point for CoFHE integration that initiates FHE operations, generates unique handles, and verifies decrypt result signatures
| Aspect | Description |
| - | - |
| **Type** | Contract deployed on every host chain, UUPS-upgradeable behind a proxy. |
| **Function** | Acts as the onchain entry point for CoFHE integration. |
| **Responsibilities** | • Initiates FHE operations requested through the `FHE.sol` library.
• Generates the unique handles that reference future FHE results.
• Emits structured task events for the offchain components.
• Verifies the signatures on encrypted inputs and published decrypt results. |
| **Deployment** | A separate TaskManager contract is deployed on each supported host chain. Its address differs between mainnet and testnet. |
| **How to find it** | Call `getTm(1)` on the CoFHEAddressBook at `0xC0F4e00E531a2B086492Ae3DCC1515038307196b`, the same address on every supported chain. `FHE.sol` and `@cofhe/sdk` (`getTaskManagerAddress`) resolve it the same way, so do not hard-code a TaskManager address. |
## Two signers
The TaskManager holds two distinct signer addresses, one per trust boundary:
| Signer | Verifies |
| - | - |
| verifierSigner | Encrypted inputs: the ZK Verifier signs each verified batch, and `batchVerifyInputs` checks that signature before emitting `InputVerified(ctHash, commitment)` per input. The signature binds the consuming contract, so a batch cannot be replayed elsewhere. |
| decryptResultSigner | Decrypt results: [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) signs each plaintext it returns, and the publish path checks that signature before storing the result. |
## Decrypt result signature verification
The TaskManager supports **permissionless publishing of decrypt results**. Anyone holding a valid signature from the decryption component can publish the result onchain. Today that signature comes from Teecryptor; a decentralized [Threshold Network](/cofhe/deep-dive/research/future-plans) is the planned successor.
Verified results are stored in the [PlaintextsStorage](/cofhe/deep-dive/cofhe-components/plaintext-storage) contract, where any contract can read them back with `getDecryptResult` (reverts with `DecryptionResultNotReady` if pending) or `getDecryptResultSafe` (returns a ready flag).
### Functions
All take `(ctHash, result, signature)`; the batch variants take parallel arrays.
| Function | Description |
| - | - |
| publishDecryptResult | Verify the signature and store the decrypt result onchain. Emits `DecryptionResult`. |
| publishDecryptResultBatch | Batch publish multiple results in one transaction for gas efficiency. |
| verifyDecryptResult | Verify a signature without publishing (view). Reverts on failure. |
| verifyDecryptResultSafe | Verify a signature without publishing (view). Returns `false` on failure. |
| verifyDecryptResultBatch | Batch verify (view). Reverts on the first failure. |
| verifyDecryptResultBatchSafe | Batch verify (view). Returns a `bool[]` of per-item outcomes. |
| setDecryptResultSigner | Admin-only. Takes an address; sets the authorized signer. |
### Signature message format
The signed message is a fixed **76-byte** buffer:
| Field | Size | Encoding |
| - | - | - |
| `result` | 32 bytes | uint256, big-endian, left-padded with zeros |
| `enc_type` | 4 bytes | i32, big-endian (extracted from ctHash metadata) |
| `chain_id` | 8 bytes | u64, big-endian (from `block.chainid`) |
| `ct_hash` | 32 bytes | uint256, big-endian |
The message is hashed with `keccak256` and verified using OpenZeppelin's `ECDSA.tryRecover`. The `enc_type` and `chain_id` are derived onchain, binding each signature to a specific ciphertext type and chain.
# Teecryptor
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/cofhe-components/teecryptor
Offchain decryption component that decrypts CoFHE ciphertexts inside a hardware-attested Trusted Execution Environment (TEE)
| Aspect | Description |
| - | - |
| **Type** | Offchain component running inside a hardware-attested Trusted Execution Environment (TEE). |
| **Function** | Process decryption requests from the [Client SDK](/cofhe/client-sdk/introduction/overview). |
| **Responsibilities** | • Authorizes each request against the onchain ACL
• Verifies the ciphertext against its onchain commitment
• Decrypts inside the enclave
• Signs (or seals) the result for delivery |
Teecryptor is the component that decrypts CoFHE ciphertexts. It holds the FHE secret key, but only inside a TEE: an isolated, hardware-encrypted execution environment (Intel TDX) whose exact code image is proven by remote attestation. The trust model is deliberate and worth stating plainly. Instead of splitting the decryption computation across independent parties, Teecryptor relies on hardware attestation to guarantee that only one specific, reviewable program can ever touch the key. A [Threshold Network](/cofhe/deep-dive/research/future-plans) that decrypts through multi-party computation is the planned next step and will eventually replace Teecryptor.
## How a decrypt request is processed
Every `decrypt` and `sealoutput` request carries a ciphertext handle, a host chain id, and optionally an [ACP](/cofhe/client-sdk/guides/acps) (Access Control Permission), sent on the wire as `acp`. A `sealoutput` request requires one, since the ACP carries the sealing key. Teecryptor then runs a fixed pipeline:
Teecryptor checks the handle against the onchain ACL through the [TaskManager](/cofhe/deep-dive/cofhe-components/task-manager) on the requested host chain. With an ACP, the contract verifies its EIP-712 signature and calls `isAllowedWithPermission`. Without one, the handle must have been marked publicly decryptable, checked via `isPubliclyAllowed`. Both paths fail closed: no onchain allowance, no decryption.
Concurrently with authorization, Teecryptor confirms that the [compute pipeline](/cofhe/deep-dive/cofhe-components/compute-pipeline) posted a commitment for this handle in the [CommitmentRegistry](/cofhe/deep-dive/cofhe-components/commitment-registry). Commitments are write-once, so a verified result is cached permanently.
The ciphertext is fetched from the CT Server exactly as the coprocessor stored it, and its bytes must hash to the onchain commitment. A missing commitment or mismatched bytes rejects the request. Teecryptor only ever decrypts bytes the coprocessor committed to publicly.
The enclave decrypts the stored bytes with the FHE secret key, exactly as they were committed. The bytes that were hashed are the bytes that get decrypted, so the integrity check stays meaningful.
For `decrypt` (the `decryptForTx` path), the plaintext is signed with an ECDSA key that never leaves the enclave, producing a result any contract can verify onchain. For `sealoutput` (the `decryptForView` path), the plaintext is instead encrypted to the ACP's sealing key using NaCl `crypto_box` (X25519 with XSalsa20-Poly1305). Only the ACP holder can read it, and the SDK unseals it locally.
A handle whose ciphertext or commitment has not landed yet is not an error. Teecryptor answers with a retryable status and the SDK re-submits automatically, roughly once per second within a multi-minute budget.
The Client SDK runs client side, the TaskManager and CommitmentRegistry onchain, and Teecryptor offchain inside CoFHE. Hover any component or message in the [explorable version](/cofhe/deep-dive/cofhe-components/teecryptor-explorable).
## Attestation
Remote attestation is what turns "trust the operator" into "verify the code". Before Teecryptor can obtain any key material, the TEE hardware produces an attestation of the exact code image it is running. Each key-share custodian independently checks that attestation and releases its share only to the approved image digest. The guarantee this yields: the FHE key can only be reconstructed by the exact reviewed code, running on genuine TEE hardware. Nothing else, including the infrastructure operator, can obtain the key or observe the enclave's memory, which the hardware keeps encrypted.
## Key custody
The FHE secret key is never stored whole. It is split with Shamir secret sharing into six shares held by independent parties, with any three required for reconstruction (3-of-6). At boot, the attested enclave collects shares from the custodians, reconstructs the key in memory, and zeroizes the transient share material. The reconstructed key exists only inside the enclave for the lifetime of the process. It is never persisted. The result-signing key travels inside the same split secret, so it too can only materialize inside the attested enclave. See [Key Management](/cofhe/deep-dive/cofhe-components/key-management) for the ceremony that creates and distributes the shares.
## Result signing
Every `decrypt` result is signed so the TaskManager can verify it onchain. Teecryptor builds a fixed 76-byte message and signs its keccak256 hash with secp256k1:
| Field | Size | Encoding |
| - | - | - |
| `result` | 32 bytes | uint256, big-endian, left-padded with zeros |
| `enc_type` | 4 bytes | i32, big-endian |
| `chain_id` | 8 bytes | u64, big-endian |
| `ct_hash` | 32 bytes | uint256, big-endian |
This layout matches what the TaskManager reconstructs in `_computeDecryptResultHash`, so `FHE.publishDecryptResult(ctHash, result, signature)` verifies the signature onchain before storing the plaintext. Request the signature with the `X-Signature-V-Format: evm` header (the SDK does this for you) so the recovery id is 27/28 and directly compatible with Solidity's `ecrecover`.
Teecryptor's signer address is registered onchain as `decryptResultSigner` in the TaskManager. Only results signed by that address are accepted.
## Future plans
Teecryptor concentrates key custody at runtime in a single attested machine. The planned Threshold Network removes that concentration by decrypting through multi-party computation, where no party ever holds the full key. See [Future Plans](/cofhe/deep-dive/research/future-plans).
# Verify a commitment
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/cofhe-components/verify-commitments
Recompute a CoFHE ciphertext from public inputs and check your result against the commitment recorded onchain.
Every ciphertext CoFHE produces or verifies is anchored by a hash commitment. This page shows you how to check one without trusting the coprocessor: read the public inputs, run the same computation yourself, and compare your hash to the recorded one. Nothing here needs an account, a key, or permission from anyone.
## What the check proves
A commitment is `keccak256` over a ciphertext's stored bytes. Those bytes are deterministic. The same inputs, the same keys, and the same FHE parameters produce the same ciphertext, byte for byte, on your machine as on ours.
So the audit is a recomputation:
1. Read the operation and its input handles from the host chain.
2. Fetch the input ciphertexts from the public archive.
3. Fetch the public key material the network computes with.
4. Re-run the operation with those inputs.
5. Hash your result and compare it to the recorded commitment.
A match means the coprocessor ran the operation the contract asked for, and did not substitute a result. A mismatch means it did not, but read [When the hashes do not match](#when-the-hashes-do-not-match) before you draw that conclusion.
The check says nothing about plaintexts. You never learn what any value decrypts to, because you never hold a decryption key. That is the point: the computation is auditable while the data stays confidential.
## What you need
| Item | Value |
| - | - |
| CoFHEAddressBook | `0xC0F4e00E531a2B086492Ae3DCC1515038307196b`, the same address on every supported chain. `getTm(1)` returns that chain's TaskManager |
| Host chain RPC | Any endpoint for the chain your contract runs on |
| CommitmentRegistry | `0x4B82F827C03fad5739EFEcfcF743E65E732be695` on Arbitrum One, one address for every host chain |
| Verified input archive | The [fhenix-mainnet-zee-k-inputs](https://storage.googleapis.com/fhenix-mainnet-zee-k-inputs?prefix=cofhe/) bucket, public. See [Fetch the inputs](#fetch-the-inputs) |
| Key store | The [fhenix-mainnet-keys](https://storage.googleapis.com/fhenix-mainnet-keys?prefix=generator/keys/) bucket, public. See [Fetch the public key material](#fetch-the-public-key-material) |
| tfhe-rs | `1.5.1` |
| Tools | Foundry's `cast`, and a Rust toolchain for the recomputation |
You need one CommitmentRegistry address, not one per chain. A handle is derived from the ciphertext bytes, or from the operation and its input handles, and neither derivation mixes in a chain id. So handles are global, one registry records them all, and your host chain never appears in the lookup.
Both stores are publicly readable. The input archive fills as the network processes inputs. The bucket links above show the live listing.
## How a ciphertext is committed
Every ciphertext in CoFHE, whether an encrypted input or a computed result, is stored in one format: the tfhe-rs value, compressed with `compress()`, then written with tfhe's `safe_serialize`. The commitment is `keccak256` over exactly those bytes. The security zone is not in the preimage.
A handle is not the commitment, and the two relate differently depending on where the ciphertext came from.
| Ciphertext | Handle | Where its commitment lives |
| - | - | - |
| Encrypted input | The commitment with its last two bytes replaced by metadata: byte 30 carries the type and the trivial-encrypt flag, byte 31 carries the security zone | `InputVerified` on the host chain, and the registry |
| Computed result | A placeholder the TaskManager derives from the operation and its inputs, before the result exists | The registry only |
That difference is why the registry exists. A result's handle is fixed onchain the moment the task is created, so it cannot carry a hash of bytes nobody has computed yet.
Color marks the zone, and the boxes group what runs where. Only the last box is yours: everything else is a public read, and none of it needs an account or a key.
## Read the computation graph
The TaskManager publishes every operation it schedules. `TaskCreated` carries the result handle, the operation name, and up to three input handles.
The TaskManager address differs between mainnet and testnet, so read it from the CoFHEAddressBook. `FHE.sol` resolves it the same way. The examples below run on Arbitrum Sepolia. For another chain, change `RPC` only.
```bash theme={null}
RPC=https://arbitrum-sepolia-rpc.publicnode.com
ADDRESS_BOOK=0xC0F4e00E531a2B086492Ae3DCC1515038307196b
TASK_MANAGER=$(cast call --rpc-url $RPC $ADDRESS_BOOK "getTm(uint256)(address)" 1)
LATEST=$(cast block-number --rpc-url $RPC)
cast logs --rpc-url $RPC --address $TASK_MANAGER \
--from-block $((LATEST - 40000)) --to-block $LATEST \
"TaskCreated(uint256 ctHash, string operation, uint256 input1, uint256 input2, uint256 input3)"
```
One real event from Arbitrum Sepolia, decoded:
| Field | Value |
| - | - |
| `operation` | `add` |
| `input1` | `0x4e6570aadde2d096ceb8214bcb54cc0aac9fa1af1d6640e9cae40052a23c0500` |
| `input2` | `0x4765f9b1760b9612da755387f067d04051c2821bb4defc33ba58cbf940f00500` |
| `ctHash` | `0xf0a73bae25878afcc52cf176616b3e3aa93f3ef6fa701ed61282cdd17df70500` |
Operation names come from `Utils.functionIdToString` in [`ICofhe.sol`](https://github.com/FhenixProtocol/cofhe-contracts/blob/master/contracts/ICofhe.sol), which also defines the type constants. Byte 30 holds the type in its low seven bits. Its high bit is set when the value was [trivially encrypted](/cofhe/fhe-library/core-concepts/trivial-encryption), so mask the byte with `0x7f` before you compare it to those constants. Here every handle ends in `05 00`, which is `EUINT64_TFHE` in security zone 0. The same type, trivially encrypted, ends `85 00` and matches no constant until you mask it.
An input handle is either another task's result, in which case you follow it and recurse, or an encrypted input. Encrypted inputs terminate the graph, and each one has its commitment onchain:
```bash theme={null}
cast logs --rpc-url $RPC --address $TASK_MANAGER \
--from-block $((LATEST - 40000)) --to-block $LATEST \
"InputVerified(uint256 indexed ctHash, bytes32 commitment)"
```
The indexed topic is the handle, and the data word is the commitment. From a real event, unrelated to the task above:
```text theme={null}
handle 0x1b2cd55ff9b0cbef294dfdc4cfb80ea6450bf0a272e45f9b3715a27bed350500
commitment 0x1b2cd55ff9b0cbef294dfdc4cfb80ea6450bf0a272e45f9b3715a27bed35c582
```
The first 30 bytes are identical, and the handle's last two are the type byte and the zone. That relation is worth checking as you go, because it catches a mistyped handle before you spend a recomputation on it.
## Read the commitment
`getCommitment` takes the version tag and the handle, and returns the recorded hash. `bytes32(0)` means nothing is posted for that pair.
```bash theme={null}
REGISTRY=0x4B82F827C03fad5739EFEcfcF743E65E732be695
REGISTRY_RPC=https://arbitrum-one-rpc.publicnode.com
VERSION=0x0000000000000000000000000000000000000000000000000000000000000002
HANDLE=0xf0a73bae25878afcc52cf176616b3e3aa93f3ef6fa701ed61282cdd17df70500
cast call $REGISTRY "getCommitment(bytes32,bytes32)(bytes32)" \
$VERSION $HANDLE --rpc-url $REGISTRY_RPC
```
The handle here is the step 1 example; put the handle you are auditing in its place.
The version tag is the number 2 encoded as `bytes32`, so `0x00...02`. It is not the ASCII character `2`. `cast format-bytes32-string "2"` returns `0x3200...00`, and every lookup under that value comes back zero.
A recorded hash on its own is not enough. Every version carries a status, and the registry accepts new commitments only while that status is `Active`. Read the status before you trust a match:
```bash theme={null}
cast call $REGISTRY "getVersionStatus(bytes32)(uint8)" $VERSION --rpc-url $REGISTRY_RPC
```
The answer is `0` for Unset, `1` for Active, `2` for Deprecated, and `3` for Revoked. A status only moves forward: Active to Deprecated or Revoked, and Deprecated to Revoked. `Deprecated` means the version was superseded and its commitments stay readable. `Revoked` means the opposite. Do not trust a match under a revoked version.
A zero answer is not proof of tampering, and it has two ordinary causes.
The first is timing: a result that is still being computed has no commitment yet, because the coprocessor posts the commitment after the compute stage finishes.
The second is the version. More than one version can be Active at once, and a handle committed before a cutover resolves only under the version it was posted with. Read the status of each version you know of, and retry the lookup under the earlier one before you conclude anything.
To audit in bulk rather than one handle at a time, enumerate the version. `getSize` reports the total, and `getHandles` pages through it, clamping `offset + limit` at the total and returning an empty array once `offset` runs past the end.
```bash theme={null}
cast call $REGISTRY "getSize(bytes32)(uint256)" $VERSION --rpc-url $REGISTRY_RPC
cast call $REGISTRY "getHandles(bytes32,uint256,uint256)(bytes32[])" \
$VERSION 0 100 --rpc-url $REGISTRY_RPC
```
Keep `limit` small. See [CommitmentRegistry](/cofhe/deep-dive/cofhe-components/commitment-registry) for the write rules, the version state machine, and the rest of the read surface.
## Fetch the inputs
The [ZK Verifier](/cofhe/deep-dive/cofhe-components/zk-verifier) archives every input it verifies, together with the proof that covered it. This store holds inputs and proofs only, not the keys. Objects are laid out by chain and security zone, sharded by the first four hex characters of the handle:
```text theme={null}
cofhe/v1/chain/{chainId}/security-zone/{zone}/verified-inputs/{shard}/{handle}
cofhe/v1/chain/{chainId}/security-zone/{zone}/verification-proofs/{shard}/{proofId}
```
The handle is written without its `0x` prefix, and `{shard}` is its first four characters. Fetch an object anonymously: append its path to the archive base URL, with your handle and chain id from step 1. The archive holds an object once the network verified that input. The `-f` flag makes a missing object fail loudly instead of saving the error page.
```bash theme={null}
# Mainnet archive
ARCHIVE=https://storage.googleapis.com/fhenix-mainnet-zee-k-inputs
curl -fsSO $ARCHIVE/cofhe/v1/chain/{chainId}/security-zone/{zone}/verified-inputs/{shard}/{handle}
# The verified input above lives on testnet; this fetch runs as is.
ARCHIVE=https://storage.googleapis.com/testnet-v2-inputs
curl -fsSO $ARCHIVE/cofhe/v1/chain/421614/security-zone/0/verified-inputs/1b2c/1b2cd55ff9b0cbef294dfdc4cfb80ea6450bf0a272e45f9b3715a27bed350500
```
Each object is a bincode-encoded record holding the ciphertext bytes and its metadata. The metadata carries the handle, the proof it belongs to, the submitting account, the security zone, the chain, the index within the proof, and the type. Take the ciphertext bytes out of that record. They are the same `safe_serialize` of a compressed value as everything else, so `keccak256` over them reproduces the commitment you read from `InputVerified`. Check that before going further.
The metadata also gives you the proof id, which is where you find the matching object under `verification-proofs`. Verifying that proof is a separate check, and it tells you the input was well formed rather than that the computation was honest.
## Fetch the public key material
Recomputation needs three artifacts, all public and all specific to a security zone. They live in the key store, which is separate from the verified input archive. The object paths below append to its base URL, `https://storage.googleapis.com/fhenix-mainnet-keys/generator/`.
| Artifact | tfhe-rs type | Object in the key store |
| - | - | - |
| Evaluation key | `CompressedServerKey` | `keys/versionized/{zone}/computation_key` |
| Network public key | `CompactPublicKey` | `keys/versionized/{zone}/public_key`, or `GetNetworkPublicKey` |
| CRS | `CompactPkeCrs` | `keys/versionized/{zone}/crs`, or `GetCrs` |
A manifest sits alongside them at `keys/versionized/{zone}/public-material`, carrying a SHA-256 digest of each artifact. Check the artifacts you downloaded against it.
Two of the three are also served over HTTP by the coprocessor, at the CoFHE URL your chain's SDK config already holds. Both take a security zone and return hex:
```bash theme={null}
COFHE_URL=$(node -e "import('@cofhe/sdk/chains').then(c => console.log(c.arbSepolia.coFheUrl))")
curl -s -X POST $COFHE_URL/GetNetworkPublicKey \
-H 'Content-Type: application/json' \
-d '{"securityZone": 0}' | jq -r .publicKey | head -c 64
curl -s -X POST $COFHE_URL/GetCrs \
-H 'Content-Type: application/json' \
-d '{"securityZone": 0}' | jq -r .crs | head -c 64
```
Neither HTTP endpoint serves the evaluation key, and you cannot compute without it. Only the key store has it, at `keys/versionized/{zone}/computation_key` under the base URL.
## Re-run the operation
Start from a crate that pins tfhe-rs to the version the network runs:
```toml Cargo.toml theme={null}
[dependencies]
tfhe = { version = "=1.5.1", features = ["shortint", "integer", "boolean", "zk-pok"] }
tiny-keccak = { version = "2.0", features = ["keccak"] }
hex = "0.4"
```
A fresh resolve picks `tfhe-zk-pok` 0.8.3, which pulls a `tfhe-versionable` that tfhe 1.5.1 does not build against. The build fails inside tfhe itself with over a hundred trait-bound errors. Pin it back with `cargo update -p tfhe-zk-pok --precise 0.8.0`.
Then decompress the evaluation key, deserialize both inputs, apply the operation the event named, and serialize the result the way the coprocessor does:
```rust src/main.rs theme={null}
use tfhe::safe_serialization::{safe_deserialize, safe_serialize};
use tfhe::{CompressedFheUint64, CompressedServerKey};
use tiny_keccak::{Hasher, Keccak};
const SIZE_LIMIT: u64 = 1 << 30;
fn recompute_add(computation_key: &[u8], input1: &[u8], input2: &[u8]) -> Result<[u8; 32], String> {
let server_key: CompressedServerKey = safe_deserialize(computation_key, SIZE_LIMIT)?;
tfhe::set_server_key(server_key.decompress());
let lhs: CompressedFheUint64 = safe_deserialize(input1, SIZE_LIMIT)?;
let rhs: CompressedFheUint64 = safe_deserialize(input2, SIZE_LIMIT)?;
let result = lhs.decompress() + rhs.decompress();
// The coprocessor commits to the compressed, safe-serialized form. Any other
// encoding of the same ciphertext hashes to something else.
let mut stored = Vec::new();
safe_serialize(&result.compress(), &mut stored, SIZE_LIMIT).map_err(|e| e.to_string())?;
let mut hasher = Keccak::v256();
hasher.update(&stored);
let mut commitment = [0u8; 32];
hasher.finalize(&mut commitment);
Ok(commitment)
}
```
Compression is deterministic. It is a modulus switch whose noise-reduction step picks its candidate by a fixed measure, with no randomness. The same inputs give the same bytes on every run and every machine.
If the value this returns equals what `getCommitment` gave you for the result handle, the coprocessor computed honestly for that task. Walk the graph up from the encrypted inputs and you have checked the whole computation.
### Parameters the keyset uses
You do not need these to recompute, because the evaluation key carries them. They are here so you can confirm the keyset you downloaded is the one you expect:
| Role | tfhe-rs parameter set |
| - | - |
| Compute keys | `PARAM_MESSAGE_2_CARRY_2_KS_PBS` |
| Compact public key | V0\_11\_PARAM\_PKE\_MESSAGE\_2\_CARRY\_2\_KS\_PBS\_TUNIFORM\_2M64 |
| Key switching | V0\_11\_PARAM\_KEYSWITCH\_MESSAGE\_2\_CARRY\_2\_KS\_PBS\_TUNIFORM\_2M64 |
## When the hashes do not match
A mismatch is a serious claim, and the first ones are almost always a setup problem rather than a dishonest coprocessor. Work through these before you conclude anything:
* **The type.** Mask byte 30 of the handle with `0x7f`, and confirm it matches the type you deserialized as. Reading a `euint32` handle as a `CompressedFheUint64` gives bytes that hash to something else.
* **The security zone.** Byte 31 names the zone. The evaluation key, the public key, and the CRS are all per zone, so a keyset from the wrong zone produces a plausible result with the wrong bytes.
* **The serialized form.** Hash `safe_serialize` of the compressed value. The expanded value and the compressed one are the same ciphertext and different bytes. The size limit you pass is only a guard, and never reaches the hash.
* **The version and the task.** Confirm `getVersionStatus` returns `Active`, and that `getCommitment` returned a non-zero hash. A result still in the compute stage has no commitment yet.
* **The tfhe-rs version.** Confirm your pin is `=1.5.1`, the version the network runs.
If all of these hold and the hashes still differ, you have a real discrepancy between what the contract asked for and what the network recorded.
## Where this check runs in production
[Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) makes the same comparison inside its enclave on every decryption request. It hashes the ciphertext bytes it is about to decrypt and refuses any that do not match the commitment anchored onchain. Your audit and its gate read the same record, so a commitment you can check yourself is the same one that gates every decryption.
## Source
* [`CommitmentRegistry.sol`](https://github.com/FhenixProtocol/cofhe-contracts/blob/master/contracts/internal/registry-chain/contracts/commitment-registry/CommitmentRegistry.sol), the registry contract.
* [`TaskManager.sol`](https://github.com/FhenixProtocol/cofhe-contracts/blob/master/contracts/internal/host-chain/contracts/TaskManager.sol), which emits `TaskCreated` and `InputVerified`.
* [`ICofhe.sol`](https://github.com/FhenixProtocol/cofhe-contracts/blob/master/contracts/ICofhe.sol), for operation names and type constants.
# ZK Verifier
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/cofhe-components/zk-verifier
Offchain component that verifies user inputs using Zero-Knowledge Proofs of Knowledge (ZKPoK) to ensure encrypted data is safe to use in smart contracts
**ZK Verifier** checks every encrypted input before it can enter a confidential smart contract.
| Aspect | Description |
| - | - |
| **Type** | Offchain component running inside a hardware-attested TEE. |
| **Function** | Verifies the user's input, ensuring that it is safe to use. |
| **Responsibilities** | • Receives users' ZKPoKs for their input batches.
• Verifies those proofs.
• Signs one approval per verified batch.
• Stores the verified ciphertext bytes. |
## Why ZKPoK?
A **Zero-Knowledge Proof of Knowledge (ZKPoK)** lets a user prove they know the plaintext behind an encrypted input, without revealing that plaintext.
ZKPoKs protect against potential malicious vectors, including:
1. **Malleability attacks**: Without ZKPoK protection, attackers could transform observed ciphertexts into new valid-looking ones without knowing their contents, for example by combining them with encrypted zeros.
2. **Chosen ciphertext attacks (CCAs)**: Attackers submit modified ciphertexts and observe the results, potentially exploiting homomorphic operations to infer sensitive information or even recover the secret key.
Requiring a ZKPoK for each encrypted input is what makes it safe to run an encryption system in a public runtime like a blockchain. Only someone who knows the original plaintext can produce a valid proof.
## Sending encrypted inputs
When providing ciphertexts as an input to a smart contract, users have to generate a ZKPoK and get a verification approval first. The Client SDK (`@cofhe/sdk`) and `FHE.sol` handle most of this work; the mechanism is described here end to end (also in the diagram below).
Color marks the zone. Your app and the Client SDK run client side, your contract and the TaskManager on the host chain. The ZK Verifier and compute pipeline run offchain inside CoFHE, and the CommitmentRegistry sits on the registry chain. Hover any component or message in the [explorable version](/cofhe/deep-dive/cofhe-components/zk-verifier-explorable).
The process of sending inputs to a smart contract:
1. Your app encrypts the inputs through the Client SDK, which generates a ZK proof of knowledge for each one.
2. The Client SDK sends the ciphertexts and proofs to the ZK Verifier, as one batch.
3. The ZK Verifier verifies each proof. If all are valid, it stores the encrypted values in the CT Server and signs one message approving the whole batch.
4. The ZK Verifier returns the handles and the batch approval to your app.
5. Your app sends the handles and the approval as inputs to a contract call.
6. The TaskManager verifies the batch signature and emits `InputVerified` per input. The [compute pipeline](/cofhe/deep-dive/cofhe-components/compute-pipeline) picks those events up and posts a commitment for each input to the [CommitmentRegistry](/cofhe/deep-dive/cofhe-components/commitment-registry), so the input becomes decryptable later.
7. The contract performs the actual logic.
Most of this process is abstracted away. Steps 1 to 6 all happen behind the SDK and library calls, while step 7 (the actual logic) is up to you to write.
## Trust model
The ZK Verifier runs inside a hardware-attested TEE (Intel TDX). Its signing key is held as Shamir shares by independent partners and released only to the exact attested code image. Neither the operator nor anyone else can sign approvals outside the reviewed program. See [Key Management](/cofhe/deep-dive/cofhe-components/key-management) for how shares are created and released. After a successful verification, the component stores the ciphertext bytes in the CT Server. It also archives the inputs and their proofs in a public Google Cloud Storage bucket for independent auditing. The repo ships the checking tool: [`verify_stored_ct.rs`](https://github.com/FhenixProtocol/zee-k-verifier/blob/main/zk-verifier/scripts/verify_stored_ct.rs) fetches a stored ciphertext and checks it against its commitment. [Verify a commitment offline](/cofhe/deep-dive/cofhe-components/verify-commitments) covers the full procedure.
The signed message is verified onchain by the TaskManager using `ecrecover`; the verifier's signer address is registered there as `verifierSigner`.
## Signature format
For developers integrating without the SDK, the signature covers a batch digest. Each input is hashed, the hashes are concatenated in input order, and the digest of that concatenation is signed:
```text theme={null}
h_i = keccak256(ct_hash (32 bytes) || utype (1 byte) || security_zone (1 byte)
|| sender (20 bytes) || chain_id (32 bytes, big-endian) || consuming_contract (20 bytes))
digest = keccak256(h_0 || h_1 || ... || h_n)
```
The digest binds the sender and the consuming contract, so a verified batch cannot be replayed by another account or into another contract. The `recid` value the component returns (0 or 1) must be adjusted to 27 or 28 for Solidity's `ecrecover`.
## Verify a batch yourself
The format above is enough to check any verified batch against public data alone. This walkthrough recovers the signer of a real batch on Arbitrum Sepolia and compares it to the `verifierSigner` registered on the TaskManager.
You need the transaction that consumed the batch, plus the 65-byte approval. The approval is in the SDK's encrypt result, and in the transaction's calldata, because your app passed it into the contract call. Everything else comes from the chain:
* Each `InputVerified` event's `commitment` field is the `ct_hash` the signature covers. The indexed `ctHash` is the handle, which replaces the last two bytes with metadata, and is not signed.
* The handle's byte 30 is the type and byte 31 is the security zone, and both are in the preimage.
* The batch is bound to your contract's caller and to your contract. When your app calls the contract directly, those are `tx.from` and `tx.to`.
The script runs with Node 22 and [viem](https://viem.sh) 2.x, after `npm install viem`:
```js verify-batch.mjs theme={null}
import {
createPublicClient,
http,
keccak256,
encodePacked,
concat,
parseAbi,
parseEventLogs,
recoverAddress,
} from "viem";
// A real verified batch of two inputs, and the approval the ZK Verifier
// returned for it. Both values are yours to replace.
const txHash =
"0xbc05f5731b9dbfff352fb90f8c8cd341a24ca3167c7844eca9c5c2672d5e3463";
const signature =
"0x537a7735ed394dc7bb66868b26ba856afd924ba858acb5149c0d17483f2a58da383ef17ca84d13ef3a9cab878b5a2ac43ab064bf1ab432f2268a57d2597587091c";
const abi = parseAbi([
"event InputVerified(uint256 indexed ctHash, bytes32 commitment)",
"function verifierSigner() view returns (address)",
]);
const client = createPublicClient({
transport: http("https://arbitrum-sepolia-rpc.publicnode.com"),
});
const tx = await client.getTransaction({ hash: txHash });
const receipt = await client.getTransactionReceipt({ hash: txHash });
const events = parseEventLogs({ abi, logs: receipt.logs });
// One h_i per input, in event order, which is input order.
const hashes = events.map(({ args }) =>
keccak256(
encodePacked(
["uint256", "uint8", "uint8", "address", "uint256", "address"],
[
BigInt(args.commitment), // the signed ct_hash, not the handle
Number((args.ctHash >> 8n) & 0xffn), // utype, from the handle
Number(args.ctHash & 0xffn), // security zone, from the handle
tx.from,
BigInt(tx.chainId),
tx.to,
],
),
),
);
const digest = keccak256(concat(hashes));
// The TaskManager runs ecrecover on the raw digest: no Ethereum
// signed-message prefix.
const signer = await recoverAddress({ hash: digest, signature });
const registered = await client.readContract({
address: events[0].address, // the emitting contract is the TaskManager
abi,
functionName: "verifierSigner",
});
console.log(signer === registered ? "verified" : "signer mismatch");
```
The script prints `verified`. Change any byte of the preimage, including the order of the inputs, and recovery returns a different address. That is the property the TaskManager enforces when it rejects a batch with `InvalidSigner`.
The onchain half of this check is `batchVerifyInputs`, `inputMessageHash`, and `extractBatchSigner` in [`TaskManager.sol`](https://github.com/FhenixProtocol/cofhe-contracts/blob/master/contracts/internal/host-chain/contracts/TaskManager.sol). The signing half is public too: the component's source is [zee-k-verifier](https://github.com/FhenixProtocol/zee-k-verifier).
# Decryption Flow
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/data-flows/decryption-request-flow
How CoFHE values get decrypted: decryptForTx with onchain publication, decryptForView with sealed output
Decryption in CoFHE is SDK-driven and happens offchain, inside [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor). A contract's role is to grant access and, when the result should go onchain, to read it back after publication.
**Contracts cannot request decryption onchain.** The TaskManager rejects decrypt tasks (`DecryptFunctionNotSupported`), and `FHE.decrypt` no longer exists in the FHE library. The coprocessor never pushes plaintexts into your contract.
There are two SDK entry points, one per destination:
* **`decryptForTx`**: returns the plaintext with a Teecryptor signature you can publish onchain. Guide: [Decrypt to Tx](/cofhe/client-sdk/guides/decrypt-to-tx).
* **`decryptForView`**: returns the plaintext sealed to your [ACP](/cofhe/client-sdk/guides/acps) (Access Control Permission), for UI display and offchain reads. Guide: [Decrypt to View](/cofhe/client-sdk/guides/decrypt-to-view).
A freshly computed handle may not be decryptable immediately: its ciphertext and commitment land shortly after the transaction. Until then Teecryptor answers with a retryable status and the SDK re-submits automatically.
## The shared pipeline
Both entry points start the same way.
The contract that owns the encrypted value marks the handle decryptable: `FHE.allow(handle, account)` for a specific account, or `FHE.allowGlobal(handle)` when the value may become public. Without an ACL grant, Teecryptor refuses the request.
The application calls one of the two builders:
```typescript theme={null}
const { ctHash, decryptedValue, signature } = await client
.decryptForTx(ctHash)
.withACP()
.execute();
```
```typescript theme={null}
const balance = await client
.decryptForView(ctHash, FheTypes.Uint32)
.withACP()
.execute();
```
The request goes to Teecryptor with the handle, the host chain id, and the ACP. For publicly decryptable handles, `decryptForTx` can use `.withoutACP()` instead.
Teecryptor runs two onchain checks at the same time. It reads the ACL grant on the host chain, and the handle's commitment in the [CommitmentRegistry](/cofhe/deep-dive/cofhe-components/commitment-registry) on the registry chain. The two live on different chains, so neither check waits for the other. Teecryptor then fetches the ciphertext from the CT Server, exactly as stored, checks that the bytes hash to that commitment, and decrypts inside the attested enclave. See the [Teecryptor page](/cofhe/deep-dive/cofhe-components/teecryptor) for the full pipeline.
From here the two paths diverge.
## The transaction path
For `decryptForTx`, Teecryptor returns the plaintext together with an ECDSA signature over a fixed 76-byte message (result, encryption type, chain id, ciphertext hash). The signing key lives only inside the enclave, and its address is registered onchain as the TaskManager's `decryptResultSigner`.
Anyone holding the signature submits it in a transaction:
```solidity theme={null}
FHE.publishDecryptResult(ctHash, result, signature);
```
The TaskManager recomputes the message hash, recovers the signer, and rejects anything not signed by `decryptResultSigner`. On success it stores the plaintext in PlaintextsStorage and emits a `DecryptionResult` event. `publishDecryptResultBatch` amortizes gas across multiple results.
Publication is permissionless: any relayer with a valid signature can deliver the result. `decryptForTx` itself costs no gas; gas is paid only by this publish transaction.
Once published, any contract reads the plaintext:
```solidity theme={null}
uint64 value = FHE.getDecryptResult(handle); // reverts if not yet published
(uint64 value, bool ready) = FHE.getDecryptResultSafe(handle); // non-reverting variant
```
To check a signature without storing the result, use the `verifyDecryptResult` family on the TaskManager.
## The view path
For `decryptForView`, Teecryptor never returns a bare plaintext. It encrypts the result to the ACP's sealing key, and the SDK unseals it locally, so the plaintext is not exposed in transit. There is nothing to publish; the value goes straight to your application.
## Flow diagram
Your app and the Client SDK run client side, your contract and the TaskManager on the host chain, Teecryptor offchain inside CoFHE, and the CommitmentRegistry on the registry chain. Hover any component or message in the [explorable version](/cofhe/deep-dive/data-flows/decryption-request-flow-explorable).
## Comparison
| | `decryptForTx` | `decryptForView` |
| - | - | - |
| **Returns** | Plaintext + Teecryptor signature | Plaintext (sealed in transit, unsealed by the SDK) |
| **Use case** | Submit the decrypted value onchain | Display in a UI, offchain reads |
| **Requires ACP** | Only if the handle is not publicly decryptable | Yes |
| **Onchain verification** | publishDecryptResult or verifyDecryptResult | Not applicable |
| **Gas cost** | None for the decryption itself; gas only for the publish transaction | None |
## Future plans
Decryption is currently performed by Teecryptor inside a hardware-attested TEE. A multi-party Threshold Network is the planned successor; see [Future Plans](/cofhe/deep-dive/research/future-plans).
# Encryption Request Flow
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/data-flows/encryption-request-flow
How a plaintext becomes a verified encrypted input: local encryption, ZK proof, verification, and onchain use
This page follows encrypted inputs from the plaintext in your application to handles a smart contract can compute on. Everything sensitive happens client-side: the values are encrypted and proven locally, and only the ciphertexts and their proofs ever leave the user's machine. Inputs travel as one batch, verified with a single signature that is bound to the contract that will consume them.
## Key components
| Component | Runs in | Description |
| - | - | - |
| **Your app** | Client side | The application that interacts with the user and the contracts |
| **Client SDK** (`@cofhe/sdk`) | Client side | TypeScript library that encrypts inputs locally and submits them for verification |
| **Your contract** | Host chain | Consumes the handles and the proof, and forwards them to the TaskManager |
| **[TaskManager](/cofhe/deep-dive/cofhe-components/task-manager)** | Host chain | Verifies the ZK Verifier's batch signature when the inputs are used onchain |
| **[ZK Verifier](/cofhe/deep-dive/cofhe-components/zk-verifier)** | CoFHE, offchain | Attested offchain component that verifies the proofs of every encrypted-input batch |
| **CT Server** | CoFHE, offchain | Stores the verified ciphertext bytes. Not drawn in the diagram; the ZK Verifier writes to it during verification |
| **[Compute pipeline](/cofhe/deep-dive/cofhe-components/compute-pipeline)** | CoFHE, offchain | Picks up each `InputVerified` event and posts the commitment |
| **[CommitmentRegistry](/cofhe/deep-dive/cofhe-components/commitment-registry)** | Registry chain | Records a commitment for every verified input |
## Flow diagram
Each color in the diagram is one zone from the table above. Hover any component or message in the [explorable version](/cofhe/deep-dive/data-flows/encryption-request-flow-explorable).
## Step-by-step flow
Install and initialize the Client SDK in your project. Full details are in the [installation guide](/cofhe/client-sdk/introduction/installation).
```bash theme={null}
npm install @cofhe/sdk
```
```javascript theme={null}
const { Encryptable, FheTypes } = require("@cofhe/sdk");
const { createCofheConfig, createCofheClient } = require("@cofhe/sdk/node");
const { chains } = require("@cofhe/sdk/chains");
const config = createCofheConfig({ supportedChains: [chains.sepolia] });
const cofheClient = createCofheClient(config);
await cofheClient.connect(publicClient, walletClient);
```
The application encrypts its values with a single builder call, naming the contract that will consume them:
```typescript theme={null}
const [encryptedInput, proof] = await cofheClient
.encryptInputs([Encryptable.uint32(42n)])
.setConsumingContract(contractAddress)
.execute();
```
Internally, `encryptInputs` encrypts each value with the TFHE library and generates a zkPoK (zero-knowledge proof of knowledge) that the encryption is correct. It then submits the whole batch to the ZK Verifier. The verifier's signature is bound to the consuming contract, so the batch cannot be replayed into a different one.
The ZK Verifier checks each proof inside its attested enclave. A valid proof shows the ciphertext is a correct, untampered encryption of a known plaintext. On success, the verifier stores the ciphertext bytes in the CT Server and returns the handles with one signature covering the batch. The SDK hands your application the tuple `[handles..., proof]`; each handle is an `external` encrypted value such as `externalEuint32`.
The application passes the handles and the proof to the contract as encrypted inputs. When the contract consumes them, the TaskManager's `batchVerifyInputs` checks the ZK Verifier's signature over the batch and emits an `InputVerified` event per input. The [compute pipeline](/cofhe/deep-dive/cofhe-components/compute-pipeline) picks those events up and posts a commitment for each input to the CommitmentRegistry. Inputs are anchored onchain the same way every computed result is.
Read more about the client-side API in [Encrypting Inputs](/cofhe/client-sdk/guides/encrypting-inputs).
# FHE Operation Request Flow
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/data-flows/fhe-operation-request-flow
How an FHE operation travels from a smart contract call through the coprocessor to an onchain commitment
This page follows a single FHE operation from the contract call that requests it to the commitment that anchors its result onchain. The request itself is synchronous: the contract gets a handle for the result immediately, while the coprocessor computes the actual ciphertext in the background.
## Key components
| Component | Runs in | Description |
| - | - | - |
| **Your contract** | Host chain | Requests FHE operations through the FHE.sol library |
| **FHE.sol** | Host chain | The Solidity library providing FHE operation functions |
| **[TaskManager](/cofhe/deep-dive/cofhe-components/task-manager)** | Host chain | Validates requests, checks the [ACL](/cofhe/deep-dive/cofhe-components/acl), and emits task events |
| **[Compute pipeline](/cofhe/deep-dive/cofhe-components/compute-pipeline)** | CoFHE, offchain | Picks up task events, validates and orders the work, executes it, and commits the result |
| **[CommitmentRegistry](/cofhe/deep-dive/cofhe-components/commitment-registry)** | Registry chain | Records a hash commitment for every result ciphertext |
## Flow diagram
Each color is one zone from the table above. Hover any component or message in the [explorable version](/cofhe/deep-dive/data-flows/fhe-operation-request-flow-explorable).
## Step-by-step flow
The application encrypts its input client-side and proves it valid, producing an encrypted-input handle plus a batch proof the contract can accept. The [Encryption Request Flow](/cofhe/deep-dive/data-flows/encryption-request-flow) covers this step.
This step happens on the client side, before any blockchain interaction.
Import the FHE library in Solidity:
```solidity theme={null}
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
```
Call the appropriate FHE function from the imported library:
```solidity theme={null}
// Using trivial encrypt or the handle and proof from the previous step.
function addExample(externalEuint32 input, bytes calldata proof) public {
euint32 lhs = FHE.asEuint32(input, proof);
euint32 rhs = FHE.asEuint32(10);
// Request an operation (addition in this example)
euint32 result = FHE.add(lhs, rhs);
}
```
FHE.sol forwards the request to the TaskManager contract.
The TaskManager is the gateway for all FHE operation requests. It:
1. Validates the request structure, so all inputs are properly formatted.
2. Verifies access permissions: the caller must have ACL access to every encrypted input.
3. Generates a unique handle that will reference the future ciphertext result.
4. Returns the handle to the calling contract, synchronously. Subsequent operations can chain on it right away.
5. Emits a `TaskCreated` event with the operation details for the offchain components.
The [compute pipeline](/cofhe/deep-dive/cofhe-components/compute-pipeline) subscribes to TaskManager events on every host chain and picks up the `TaskCreated` event. It checks that the operation is well formed and that the inputs it references exist. An operation that arrives before its inputs finish computing is deferred and released once they land.
The pipeline then:
1. Executes the requested operation on the encrypted data.
2. Stores the result ciphertext in the ciphertext store, keyed by the handle.
3. Releases any dependent operations that were deferred while waiting for this result.
The pipeline produces a commitment for the result, the hash of the stored ciphertext bytes, and posts it in a batch to the CommitmentRegistry on the registry chain. The commitment anchors the result. Teecryptor will only decrypt ciphertext bytes that hash to a registered commitment.
At this point the operation cycle is complete, and the confidentiality of every encrypted value is preserved.
# Future Plans
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/research/future-plans
Roadmap for CoFHE decentralization, upcoming features, and planned improvements
## Road to decentralization
Integrating FHE into a blockchain runtime is a hard and complex task. Our engineering philosophy is to ship fast: to build the best possible product we need to meet real users early. Similar to the approach described in [Vitalik's "training wheels" post](https://ethereum-magicians.org/t/proposed-milestones-for-rollups-taking-off-training-wheels/11571) (in the context of rollups), CoFHE ships with training wheels of its own.
Outlined here is a non-exhaustive list of trust points, centralized components, and compromises made to ship CoFHE to users as fast as possible. Each row notes how we plan to address it, and the list will be updated as things progress.
| Component | Compromise | Plan to solve | Status |
| - | - | - | - |
| Teecryptor | Decryption is served by a single TEE component operated by Fhenix | Replace with multi-party computation (below) | Planned |
| Teecryptor | Trust in the TEE hardware vendor and its attestation chain | Multi-party computation removes the hardware trust anchor | Planned |
| ZK Verifier | Input proofs are checked by a single TEE component operated by Fhenix | Move to multi-party computation, with each partner verifying inputs on its own side | Planned |
| Key custody | The FHE key is reconstructed inside one attested enclave at runtime | Threshold decryption, where no party ever holds the full key | Planned |
| CoFHE | Trust in CoFHE to perform correct FHE computations | External verification through an AVS (actively validated service) | Planned |
| CoFHE | User inputs are held by a single cloud provider | No current plan. The inputs and proofs are public, so anyone can audit them | Not planned |
| All | Codebase is unaudited | Perform a security audit | Planned |
| All | Codebase is not fully open source | Open-source the codebase | Planned |
## The path to multi-party computation
CoFHE runs two TEE components today, [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) and the [ZK Verifier](/cofhe/deep-dive/cofhe-components/zk-verifier). Both move to multi-party computation, and they move together. No TEE stays on the critical path after that.
Decryption becomes a Threshold Network. Independent parties decrypt through multi-party computation, so no single party (and no single machine) ever holds the FHE key. Robustness goals for it include a t-out-of-n protocol (no requirement that every party stays online), party share rotation, and full protocol key rotation.
Input verification follows the same model. Each partner runs ZK verification on its own side, instead of one attested enclave checking every proof and signing the batch alone.
See [Research in Fhenix](/cofhe/deep-dive/research/research-in-fhenix) for the threshold decryption protocol behind this plan.
## Upcoming features
In the spirit of transparency, here is the general feature roadmap planned for CoFHE.
| Feature | Type | Description | Status |
| - | - | - | - |
| RNG | DevX | Secure randomness in contracts (`FHE.random`) | Shipped |
| Additional host chains | DevX/UX | Multiple EVM host chains (three today) | Shipped, more planned |
| Hardhat and Foundry plugins | DevX | First-party tooling for the standard EVM dev stacks | Shipped |
| React SDK | DevX | `@cofhe/react` hooks and components for dApp frontends | Shipped |
| Additional external devtools | DevX | Remix, Alchemy SDK and more | Planned |
| GPU support | UX | Run FHE operations on a GPU backend for lower latency | Planned |
| FPGA support | UX | Run FHE operations on an FPGA backend | Planned |
| FHE ops in view functions | DevX | Execute FHE operations in contract view functions | Planned |
| Alternative runtimes | DevX | Support for runtimes other than the EVM | Planned |
# Research in Fhenix
Source: https://cofhe-docs.fhenix.zone/cofhe/deep-dive/research/research-in-fhenix
Research into novel cryptographic techniques and optimization of Fully Homomorphic Encryption for blockchain applications
Our research explores novel cryptographic techniques to push the boundaries of Fully Homomorphic Encryption (FHE). We mainly work on optimizing the latency of FHE-based smart contracts using the latest published schemes, keeping them both efficient and secure. Ultimately, the aim is to broaden the practical viability of FHE by delivering protocols that meet real-world performance needs.
## Threshold decryption for FHE
Decryption in CoFHE is currently served by [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor), a TEE-based component. The research below is the path to its planned decentralized successor, the [Threshold Network](/cofhe/deep-dive/research/future-plans).
We designed a new threshold FHE decryption protocol that improves throughput by roughly 20,000x and cuts latency by up to 37x relative to the prior state of the art. This is achieved by securely removing ciphertext noise with an efficient MPC-based approach, eliminating the need for noise flooding while maintaining strong simulation-based security.
# Call a token from your contract
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/calling-from-contracts
Move confidential tokens from another contract by sharing encrypted amounts with the token and receiving what it returns
When your contract moves confidential tokens, it passes encrypted amounts to the token and gets encrypted amounts back. Both directions use `sharedEuint64`, a handle tagged with who shared it and with whom. This page shows the pattern on a vault, and it applies to [FHERC20](/cofhe/fhe-library/confidential-contracts/fherc20/overview) and [ERC20Confidential](/cofhe/fhe-library/confidential-contracts/dual-mode/overview) alike.
## Why amounts cross as shared values
FHE operations check the permission of the contract performing them, not of whoever called it. Suppose a token accepted a bare `euint64` from any caller. That caller could pass a handle only the token may use, such as someone's balance, and get back a value derived from it. A share records the sharer and the intended receiver, and lasts one transaction, so the receiver can check who handed the value over. [Passing encrypted values between contracts](/cofhe/fhe-library/core-concepts/inputs#passing-encrypted-values-between-contracts) covers the mechanism.
For a token call, that means two steps around every call:
1. Share the amount with the token: `FHE.shareEuint64(amount, address(token))`.
2. Receive the result from the token: `FHE.receiveEuint64FromCall(result, address(token))`.
Name the token in both. In `receiveEuint64FromCall`, the address must be the contract you called in that same expression. Naming any other trusted address checks who created the share, not who handed it to you.
## A vault that holds deposits
This vault takes deposits with `confidentialTransferFrom` and pays out with `confidentialTransfer`. Each deposit is kept as an encrypted balance the depositor can decrypt:
```solidity ConfidentialVault.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { FHE, euint64, externalEuint64, sharedEuint64 } from "@fhenixprotocol/cofhe-contracts/FHE.sol";
import { IERC7984 } from "fhenix-confidential-contracts/contracts/interfaces/IERC7984.sol";
contract ConfidentialVault {
IERC7984 public immutable token;
mapping(address => euint64) private _deposits;
constructor(IERC7984 token_) {
token = token_;
}
function deposit(externalEuint64 encryptedAmount, bytes calldata inputProof) external {
euint64 amount = FHE.asEuint64(encryptedAmount, inputProof);
sharedEuint64 moved = token.confidentialTransferFrom(
msg.sender,
address(this),
FHE.shareEuint64(amount, address(token))
);
euint64 received = FHE.receiveEuint64FromCall(moved, address(token));
euint64 updated = FHE.add(_deposits[msg.sender], received);
FHE.allowThis(updated);
FHE.allow(updated, msg.sender);
_deposits[msg.sender] = updated;
}
function withdraw(externalEuint64 encryptedAmount, bytes calldata inputProof) external {
euint64 requested = FHE.asEuint64(encryptedAmount, inputProof);
euint64 amount = FHE.select(FHE.lte(requested, _deposits[msg.sender]), requested, FHE.asEuint64(0));
sharedEuint64 moved = token.confidentialTransfer(msg.sender, FHE.shareEuint64(amount, address(token)));
euint64 sent = FHE.receiveEuint64FromCall(moved, address(token));
euint64 updated = FHE.sub(_deposits[msg.sender], sent);
FHE.allowThis(updated);
FHE.allow(updated, msg.sender);
_deposits[msg.sender] = updated;
}
function depositOf(address account) external view returns (euint64) {
return _deposits[account];
}
}
```
Three details carry the design:
* **Credit what moved, not what was asked.** A transfer that exceeds the sender's balance moves zero instead of reverting. The vault credits `received`, the amount the token returns, so a failed deposit credits nothing.
* **Clamp before you send.** `withdraw` uses `FHE.select` to replace an over-deposit request with zero. The contract cannot revert on an encrypted comparison without revealing it.
* **Persist with `allowThis`.** A received handle carries access for this transaction only. Values you store must get `FHE.allowThis`, and `FHE.allow` for the account that should decrypt them. See [access control](/cofhe/fhe-library/core-concepts/access-control).
`IERC7984` is the interface both token families implement, so the vault works with either. The shared overloads of `confidentialTransfer` and `confidentialTransferFrom` are selected by the `sharedEuint64` argument type.
## Call the vault from your app
`confidentialTransferFrom` runs with the vault as `msg.sender`, so the depositor must first make the vault an [operator](/cofhe/fhe-library/confidential-contracts/fherc20/operators). Without it, the deposit reverts with `FHERC20UnauthorizedSpender` on FHERC20, or `ERC20ConfidentialUnauthorizedSpender` on ERC20Confidential.
Encrypt the amount with the vault as the consuming contract, because the vault calls `FHE.asEuint64`, not the token:
```typescript theme={null}
import { Encryptable } from '@cofhe/sdk';
const vaultAddress = await vault.getAddress();
const until = Math.floor(Date.now() / 1000) + 600;
await token.setOperator(vaultAddress, until);
const [amount, inputProof] = await client
.encryptInputs([Encryptable.uint64(40_000_000n)])
.setConsumingContract(vaultAddress)
.execute();
await vault.deposit(amount, inputProof);
```
An input encrypted for the token instead of the vault fails with `InvalidSigner`. The same happens when one account encrypts and another sends the transaction: encrypt with the client of the account that calls the vault. See [encrypting inputs](/cofhe/client-sdk/guides/encrypting-inputs).
## What goes wrong
| Error | Cause |
| - | - |
| `NotShared` | The receiver found no share for it. An EOA calling a `sharedEuint64` overload directly hits this, because only a contract can share a value. Wallets use the `externalEuint64` overloads |
| `UnexpectedSharer` | The share came from a different address than the receiver checked for |
| `SenderNotAllowed` | Your contract shared a handle it is not allowed to use |
| `InvalidSigner` | The input was encrypted for a different contract or account |
These errors come from the TaskManager. See [common errors](/cofhe/fhe-library/core-concepts/common-errors).
## Receive tokens with a callback
To react when tokens arrive, rather than pulling them, implement `IERC7984Receiver`. The token calls it during `confidentialTransferAndCall` with the amount as a `sharedEuint64`, which you receive with `FHE.receiveEuint64Param`. See [transfer callbacks](/cofhe/fhe-library/confidential-contracts/fherc20/transfer-callbacks).
# ERC20Confidential best practices
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/dual-mode/best-practices
What to check before you ship a token built on ERC20Confidential
These are the mistakes that are easiest to make with `ERC20Confidential`. Each one links to the page that explains the behavior behind it.
## Treat shield and unshield amounts as public
Privacy covers balances and transfers inside the confidential layer, not the boundary. The amount passed to `shield` is in the call data and in the `TokensShielded` event. The amount passed to `unshield(uint64)` is in the call data, and every claim reveals its amount when it settles.
Tell users that what they move in and out is visible. A user who shields 1,000 tokens and later unshields 1,000 links the two. See [shield and unshield](/cofhe/fhe-library/confidential-contracts/dual-mode/shield-unshield).
## Check the amount that actually moved
A confidential transfer or unshield of more than the balance does not revert. It moves zero. Every transfer returns the amount that moved as a `sharedEuint64`, so a contract that credits a user should credit that returned amount, never the amount it asked for. See [call a token from your contract](/cofhe/fhe-library/confidential-contracts/calling-from-contracts).
## Work in confidential units
Transfers, `unshield`, and `_confidentialMint` take confidential units, while `shield` and every ERC-20 function take public base units. For an 18-decimal token one confidential unit is 10^12 base units, and `shield` leaves anything below that in the public balance. Read `confidentialDecimals()` instead of assuming 6. See [decimals and units](/cofhe/fhe-library/confidential-contracts/dual-mode/overview#decimals-and-units).
## Use the plaintext supply, or sync before reading the handle
`confidentialTotalSupplyPlaintext()` is always current. The `confidentialTotalSupply()` handle goes stale when someone transfers public tokens straight to the pool, and stays stale until anyone calls `syncConfidentialTotalSupply()`. It is also the zero handle until the first shield. See [what total supply counts](/cofhe/fhe-library/confidential-contracts/dual-mode/dual-balance-model#what-total-supply-counts).
## Gate both balances if you freeze accounts
`_beforeConfidentialMove` covers confidential transfers and unshield only. A freeze built on it alone still lets the account use its public balance, shield, and receive claim payouts. Override `_update` as well. See [freeze accounts](/cofhe/fhe-library/confidential-contracts/dual-mode/extending#freeze-accounts).
## Backfill the observer when you set it
A compliance observer only gets access to handles created after it is set. Call `grantObserverPast` with the existing balance handles, or the observer cannot read balances that have not moved since. See [add a compliance observer](/cofhe/fhe-library/confidential-contracts/dual-mode/extending#add-a-compliance-observer).
## Keep operators short-lived
A confidential operator can move any amount of the holder's confidential balance until its deadline. It is separate from the public allowance: `approve` grants nothing on the confidential side, and `setOperator` grants nothing on the public side. Set a deadline that covers the action you need, and set it to `0` when you are done. See [operators](/cofhe/fhe-library/confidential-contracts/dual-mode/confidential-operations#operators).
## Link the library and enable the optimizer
Deploy `ERC20ConfidentialLib` from `fhenix-confidential-contracts@0.4.0` and link every token to it; the address cannot change after deployment. Compile with the optimizer on, so added functions do not push the token past the 24 KB contract size limit. See [link the shared library](/cofhe/fhe-library/confidential-contracts/overview#link-the-shared-library).
# Confidential transfers and operators
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/dual-mode/confidential-operations
How encrypted transfers and operators work on ERC20Confidential, and where they differ from FHERC20
`ERC20Confidential` moves confidential balances with the same functions as [FHERC20](/cofhe/fhe-library/confidential-contracts/fherc20/core-features). This page lists those functions and the few places where `ERC20Confidential` behaves differently. Read the FHERC20 page for the full behavior of each overload.
## The transfer functions
Every transfer function comes in two overloads: one for a wallet, which passes an encrypted input and its proof, and one for a contract, which passes a `sharedEuint64`.
| Function | Wallet overload | Contract overload |
| - | - | - |
| `confidentialTransfer` | `(to, externalEuint64, bytes inputProof)` | `(to, sharedEuint64)` |
| `confidentialTransferFrom` | `(from, to, externalEuint64, bytes inputProof)` | `(from, to, sharedEuint64)` |
| `confidentialTransferAndCall` | `(to, externalEuint64, bytes inputProof, bytes data)` | `(to, sharedEuint64, bytes data)` |
| `confidentialTransferFromAndCall` | `(from, to, externalEuint64, bytes inputProof, bytes data)` | `(from, to, sharedEuint64, bytes data)` |
Each returns a `sharedEuint64` holding the amount that actually moved. From a wallet, encrypt the amount in confidential units and name the token as the consuming contract:
```typescript theme={null}
import { Encryptable } from '@cofhe/sdk';
const [amount, inputProof] = await client
.encryptInputs([Encryptable.uint64(3_000_000n)])
.setConsumingContract(await token.getAddress())
.execute();
await token['confidentialTransfer(address,bytes32,bytes)'](recipient, amount, inputProof);
```
The account that encrypts the input must be the account that sends the transaction. For calls from a contract, see [call a token from your contract](/cofhe/fhe-library/confidential-contracts/calling-from-contracts).
As on FHERC20, a transfer of more than the balance does not revert. It moves zero, and the returned amount decrypts to zero.
## Where it differs from FHERC20
| Behavior | `ERC20Confidential` | `FHERC20` |
| - | - | - |
| Sender has never held a confidential balance | The transfer moves zero | Reverts with `FHERC20ZeroBalance` |
| Caller is not `from` or an operator | `ERC20ConfidentialUnauthorizedSpender(holder, spender)` | `FHERC20UnauthorizedSpender(holder, spender)` |
| `to` is the zero address | `ConfidentialInvalidReceiver(address(0))` | `FHERC20InvalidReceiver(address(0))` |
| Hook before each move | `_beforeConfidentialMove(from, to)` | None |
| Indicator updates | On the separate [indicator token](/cofhe/fhe-library/confidential-contracts/dual-mode/dual-balance-model#the-indicator-token) | On the token's own `balanceOf` |
`_beforeConfidentialMove` runs before all eight transfer functions and both `unshield` overloads. It does nothing by default. Override it to freeze accounts or pause the confidential side; see [extend ERC20Confidential](/cofhe/fhe-library/confidential-contracts/dual-mode/extending).
All eight transfer functions and both `unshield` overloads are `nonReentrant`, so a callback cannot call another transfer or `unshield` on the same token.
## Operators
An operator can move any amount of a holder's confidential balance until a deadline:
```typescript theme={null}
const oneHour = Math.floor(Date.now() / 1000) + 3600;
await token.setOperator(operatorAddress, oneHour);
```
`isOperator(holder, spender)` returns `true` while `block.timestamp` is at or before the deadline, and always returns `true` when `holder` and `spender` are the same address. Setting the deadline to `0` revokes the operator. Each call emits `OperatorSet(holder, operator, until)`. For the operator model in depth, see [operators](/cofhe/fhe-library/confidential-contracts/fherc20/operators).
Confidential operators and public allowances are separate. `approve` and `transferFrom` keep working on the public balance and have no effect on the confidential one, and `setOperator` grants nothing on the public side.
## Transfers with a callback
`confidentialTransferAndCall` and `confidentialTransferFromAndCall` call `onConfidentialTransferReceived` on the recipient after the transfer, using the same `IERC7984Receiver` interface as FHERC20. If the receiver returns an encrypted `false`, the token moves the amount back to the sender. See [transfer callbacks](/cofhe/fhe-library/confidential-contracts/fherc20/transfer-callbacks) for how to write a receiver.
## Events
| Event | When |
| - | - |
| `ConfidentialTransfer(from, to, amount)` | Every confidential move, including shield (`from` is zero) and unshield (`to` is zero). `amount` is the handle of the moved amount |
| `OperatorSet(holder, operator, until)` | `setOperator` |
The sender and the recipient can both decrypt the `amount` handle of a transfer they took part in.
# How the two balances relate
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/dual-mode/dual-balance-model
The pool that backs ERC20Confidential's encrypted balances, how total supply is counted, and the indicator token
Every confidential token on an `ERC20Confidential` contract is backed by public tokens that the contract holds aside. This page explains where those tokens sit, what each supply function counts, and what the indicator token shows. Read it before you build accounting or analytics on top of the token.
## Where the backing tokens sit
Public tokens backing confidential balances sit in the public balance of a fixed address, `CONFIDENTIAL_POOL`:
```text theme={null}
0x1011000000000000000000000000000000000000
```
The contract moves tokens in and out of it with internal transfers:
| Operation | Public balance | Pool | Confidential balance |
| - | - | - | - |
| `shield(amount)` | Caller's balance decreases | Increases | Caller's balance increases by `amount / rate` |
| `unshield(amount)` | No change | No change | Caller's balance decreases, a claim opens |
| `claimUnshielded(...)` | Recipient's balance increases | Decreases | No change |
| `_confidentialMint(to, amount)` | No change | Increases by newly minted tokens | `to`'s balance increases |
`unshield` leaves the pool alone. The pool keeps custody of the tokens until the claim settles, so a pending claim is still backed.
Because the pool is an ordinary address, anyone can send public tokens to it with `transfer`. Those tokens back no confidential balance, and the token has no function that returns them.
## What total supply counts
`totalSupply()` is the OpenZeppelin total of all public balances, and the pool is one of them. Shielding and unshielding move tokens between addresses, so they never change `totalSupply()`. Only public mints and burns change it, including the public mint inside `_confidentialMint`.
The confidential supply is the pool's balance converted to confidential units. The token exposes it two ways:
| Function | Returns | When it updates |
| - | - | - |
| confidentialTotalSupplyPlaintext | `uint256`, the pool's balance divided by the rate | On every read, so it is always current |
| confidentialTotalSupply | `euint64` handle that anyone can decrypt | On `shield`, `claimUnshielded`, `_confidentialMint`, and `syncConfidentialTotalSupply` |
The confidential supply is not secret: anyone can compute it from the pool's public balance. `confidentialTotalSupply` exists for contracts that need the value as a ciphertext for FHE operations. The handle is the zero handle until the first shield, claim, or confidential mint.
A transfer straight into the pool raises `confidentialTotalSupplyPlaintext` immediately but leaves the stored handle stale. Anyone can refresh it by calling `syncConfidentialTotalSupply()`.
The stored value saturates at `type(uint64).max` rather than reverting, so a large transfer into the pool cannot block `shield` or claims.
## The indicator token
The `ERC20Confidential` constructor deploys a second contract, `ERC20ConfidentialIndicator`, and records it as `indicatorToken()`. Wallets and block explorers that only understand ERC-20 can track it to see that confidential activity happened, without seeing amounts.
| Property | Value |
| - | - |
| `name()` | `"1011000 "` followed by the token's name, for example `1011000 Dual Token` |
| `symbol()` | `"c"` followed by the token's symbol, for example `cDUAL` |
| `decimals()` | `4` |
| `balanceOf(account)` | `1011000.0000` before any confidential activity |
Each confidential move updates the indicator for both parties. The first move into an account sets its indicator to `1011000.5001`, and each later one adds `0.0001`, up to `1011000.9999`. The first move out sets it to `1011000.4999`, and each later one subtracts `0.0001`, down to `1011000.0001`. The indicator also emits a `Transfer` event for each move, so the activity shows in transaction history.
The indicator's `transfer`, `transferFrom`, `approve`, and `allowance` revert with `ERC20ConfidentialIndicatorNoOp`. Only the parent token can update it.
## Read both balances
The public balance is a plain view call. The confidential balance is a handle, which the holder decrypts with an Access Control Permission (ACP) through [`decryptForView`](/cofhe/client-sdk/guides/decrypt-to-view):
```typescript theme={null}
import { FheTypes } from '@cofhe/sdk';
const publicBalance = await token.balanceOf(account);
const handle = await token.confidentialBalanceOf(account);
const confidentialBalance = await client
.decryptForView(handle, FheTypes.Uint64)
.execute();
```
`confidentialBalance` is in confidential units. Multiply it by the rate to compare it with the public balance.
# Extend ERC20Confidential
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/dual-mode/extending
Freeze accounts, add a compliance observer, mint confidentially, auto-shield mints, or add the confidential layer to your own ERC-20
`ERC20Confidential` exposes hooks and library functions for the policies a regulated or product-specific token needs. This page shows each one with a contract that compiles against `fhenix-confidential-contracts@0.4.0`. None of them is active unless your contract wires it in.
Every contract here inherits `ERC20Confidential`, so it must be [linked to `ERC20ConfidentialLib`](/cofhe/fhe-library/confidential-contracts/overview#link-the-shared-library) when you deploy it.
## Freeze accounts
`_beforeConfidentialMove(from, to)` runs before every account-initiated confidential move: the eight transfer functions and both `unshield` overloads. It is empty by default. Override it and revert to block a move.
It does not run on `shield`, on public ERC-20 transfers, on `claimUnshielded`, or on `_confidentialMint`. A freeze that only overrides this hook leaves the public balance, shielding, and pending claims open. To close those too, also override OpenZeppelin's `_update`, which every public balance change goes through, including the pool transfers inside `shield` and `claimUnshielded`:
```solidity FreezableToken.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ERC20Confidential } from "fhenix-confidential-contracts/contracts/ERC20Confidential/ERC20Confidential.sol";
contract FreezableToken is ERC20Confidential, Ownable {
mapping(address => bool) public frozen;
error AccountFrozen(address account);
constructor() ERC20Confidential("Freezable Token", "FRZ", 18) Ownable(msg.sender) {
_mint(msg.sender, 1_000_000 ether);
}
function setFrozen(address account, bool isFrozen) external onlyOwner {
frozen[account] = isFrozen;
}
function _checkNotFrozen(address account) private view {
if (frozen[account]) revert AccountFrozen(account);
}
// Confidential transfers and unshield.
function _beforeConfidentialMove(address from, address to) internal view override {
_checkNotFrozen(from);
_checkNotFrozen(to);
}
// Public transfers, shield, and claim payouts.
function _update(address from, address to, uint256 value) internal override {
_checkNotFrozen(from);
_checkNotFrozen(to);
super._update(from, to, value);
}
}
```
With both overrides, a frozen account cannot send or receive in either balance, cannot shield, and cannot be paid out by a claim.
## Add a compliance observer
A compliance observer is an address that can decrypt confidential balances and transfer amounts, for example an auditor. The library stores one observer per token. Once set, every confidential move grants the observer access to the new balances of both parties and to the moved amount.
The library exposes the setter and the backfill as functions your token calls. Put them behind your own access control:
```solidity ObservedToken.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ERC20Confidential } from "fhenix-confidential-contracts/contracts/ERC20Confidential/ERC20Confidential.sol";
import { ERC20ConfidentialLib } from "fhenix-confidential-contracts/contracts/ERC20Confidential/ERC20ConfidentialLib.sol";
contract ObservedToken is ERC20Confidential, Ownable {
constructor() ERC20Confidential("Observed Token", "OBS", 18) Ownable(msg.sender) {
_mint(msg.sender, 1_000_000 ether);
}
function setObserver(address observer_) external onlyOwner {
ERC20ConfidentialLib.setObserver(observer_);
}
function grantObserverPast(uint256[] calldata handles) external onlyOwner {
ERC20ConfidentialLib.grantObserverPast(handles);
}
}
```
`observer()` returns the current observer, or the zero address for none. Setting it emits `ObserverSet(observer)`.
Grants only cover handles created after the observer is set. A balance that has not moved since then is still unreadable to the observer. To grant access to older handles, collect them, for example from `confidentialBalanceOf` and past `ConfidentialTransfer` events, and pass them to `grantObserverPast`. It emits `ObserverPastGranted(observer, count)` and reverts with `NoObserverSet` if no observer is set. Pass only handles this token created; on CoFHE a handle the token is not allowed on makes the whole call revert.
The observer reads values the same way a holder does, with [`decryptForView`](/cofhe/client-sdk/guides/decrypt-to-view).
Changing the observer does not revoke the old observer's access to handles it was already granted.
## Mint straight into confidential balances
`_confidentialMint(address to, uint64 amount)` mints `amount * rate` public tokens into the [pool](/cofhe/fhe-library/confidential-contracts/dual-mode/dual-balance-model#where-the-backing-tokens-sit) and credits `amount` confidential units to `to`. The amount is in confidential units and is public in your call data. The recipient never holds the tokens publicly.
```solidity theme={null}
function mintConfidential(address to, uint64 amount) external onlyOwner {
_confidentialMint(to, amount);
}
```
`_confidentialMint` raises `totalSupply()` and updates `confidentialTotalSupply`. It does not call `_beforeConfidentialMove`, so it mints to frozen accounts unless your `_update` override blocks the recipient. In `FreezableToken` above it does not, because the public leg goes to the pool, not to `to`.
## Auto-shield minted tokens
`ERC20ConfidentialLib.autoShield(policy, to, amount)` shields `amount` for `to` right after a mint, if a policy contract says `to` wants private mints. The policy implements `IMintModePolicy`:
```solidity AutoShieldToken.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { ERC20Confidential } from "fhenix-confidential-contracts/contracts/ERC20Confidential/ERC20Confidential.sol";
import { ERC20ConfidentialLib, IMintModePolicy, MintMode } from "fhenix-confidential-contracts/contracts/ERC20Confidential/ERC20ConfidentialLib.sol";
contract MintModeRegistry is IMintModePolicy {
mapping(address => MintMode) private _modes;
function setMintMode(MintMode mode) external {
_modes[msg.sender] = mode;
}
function mintModeFor(address account) external view returns (MintMode) {
MintMode mode = _modes[account];
return mode == MintMode.UNSET ? MintMode.PUBLIC : mode;
}
}
contract AutoShieldToken is ERC20Confidential {
address public immutable mintModes;
constructor(address mintModes_) ERC20Confidential("Auto Shield Token", "AST", 18) {
mintModes = mintModes_;
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
ERC20ConfidentialLib.autoShield(mintModes, to, amount);
}
}
```
`MintMode` has three values: `UNSET`, `PUBLIC`, and `PRIVATE`. `autoShield` shields only when the policy returns `PRIVATE`. It never reverts, so it cannot break the mint around it. It leaves the tokens public when:
* the policy address is zero,
* the policy call reverts,
* the amount is smaller than the rate,
* or the amount exceeds what a `uint64` holds in confidential units.
As with `shield`, the remainder below the rate stays public. Minting `1 ether + 5` to an account set to `PRIVATE` shields `1 ether` and leaves `5` base units public.
## Add the confidential layer to your own ERC-20
If your token already has its own ERC-20 implementation, inherit `ERC20ConfidentialCoreUpgradeable` instead of `ERC20Confidential`. It carries the whole confidential layer but no ERC-20 and no OpenZeppelin base contracts, so it does not clash with the ones your token brings. It reaches your ledger through three hooks:
| Hook | Must |
| - | - |
| \_ledgerMint(to, amount) | Mint `amount` public tokens to `to` |
| \_ledgerTransfer(from, to, amount) | Move `amount` public tokens without an allowance check |
| \_ledgerBalanceOf(account) | Return the public balance of `account` |
Your token must also expose the standard public `balanceOf(address)` over the same ledger, because the library reads the pool's balance through it. Call `__ERC20ConfidentialCore_init(publicDecimals, confidentialDecimals)` once from your constructor or initializer, and add `_confidentialSupportsInterface(interfaceId)` to your `supportsInterface`:
```solidity LedgerToken.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import { ERC20ConfidentialCoreUpgradeable } from "fhenix-confidential-contracts/contracts/ERC20Confidential/ERC20ConfidentialCoreUpgradeable.sol";
contract LedgerToken is ERC20, ERC165, ERC20ConfidentialCoreUpgradeable {
constructor() ERC20("Ledger Token", "LDG") {
__ERC20ConfidentialCore_init(decimals(), 6);
_mint(msg.sender, 1_000_000 ether);
}
function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
return _confidentialSupportsInterface(interfaceId) || super.supportsInterface(interfaceId);
}
function _ledgerMint(address to, uint256 amount) internal override {
_mint(to, amount);
}
function _ledgerTransfer(address from, address to, uint256 amount) internal override {
_transfer(from, to, amount);
}
function _ledgerBalanceOf(address account) internal view override returns (uint256) {
return balanceOf(account);
}
}
```
The confidential decimals you pass are clamped to the public decimals. This host has no indicator token, which is optional: confidential transfers work without one. To add one, deploy an `ERC20ConfidentialIndicator` with your token as its parent and register it with `_setIndicatorToken`. [`ERC20Confidential.sol`](https://github.com/FhenixProtocol/fhenix-confidential-contracts/blob/v0.4.0/contracts/ERC20Confidential/ERC20Confidential.sol) is the reference host to compare against.
# Overview
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/dual-mode/overview
A public ERC-20 token with an encrypted balance next to it, in one contract
`ERC20Confidential` is an OpenZeppelin `ERC20` with a second, encrypted balance for every holder. Use it when your token must keep working with every ERC-20 integration and holders opt in to privacy by moving tokens into the encrypted balance.
## How it differs from FHERC20
Each holder has two balances in the same contract:
* **A public balance.** `balanceOf`, `transfer`, `approve`, `transferFrom`, `allowance`, and `totalSupply` are the standard OpenZeppelin functions and behave exactly as on any ERC-20.
* **A confidential balance.** An encrypted `euint64` handle that only the holder, and accounts the token grants access to, can decrypt. You read it with `confidentialBalanceOf` and move it with the `confidentialTransfer` family.
`shield` moves tokens from the public balance into the confidential one, and `unshield` plus `claimUnshielded` move them back.
On an [FHERC20](/cofhe/fhe-library/confidential-contracts/fherc20/overview) token, the ERC-20 surface is only an indicator and its transfers revert. Here it is real, so `balanceOfIsIndicator()` returns `false` and `indicatorTick()` returns `0`.
| | `ERC20Confidential` | `FHERC20` |
| - | - | - |
| Public balance | Real, standard ERC-20 | None. `balanceOf` returns an indicator |
| Confidential balance | Opt in with `shield` | Every balance |
| ERC-20 transfers | Work | Revert |
| Needs `ERC20ConfidentialLib` linked | Yes | No |
Pick `ERC20Confidential` for a new token that needs both modes. To add privacy to a token that is already deployed, use a [wrapper](/cofhe/fhe-library/confidential-contracts/fherc20/fherc20-wrapper) instead.
## A minimal token
`ERC20Confidential` takes a name, a symbol, and the decimals of the public balance:
```solidity DualToken.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { ERC20Confidential } from "fhenix-confidential-contracts/contracts/ERC20Confidential/ERC20Confidential.sol";
contract DualToken is ERC20Confidential {
constructor() ERC20Confidential("Dual Token", "DUAL", 18) {
_mint(msg.sender, 1_000_000 ether);
}
}
```
`_mint` is the OpenZeppelin function, so this mints public tokens. Holders call `shield` to make them confidential.
The constructor also deploys a small indicator token that wallets can track. See [how the two balances relate](/cofhe/fhe-library/confidential-contracts/dual-mode/dual-balance-model#the-indicator-token).
## Decimals and units
The confidential balance is a 64-bit integer with at most 6 decimals. `confidentialDecimals()` returns `6`, or the public decimals if those are lower. Every confidential unit is backed by a fixed number of public base units:
```text theme={null}
rate = 10 ** (decimals - confidentialDecimals)
```
For an 18-decimal token the rate is 10^12. Shielding `1 ether` credits `1_000_000` confidential units, and you pass confidential amounts in those units to transfers and `unshield`.
## Deploy it
`ERC20Confidential` and `ERC20ConfidentialUpgradeable` call into the external `ERC20ConfidentialLib`, so you must deploy that library and link it before you deploy the token. See [link the shared library](/cofhe/fhe-library/confidential-contracts/overview#link-the-shared-library). For the upgradeable variant, see [upgradeable tokens](/cofhe/fhe-library/confidential-contracts/upgradeable).
Enable the Solidity optimizer. `DualToken` compiles to about 16 KB of bytecode with the optimizer off, and a token with a few extra functions grows quickly toward the 24 KB limit.
## Pages in this section
The pool that backs confidential balances, total supply, and the indicator token.
Move tokens between the public and the confidential balance.
How encrypted transfers differ from FHERC20.
Freeze accounts, add a compliance observer, mint confidentially, or bring your own ledger.
# Shield and unshield
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/dual-mode/shield-unshield
Move ERC20Confidential tokens from the public balance into the encrypted one and back
`shield` moves tokens from a holder's public balance into their confidential balance in one transaction. Going back takes two: `unshield` burns the confidential amount and opens a claim, and `claimUnshielded` pays out public tokens once the amount is decrypted. This page covers both directions and the claim lifecycle.
Amounts in this page use the [conversion rate](/cofhe/fhe-library/confidential-contracts/dual-mode/overview#decimals-and-units): for an 18-decimal token, one confidential unit is 10^12 public base units.
## Shield public tokens
`shield(uint256 amount)` takes `amount` in public base units from the caller's public balance:
```typescript theme={null}
await token.shield(ethers.parseEther('10'));
```
The token rounds `amount` down to a multiple of the rate. It moves the rounded amount into the [pool](/cofhe/fhe-library/confidential-contracts/dual-mode/dual-balance-model#where-the-backing-tokens-sit) and credits `amount / rate` confidential units to the caller. The remainder never leaves the public balance. If the rounded amount is zero, `shield` reverts with `AmountTooSmallForConfidentialPrecision`.
`shield` always acts on the caller's own tokens and credits the caller. There is no `shield` for another account and no approval involved.
The shielded amount is public: it appears in the call data and in these events.
| Event | Emitted by | Carries |
| - | - | - |
| `Transfer(caller, CONFIDENTIAL_POOL, amount)` | The token | The public amount moved into the pool |
| `Transfer(address(0), caller, 10110000001)` | The indicator token | A fixed value that marks confidential activity |
| `ConfidentialTransfer(address(0), caller, handle)` | The token | The handle of the credited amount |
| `TokensShielded(caller, amount)` | The token | The public amount shielded |
## Unshield confidential tokens
`unshield` has two overloads. Both burn from the caller's confidential balance and open a claim that pays the caller.
| Overload | Called by | The amount is |
| - | - | - |
| unshield(uint64 amount) | A wallet or a contract | Public, in confidential units |
| unshield(sharedEuint64 amount) | A contract that holds an encrypted amount | Encrypted until the claim reveals it |
The plaintext overload puts the amount in the call data. The encrypted overload keeps it hidden until the claim, but only a contract can call it, because the caller creates the argument with `FHE.shareEuint64(amount, address(token))`. See [call a token from your contract](/cofhe/fhe-library/confidential-contracts/calling-from-contracts).
The burn follows the same rule as a transfer: if the amount exceeds the confidential balance, nothing is burned and the claim decrypts to zero.
`unshield` emits `ConfidentialTransfer(caller, address(0), handle)` and `TokensUnshielded(caller, handle)`, where `handle` is the burned amount. The token makes that handle publicly decryptable, since the claim reveals it anyway.
## Claim the public tokens
Each `unshield` opens a claim with a unique ID. The ID is not the burned handle. Read it from the token:
| Function | Returns |
| - | - |
| getUserClaims(account) | The account's pending claims |
| getClaim(id) | One claim, pending or settled |
A claim has five fields: `id`, `to` (the account paid out), `ctHash` (the burned handle), `decryptedAmount` (set when the claim settles), and `claimed`.
Decrypt `ctHash` with [`decryptForTx`](/cofhe/client-sdk/guides/decrypt-to-tx). No ACP is needed, because the handle is publicly decryptable. Then submit the result:
```typescript theme={null}
await (await token['unshield(uint64)'](1_000_000n)).wait();
const [claim] = await token.getUserClaims(account);
const { decryptedValue, signature } = await client
.decryptForTx(claim.ctHash)
.withoutACP()
.execute();
await token.claimUnshielded(claim.id, decryptedValue, signature);
```
`claimUnshielded` verifies the signature against the burned handle, then moves `decryptedValue * rate` public tokens from the pool to `claim.to`. Anyone can submit the claim, because the payout always goes to `claim.to`. A relayer can pay the gas for a user.
To settle several claims in one transaction, pass parallel arrays:
```typescript theme={null}
await token.claimUnshieldedBatch(ids, decryptedValues, signatures);
```
The batch reverts as a whole if any element is invalid. On success each claim emits `UnshieldedTokensClaimed(to, id, handle, decryptedValue)`, along with the public `Transfer` from the pool.
## Errors
| Error | Cause |
| - | - |
| `AmountTooSmallForConfidentialPrecision` | `shield` with an amount smaller than the rate |
| `ClaimNotFound` | No claim has this ID |
| `AlreadyClaimed` | The claim has settled |
| `ClaimBatchLengthMismatch` | The three batch arrays differ in length |
A `decryptedValue` or `signature` that does not match the burned handle makes the decryption check revert, and the claim stays pending.
# FHERC20 best practices
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/fherc20/best-practices
Checks to make when you build on, integrate with, or deploy an FHERC20 token
These are the mistakes that cost the most when you work with FHERC20 tokens. Each item says what to do and links to the page that explains why.
## Transfers
**Use the amount that moved, not the amount you asked for.** A transfer that exceeds the sender's balance moves zero and does not revert. Every transfer function returns the amount that actually moved, as a `sharedEuint64`. A contract that credits users, settles trades, or counts deposits must work from that returned amount. See [transfers](/cofhe/fhe-library/confidential-contracts/fherc20/core-features).
**Expect a revert from accounts that never held tokens.** A transfer from an account whose balance was never set reverts with `FHERC20ZeroBalance`. That revert is public, so it reveals that the account has never received the token. An account that held tokens and spent them all does not revert.
**Do not read `balanceOf` or `totalSupply` as amounts.** On an FHERC20 token they return an indicator that moves by one tick per transfer, not a balance. Read `confidentialBalanceOf` and decrypt it. The indicator still shows each account's transfer activity, even though it hides the amounts. See [FHERC20 overview](/cofhe/fhe-library/confidential-contracts/fherc20/overview).
## Encrypted inputs
**Encrypt with the account that sends the transaction.** An input proof is bound to the account and the consuming contract it was created for. If an operator submits a transfer, the operator's client encrypts the amount, not the holder's. See [encrypting inputs](/cofhe/client-sdk/guides/encrypting-inputs).
**Name the contract that converts the input.** The consuming contract is the one that calls `FHE.asEuint64`. For a direct transfer, that is the token. For a call through your own contract, that is your contract. Anything else fails with `InvalidSigner`.
## Contracts that call the token
**Receive returned values from the token by name.** Read a token's return value with `FHE.receiveEuint64FromCall(result, address(token))`, naming the token you called. See [call a token from your contract](/cofhe/fhe-library/confidential-contracts/calling-from-contracts).
**Persist what you keep.** A received handle is usable for the current transaction only. Call `FHE.allowThis` on any value you store, and `FHE.allow` for each account that should decrypt it. See [access control](/cofhe/fhe-library/core-concepts/access-control).
**Check who calls your receiver.** `onConfidentialTransferReceived` is an external function, and any contract can call it. Check that `msg.sender` is the token you accept, and receive the amount with `FHE.receiveEuint64Param`. Every confidential transfer is `nonReentrant`, so your callback cannot start another transfer on the same token. See [transfer callbacks](/cofhe/fhe-library/confidential-contracts/fherc20/transfer-callbacks).
## Operators
**Keep operator windows short.** An operator can move any amount until its `until` timestamp, because there is no per-amount allowance. Grant the shortest window the flow needs, and revoke with `setOperator(operator, 0)` when it is done. See [operators](/cofhe/fhe-library/confidential-contracts/fherc20/operators).
## Wrappers
**Look up claims by ID.** `claimUnshielded` takes the claim ID from `getUserClaims` or `getClaim`. The ciphertext handle is what you decrypt, not what you claim with. See [wrap ERC-20 tokens and ETH](/cofhe/fhe-library/confidential-contracts/fherc20/fherc20-wrapper).
**Treat unshielded amounts as public.** Unshielding makes the burned amount publicly decryptable, and the claim publishes it in plaintext. The `uint64` overload of `unshield` also shows it in calldata before that. Only the balances that remain stay confidential.
**Do not wrap fee-on-transfer tokens.** The ERC-20 wrapper mints the full amount it requests, so a token that delivers less leaves the wrapper holding less than it owes.
# Transfers and disclosure
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/fherc20/core-features
Send FHERC20 tokens from a wallet or a contract, read what moved, and make an encrypted amount public
This page covers the FHERC20 transfer functions and the disclosure flow that turns an encrypted amount into a public, verified one. It assumes you know what an [FHERC20 token](/cofhe/fhe-library/confidential-contracts/fherc20/overview) stores.
## Which transfer function do you call?
Each transfer comes in two forms. Pick by who holds the amount:
| Caller | Amount parameter | ethers signature |
| - | - | - |
| A wallet | `externalEuint64 encryptedAmount, bytes inputProof` | `confidentialTransfer(address,bytes32,bytes)` |
| A contract | `sharedEuint64 sharedAmount` | `confidentialTransfer(address,bytes32)` |
| A wallet, as an [operator](/cofhe/fhe-library/confidential-contracts/fherc20/operators) | `externalEuint64 encryptedAmount, bytes inputProof` | `confidentialTransferFrom(address,address,bytes32,bytes)` |
| A contract, as an operator | `sharedEuint64 sharedAmount` | `confidentialTransferFrom(address,address,bytes32)` |
Both encrypted types are `bytes32` in the ABI, so ethers cannot pick the overload by argument types. Index the contract with the full signature, as in the table.
The `AndCall` versions of both functions also notify the recipient. See [transfer callbacks](/cofhe/fhe-library/confidential-contracts/fherc20/transfer-callbacks).
## How do you send from a wallet?
Encrypt the amount with the SDK. Name the token as the consuming contract, and encrypt with the client of the account that sends the transaction:
```typescript theme={null}
import { Encryptable } from '@cofhe/sdk';
const [amount, inputProof] = await client
.encryptInputs([Encryptable.uint64(25_000_000n)])
.setConsumingContract(await token.getAddress())
.execute();
await token['confidentialTransfer(address,bytes32,bytes)'](recipient, amount, inputProof);
```
The proof is bound to both the consuming contract and the sender. If either differs, the transaction reverts with `InvalidSigner`. That includes encrypting as one account and submitting from another, so an operator encrypts with its own client. See [encrypting inputs](/cofhe/client-sdk/guides/encrypting-inputs) for client setup.
## How do you send from a contract?
A contract that already holds an encrypted amount shares it with the token, then reads back what moved:
```solidity theme={null}
sharedEuint64 moved = token.confidentialTransfer(recipient, FHE.shareEuint64(amount, address(token)));
euint64 sent = FHE.receiveEuint64FromCall(moved, address(token));
```
The `sharedEuint64` overloads accept only a share that the direct caller created for the token. An account that passes a bare handle gets `NotShared`, so a wallet cannot use them. [Call a token from your contract](/cofhe/fhe-library/confidential-contracts/calling-from-contracts) walks through a full example.
## What happens when the balance is too low?
The transfer succeeds and moves zero. The token cannot revert on an encrypted comparison without revealing its result. Instead it moves the requested amount if the balance covers it, and zero otherwise.
Every transfer function returns that amount as a `sharedEuint64` directed at the caller. A contract caller reads it with `FHE.receiveEuint64FromCall` and should act on it, not on the amount it requested. A wallet can find the same handle in the `ConfidentialTransfer` event.
The token grants persistent access to the moved amount to the sender, the recipient, and the token itself. Either party can decrypt it with `decryptForView`.
The one case that does revert is a sender that has never held the token. Its balance handle does not exist, so the transfer fails with `FHERC20ZeroBalance`.
## What does a transfer emit?
| Event | Arguments |
| - | - |
| Transfer | `from`, `to`, and a fixed value that reads as `7984.0001`. Present for ERC-20 indexers; the value is not the amount |
| ConfidentialTransfer | `from`, `to`, and the handle of the amount that moved, all indexed |
Mints emit both events with `from` set to the zero address, and burns with `to` set to the zero address.
All transfer functions are `nonReentrant`. A callback that tries to start a second transfer on the same token in the same call reverts.
## Errors
| Error | When |
| - | - |
| FHERC20InvalidReceiver | `to` is the zero address, or a callback recipient reverted without a reason. See [transfer callbacks](/cofhe/fhe-library/confidential-contracts/fherc20/transfer-callbacks) |
| FHERC20InvalidSender | `from` is the zero address |
| FHERC20ZeroBalance | `from` has never held the token |
| FHERC20UnauthorizedSpender | The caller of a `From` function is not an operator for `from` |
| FHERC20IncompatibleFunction | The caller used `transfer`, `transferFrom`, `approve`, or `allowance` |
| InvalidSigner | The input proof was signed for a different consuming contract or sender, or a decryption result does not match its handle. See [common errors](/cofhe/fhe-library/core-concepts/common-errors) |
| NotShared | A `sharedEuint64` argument was not shared with the token by the caller |
## How do you make an amount public?
Disclosure publishes a decrypted amount onchain together with a proof that it is correct. Use it when an amount has to become public, such as a payment that a third party must verify.
Call `requestDiscloseEncryptedAmount(handle)` from an account that has access to the handle, such as the holder of a balance. The token makes the handle publicly decryptable and emits `AmountDiscloseRequested`. A caller without access gets `FHERC20UnauthorizedUseOfEncryptedAmount`.
Anyone can now decrypt the handle with `decryptForTx`, without an ACP:
```typescript theme={null}
const { decryptedValue, signature } = await client.decryptForTx(handle).withoutACP().execute();
```
Call `discloseEncryptedAmount(handle, decryptedValue, signature)`. The token verifies the signature and emits `AmountDisclosed(handle, amount)`. A value that does not match the handle reverts with `InvalidSigner`.
`discloseEncryptedAmount` checks only the proof. It does not need a prior request, so you can publish any handle you hold a valid decryption proof for.
Disclosing a balance handle reveals that balance at that moment and cannot be undone. Later balance changes create new handles, which stay private.
## Related pages
* [Operators](/cofhe/fhe-library/confidential-contracts/fherc20/operators)
* [Decrypt to transaction](/cofhe/client-sdk/guides/decrypt-to-tx)
* [Best practices](/cofhe/fhe-library/confidential-contracts/fherc20/best-practices)
# Wrap ERC-20 tokens and ETH
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/fherc20/fherc20-wrapper
Shield an existing ERC-20 or ETH into an FHERC20 token, and unshield it back with a decrypted claim
A wrapper turns a public asset into an FHERC20 token (a token whose balances are encrypted) backed one to one by what the wrapper holds. `FHERC20ERC20Wrapper` wraps any standard ERC-20. `FHERC20NativeWrapper` wraps ETH, taken directly or as WETH. Use this page to deploy a wrapper, shield into it, and get the underlying asset back out.
## Deploy a wrapper
Both wrappers are abstract. You inherit one and pass the FHERC20 constructor arguments next to the wrapper's own:
```solidity ConfidentialUSDC.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { FHERC20 } from "fhenix-confidential-contracts/contracts/FHERC20/FHERC20.sol";
import { FHERC20ERC20Wrapper } from "fhenix-confidential-contracts/contracts/FHERC20/extensions/FHERC20ERC20Wrapper.sol";
contract ConfidentialUSDC is FHERC20ERC20Wrapper {
constructor(IERC20 usdc) FHERC20("Confidential USDC", "eUSDC", 6, "") FHERC20ERC20Wrapper(usdc) {}
}
```
```solidity ConfidentialETH.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { IWETH } from "fhenix-confidential-contracts/contracts/interfaces/IWETH.sol";
import { FHERC20 } from "fhenix-confidential-contracts/contracts/FHERC20/FHERC20.sol";
import { FHERC20NativeWrapper } from "fhenix-confidential-contracts/contracts/FHERC20/extensions/FHERC20NativeWrapper.sol";
contract ConfidentialETH is FHERC20NativeWrapper {
constructor(IWETH weth) FHERC20("Confidential Ether", "eETH", 6, "") FHERC20NativeWrapper(weth) {}
}
```
The native wrapper takes the WETH address even if you only use `shieldNative`, because it reads its decimals from WETH.
The `6` passed to `FHERC20` does not set the wrapper's decimals. The wrapper overrides `decimals()` with a value derived from the underlying token. The constructor argument only sets the indicator tick, the unit that `balanceOf` reports in. Pass the wrapper's real decimals, which is 6 for any underlying with 6 or more.
Both wrappers call into `ERC20ConfidentialLib`, so you must deploy that library and link it into the wrapper. See [link the shared library](/cofhe/fhe-library/confidential-contracts/overview#link-the-shared-library) for the steps.
Enable the Solidity optimizer for wrappers. With the optimizer off, `ConfidentialETH` compiles to 24,693 bytes and an upgradeable ERC-20 wrapper to 26,751 bytes. Both exceed the 24,576-byte limit and fail to deploy. With `runs: 200` they compile to 14,839 and 15,730 bytes.
## How decimals and the rate work
Encrypted amounts are `euint64`, so a wrapper caps its own decimals at 6. When the underlying has more, the wrapper converts at a fixed `rate()`:
| Underlying decimals | Wrapper `decimals()` | `rate()` |
| - | - | - |
| 6 (USDC) | 6 | 1 |
| 18 (DAI, WETH, ETH) | 6 | 10^12 |
| 4 | 4 | 1 |
One confidential unit is backed by `rate()` units of the underlying. Shielding rounds down to a multiple of `rate()`, and a claim pays `amount * rate()`. If the underlying has no `decimals()` function, the ERC-20 wrapper assumes 18.
## Shield an ERC-20
`shield(to, amount)` pulls the underlying from the caller and mints confidential tokens to `to`. Approve the wrapper first:
```typescript theme={null}
await usdc.approve(await eUSDC.getAddress(), 100_000_000n);
await eUSDC.shield(alice.address, 100_000_000n);
```
The amount shielded is public: it is a plaintext argument and an ERC-20 transfer. Only the balances that follow are confidential.
* The wrapper pulls `amount - (amount % rate())` and mints `amount / rate()`. The remainder never leaves your account.
* An `amount` below `rate()` mints zero and pulls nothing. It does not revert.
* `to` cannot be `address(0)`. The mint reverts with `FHERC20InvalidReceiver`.
### Shield in one transaction with ERC-1363
If the underlying implements ERC-1363, you can skip the approval. Call `transferAndCall` on the underlying with the wrapper as the recipient. The wrapper's `onTransferReceived` mints to the address in the first 20 bytes of `data`, or to the sender when `data` is shorter than 20 bytes:
```typescript theme={null}
await pay['transferAndCall(address,uint256,bytes)'](
await ePAY.getAddress(),
amount,
ethers.solidityPacked(['address'], [bob.address]),
);
```
Here the full `amount` arrives first, so the wrapper refunds the part below a multiple of `rate()` to the sender. `onTransferReceived` reverts with `FHERC20UnauthorizedCaller` unless the underlying token calls it.
## Shield the native token
`FHERC20NativeWrapper` has two entry points:
* `shieldNative(to)` takes `msg.value`. It refunds the part below a multiple of `rate()` to the caller.
* `shieldWrappedNative(to, value)` pulls WETH, which you approve first, and unwraps it. The part below a multiple of `rate()` is never pulled.
```typescript theme={null}
await eETH.shieldNative(ethers.ZeroAddress, { value: ethers.parseEther('1') });
await weth.approve(await eETH.getAddress(), ethers.parseEther('0.5'));
await eETH.shieldWrappedNative(bob.address, ethers.parseEther('0.5'));
```
Unlike the ERC-20 wrapper, both functions treat `to == address(0)` as the caller, and both revert with `AmountTooSmallForConfidentialPrecision` when the value is below `rate()`. With 18-decimal ETH the rate is 10^12, so 1 ETH mints 1,000,000 units.
## Unshield back to the underlying
Getting the underlying back takes two transactions, because the amount is encrypted and the wrapper can only pay out a plaintext:
Call `unshield(from, to, amount)`. The wrapper burns the amount from `from`, makes the burned amount publicly decryptable, and records a claim for `to`.
Read the claim and decrypt its handle with `decryptForTx`. No ACP (Access Control Permission) is needed, because the burned amount is public.
Call `claimUnshielded(id, value, signature)`. The wrapper verifies the decryption and sends `value * rate()` of the underlying to the claim's recipient.
There are two `unshield` overloads:
| Overload | Caller | Note |
| - | - | - |
| `unshield(address from, address to, uint64 amount)` | Any account | `amount` is plaintext, so it is visible in the transaction |
| `unshield(address from, address to, sharedEuint64 amount)` | A contract | The caller shares an encrypted amount it holds. See [call a token from your contract](/cofhe/fhe-library/confidential-contracts/calling-from-contracts) |
The caller must be `from` or an [operator](/cofhe/fhe-library/confidential-contracts/fherc20/operators) for `from`, or the call reverts with `FHERC20UnauthorizedSpender`. `to` cannot be `address(0)`. If the amount exceeds the balance, the wrapper burns zero, and the claim decrypts to `0`.
### Find and settle the claim
A claim is keyed by a claim ID, not by the ciphertext handle. Two unshields can burn values with the same handle, so the handle cannot identify a claim. Read pending claims with `getUserClaims(to)`, then decrypt `ctHash` and claim by `id`:
```typescript theme={null}
await eUSDC['unshield(address,address,uint64)'](alice.address, alice.address, 40_000_000n);
const [claim] = await eUSDC.getUserClaims(alice.address);
const { decryptedValue, signature } = await client
.decryptForTx(claim.ctHash)
.withoutACP()
.execute();
await eUSDC.claimUnshielded(claim.id, decryptedValue, signature);
```
Any account can submit the claim. The payout always goes to the claim's `to`, so a relayer can settle claims for your users. `claimUnshieldedBatch(ids, values, signatures)` settles several in one transaction and reverts as a whole if any element is invalid. See [decrypt to transaction](/cofhe/client-sdk/guides/decrypt-to-tx) for the SDK side.
`getUserClaims` returns only pending claims. `getClaim(id)` returns any claim as this struct:
| Field | Type | Meaning |
| - | - | - |
| `id` | `bytes32` | The claim ID you pass to `claimUnshielded` |
| `to` | `address` | Who receives the payout |
| `ctHash` | `bytes32` | The burned amount's handle, the value you decrypt |
| `decryptedAmount` | `uint64` | The verified amount, set when the claim settles |
| `claimed` | `bool` | Whether the claim has settled |
## Total supply checks
Because the wrapper's holdings are public, it can bound its own supply. `inferredTotalSupply()` returns the underlying balance divided by `rate()`, and every mint reverts with `FHERC20TotalSupplyOverflow` if that exceeds `maxTotalSupply()`, which is `type(uint64).max`. Anyone can raise `inferredTotalSupply()` by sending the underlying to the wrapper directly. It also lags `confidentialTotalSupply()` between an unshield and its claim.
The ERC-20 wrapper assumes it receives the full amount it pulls. Fee-on-transfer and other deflationary tokens are not supported: the wrapper would mint more than it holds.
## Errors
| Error | When |
| - | - |
| `FHERC20InvalidReceiver` | `unshield` targets `address(0)`, or the ERC-20 wrapper's `shield` does |
| `FHERC20UnauthorizedSpender` | The caller of `unshield` is neither `from` nor its operator |
| `FHERC20UnauthorizedCaller` | `onTransferReceived` is called by a token other than the underlying |
| `AmountTooSmallForConfidentialPrecision` | A native shield is below `rate()` |
| `FHERC20TotalSupplyOverflow` | A mint would push `inferredTotalSupply()` past `maxTotalSupply()` |
| `NativeTransferFailed` | A dust refund or an ETH payout could not be sent |
| `ClaimNotFound` | No claim has that ID |
| `AlreadyClaimed` | The claim has already settled |
| `ClaimBatchLengthMismatch` | The batch arrays have different lengths |
| `InvalidSigner` | The decrypted value or signature does not match the claim's handle |
`InvalidSigner` comes from the TaskManager, which verifies the decryption. See [common errors](/cofhe/fhe-library/core-concepts/common-errors) for the other TaskManager errors.
## Events
| Event | Emitted by |
| - | - |
| `ShieldedNative(from, to, value)` | `shieldNative`, `shieldWrappedNative` |
| `Unshielded(to, amount)` | Both `unshield` overloads |
| `ClaimedUnshielded(to, unshieldRequestId, unshieldAmount, unshieldAmountCleartext)` | `claimUnshielded`, `claimUnshieldedBatch` |
Shielding also emits the FHERC20 `Transfer` and `ConfidentialTransfer` events for the mint. The ERC-20 wrapper has no shield event of its own.
# Operators
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/fherc20/operators
Let another account or contract move FHERC20 tokens for a holder until a deadline
An operator is an address that may move any amount of a holder's tokens until a deadline. FHERC20 has no per-amount allowance, so operators are how a holder lets someone else spend: a payment app, a vault, or a second wallet.
## How do you grant and revoke an operator?
The holder calls `setOperator(address operator, uint48 until)`, where `until` is a Unix timestamp in seconds:
```typescript theme={null}
const block = await provider.getBlock('latest');
await token.setOperator(operatorAddress, block.timestamp + 3600);
```
Each call replaces the previous deadline for that pair, so the same call extends or shortens it. To revoke, set `until` to `0`:
```typescript theme={null}
await token.setOperator(operatorAddress, 0);
```
Every call emits `OperatorSet(holder, operator, until)`.
`isOperator(holder, spender)` returns `true` while `block.timestamp <= until`, so the deadline second itself is still valid. A holder is always its own operator.
An operator can move the holder's whole balance, in any number of transfers, until the deadline. There is no amount cap. Grant the shortest window that covers the task, and revoke it when the task is done.
## What can an operator call?
Operator rights apply to every function that takes a `from` address:
* `confidentialTransferFrom`, in both the wallet and the contract form. See [transfers](/cofhe/fhe-library/confidential-contracts/fherc20/core-features#which-transfer-function-do-you-call).
* `confidentialTransferFromAndCall`. See [transfer callbacks](/cofhe/fhe-library/confidential-contracts/fherc20/transfer-callbacks).
* `unshield(from, to, amount)` on the [wrappers](/cofhe/fhe-library/confidential-contracts/fherc20/fherc20-wrapper), which sends the underlying tokens to any `to`.
A caller that is not an operator for `from` gets `FHERC20UnauthorizedSpender(holder, spender)`.
## How does an operator transfer from a wallet?
The operator encrypts the amount with its own SDK client and sends the transaction itself. An input encrypted by the holder fails with `InvalidSigner` when the operator submits it, because the proof is bound to the sender.
```typescript theme={null}
import { Encryptable } from '@cofhe/sdk';
const [amount, inputProof] = await operatorClient
.encryptInputs([Encryptable.uint64(30_000_000n)])
.setConsumingContract(await token.getAddress())
.execute();
await token
.connect(operatorSigner)
['confidentialTransferFrom(address,address,bytes32,bytes)'](holder, recipient, amount, inputProof);
```
A balance that is too low moves zero, as with any transfer. The operator does not learn the holder's balance from the result unless it is the recipient.
## How does a contract act as an operator?
A contract that pulls tokens from users, such as a vault, must be an operator for each user. The user calls `setOperator(vaultAddress, until)` before calling the vault. The vault then calls the `sharedEuint64` form of `confidentialTransferFrom`, and `msg.sender` in the token is the vault.
[Call a token from your contract](/cofhe/fhe-library/confidential-contracts/calling-from-contracts) has a complete vault that deposits and withdraws this way.
## Related pages
* [Transfers and disclosure](/cofhe/fhe-library/confidential-contracts/fherc20/core-features)
* [Best practices](/cofhe/fhe-library/confidential-contracts/fherc20/best-practices)
# Overview
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/fherc20/overview
How an FHERC20 token stores encrypted balances, what stays public, and how to mint, burn, and read balances
FHERC20 is a token whose balances exist only as encrypted values. It implements the confidential token interface from [ERC-7984](https://eips.ethereum.org/EIPS/eip-7984) and keeps a read-only ERC-20 surface so wallets and explorers still recognize it. Read this page to build an FHERC20 token and to understand what it reveals.
If your token must also keep working as a normal ERC-20, use [ERC20Confidential](/cofhe/fhe-library/confidential-contracts/dual-mode/overview) instead. To make an existing ERC-20 or ETH confidential, use a [wrapper](/cofhe/fhe-library/confidential-contracts/fherc20/fherc20-wrapper).
## What is encrypted and what is public?
Each balance is an `euint64` handle (a reference to an encrypted value). The total supply is an `euint64` too. Transfer amounts are encrypted inputs or handles, never plaintext.
What anyone can still see:
* The sender and recipient of every transfer, from the `ConfidentialTransfer` event.
* That an account has never held the token. A transfer or burn from such an account reverts with `FHERC20ZeroBalance`, because its balance handle was never created.
* Transfer activity per account: the indicator values below move by one step per transfer received or sent.
* Amounts you pass in plaintext, such as the `uint64` argument of a mint function you write.
## What do the ERC-20 functions return?
`balanceOf` and `totalSupply` return an indicator, not a balance. The indicator tells a wallet that something changed without saying how much.
| Function | Returns |
| - | - |
| balanceOf | `0` for an account that never interacted. Otherwise starts at `7984.0000` plus `0.0001` per received transfer, minus `0.0001` per sent transfer |
| totalSupply | Starts at `7984.0000`, plus `0.0001` per mint, minus `0.0001` per burn |
| balanceOfIsIndicator | `true` |
| indicatorTick | The raw size of one `0.0001` step: `10 ** (decimals - 4)`, or `1` when `decimals` is 4 or less |
| transfer, transferFrom, approve, allowance | Revert with `FHERC20IncompatibleFunction` |
The values read as `7984.xxxx` when `decimals` is 4 or more. A token with 6 decimals reports a first-time recipient as `7984000100` raw, which a wallet shows as `7984.0001`.
Every transfer, mint, and burn also emits the standard ERC-20 `Transfer` event with a fixed value of `7984.0001`, so explorers index the activity. The real amount is in `ConfidentialTransfer`, as a handle.
An account can call `resetIndicatedBalance()` to set its own indicator back to `0`.
## Who can decrypt a balance?
The token grants access to every new balance handle for two accounts: the token itself and the holder. Nobody else can decrypt a balance unless your contract grants access with `FHE.allow`.
To show a balance in your app, read the handle and decrypt it with the SDK:
```typescript theme={null}
import { FheTypes } from '@cofhe/sdk';
const handle = await token.confidentialBalanceOf(account);
const balance = await client.decryptForView(handle, FheTypes.Uint64).execute();
```
`decryptForView` signs with an [Access Control Permission](/cofhe/client-sdk/guides/acps) (ACP) for the connected account. See [decrypt to view](/cofhe/client-sdk/guides/decrypt-to-view) for setting one up.
`confidentialBalanceOf` returns a handle, not an amount. A handle you read once goes stale after the next transfer, because every balance change creates a new handle.
## How do you mint and burn?
`FHERC20` exposes no public mint or burn. Your contract calls the internal functions and decides who may use them:
| Function | Effect |
| - | - |
| \_mint | `_mint(address to, euint64 amount)` adds `amount` to `to` and to the total supply |
| \_burn | `_burn(address from, euint64 amount)` removes `amount` from `from` and from the total supply |
Both return the encrypted amount that actually moved. Neither reverts on an encrypted condition:
* A burn larger than the balance burns zero.
* A mint that would overflow the 64-bit total supply mints zero.
This token mints and burns from encrypted inputs, so the amounts stay private:
```solidity RewardPoints.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { FHE, externalEuint64 } from "@fhenixprotocol/cofhe-contracts/FHE.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { FHERC20 } from "fhenix-confidential-contracts/contracts/FHERC20/FHERC20.sol";
contract RewardPoints is FHERC20, Ownable {
constructor() FHERC20("Reward Points", "ePTS", 6, "") Ownable(msg.sender) {}
function mint(address to, externalEuint64 amount, bytes calldata inputProof) external onlyOwner {
_mint(to, FHE.asEuint64(amount, inputProof));
}
function burn(address from, externalEuint64 amount, bytes calldata inputProof) external onlyOwner {
_burn(from, FHE.asEuint64(amount, inputProof));
}
}
```
The constructor takes the name, symbol, decimals, and a contract URI ([ERC-7572](https://eips.ethereum.org/EIPS/eip-7572)). Pass an empty string if you have no URI. `FHERC20` does not need [the shared library](/cofhe/fhe-library/confidential-contracts/overview#link-the-shared-library), so it deploys like any other contract.
A mint function that takes a plaintext `uint64` works too, but the amount is visible in the transaction's calldata.
## Which variants exist?
| Contract | Use it for |
| - | - |
| FHERC20 | A token deployed with a constructor |
| FHERC20Upgradeable | The same token behind a proxy. See [upgradeable tokens](/cofhe/fhe-library/confidential-contracts/upgradeable) |
| FHERC20ERC20Wrapper | An FHERC20 backed by an existing ERC-20 |
| FHERC20NativeWrapper | An FHERC20 backed by ETH |
`supportsInterface` reports `IFHERC20`, `IERC7984`, `IERC20`, and `IERC165`.
## Next steps
* [Transfers and disclosure](/cofhe/fhe-library/confidential-contracts/fherc20/core-features): send tokens and make an amount public.
* [Operators](/cofhe/fhe-library/confidential-contracts/fherc20/operators): let another account move tokens for a holder.
* [Transfer callbacks](/cofhe/fhe-library/confidential-contracts/fherc20/transfer-callbacks): notify a receiving contract in the same transaction.
* [Best practices](/cofhe/fhe-library/confidential-contracts/fherc20/best-practices): mistakes that leak amounts or lose funds.
# Transfer callbacks
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/fherc20/transfer-callbacks
Send FHERC20 tokens to a contract and let it accept or refund them in the same transaction
A plain transfer to a contract gives the contract tokens but no chance to react. The `AndCall` functions transfer, then call the recipient, which can record the deposit or send the tokens back. Use them when a contract must act on what it received, such as crediting a deposit.
## Which functions make the callback?
| ethers signature | Caller |
| - | - |
| `confidentialTransferAndCall(address,bytes32,bytes,bytes)` | A wallet, with `to`, the encrypted amount, `inputProof`, and `data` |
| `confidentialTransferAndCall(address,bytes32,bytes)` | A contract, with `to`, a `sharedEuint64`, and `data` |
| `confidentialTransferFromAndCall(address,address,bytes32,bytes,bytes)` | A wallet [operator](/cofhe/fhe-library/confidential-contracts/fherc20/operators), with `from`, `to`, the encrypted amount, `inputProof`, and `data` |
| `confidentialTransferFromAndCall(address,address,bytes32,bytes)` | A contract operator, with `from`, `to`, a `sharedEuint64`, and `data` |
In the wallet forms, `inputProof` comes right after the encrypted amount and before `data`. Both are `bytes`, so swapping them compiles and fails at runtime. `data` is passed to the recipient unchanged.
```typescript theme={null}
const [amount, inputProof] = await client
.encryptInputs([Encryptable.uint64(30_000_000n)])
.setConsumingContract(await token.getAddress())
.execute();
await token['confidentialTransferAndCall(address,bytes32,bytes,bytes)'](box, amount, inputProof, '0x');
```
## How does the callback work?
The token moves the amount first. If `to` has code, it then calls `onConfidentialTransferReceived` on it:
1. The token shares the moved amount with `to` as a `sharedEuint64` and calls the receiver with the operator (the caller of the token function), `from`, that amount, and `data`.
2. The receiver returns a `sharedEbool`: true to keep the tokens, false to refuse them.
3. The token moves back whatever the receiver refused: the full amount on false, zero on true.
If `to` has no code, there is no callback and the tokens stay with `to`.
The function returns `sent - refund` as a `sharedEuint64`. That is zero when the receiver refused, and also zero when the sender's balance was too low.
## How do you implement a receiver?
Implement `IERC7984Receiver`. This receiver accepts deposits up to a cap and refuses larger ones:
```solidity CappedDepositBox.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { FHE, ebool, euint64, sharedEbool, sharedEuint64 } from "@fhenixprotocol/cofhe-contracts/FHE.sol";
import { IERC7984Receiver } from "fhenix-confidential-contracts/contracts/interfaces/IERC7984Receiver.sol";
contract CappedDepositBox is IERC7984Receiver {
address public immutable token;
euint64 private _cap;
mapping(address => euint64) private _deposits;
error UnexpectedToken(address caller);
constructor(address token_, uint64 cap) {
token = token_;
_cap = FHE.asEuint64(cap);
FHE.allowThis(_cap);
}
function onConfidentialTransferReceived(
address,
address from,
sharedEuint64 sharedAmount,
bytes calldata
) external returns (sharedEbool) {
// Any contract can share a value it made up and call this function.
if (msg.sender != token) revert UnexpectedToken(msg.sender);
euint64 amount = FHE.receiveEuint64Param(sharedAmount);
ebool accepted = FHE.lte(amount, _cap);
euint64 credited = FHE.add(_deposits[from], FHE.select(accepted, amount, FHE.asEuint64(0)));
FHE.allowThis(credited);
FHE.allow(credited, from);
_deposits[from] = credited;
return FHE.shareEbool(accepted, msg.sender);
}
function depositOf(address account) external view returns (euint64) {
return _deposits[account];
}
}
```
Each step in the callback has a reason:
* **Check `msg.sender`.** `FHE.receiveEuint64Param` accepts any share whose sharer is the caller. A contract you do not know can share an amount it invented and call you directly, so only trust the token you expect.
* **Unwrap with `FHE.receiveEuint64Param`.** It reverts with `NotShared` or `UnexpectedSharer` unless the caller shared the amount with this contract in this transaction.
* **Call `FHE.allowThis` on what you store.** The received amount carries access for this transaction only. Values you derive from it and keep need a persistent grant.
* **Return `FHE.shareEbool(result, msg.sender)`.** The token accepts the result only as a share from the address it called.
Decide in encrypted form. A receiver that branches on the amount in plaintext would have to decrypt it, which it cannot do inside the callback.
Do not move the tokens back yourself and also return false. The token refunds on false, so a receiver that does both pays twice. If the receiver no longer holds the amount when it returns false, the refund moves zero and the sender gets nothing back.
## When does the transfer revert?
| Receiver behavior | Result |
| - | - |
| Returns a shared true or false | The transfer completes, with a refund on false |
| Has code but no `onConfidentialTransferReceived` | Reverts with `FHERC20InvalidReceiver(to)` |
| Reverts with no reason | Reverts with `FHERC20InvalidReceiver(to)` |
| Reverts with a reason | Reverts with the receiver's error |
| Returns an `ebool` it did not share with the token | Reverts with `NotShared` |
## What does a callback transfer emit?
The refund step runs on every `AndCall`, even when it refunds zero or `to` has no code. Each call therefore emits `Transfer` and `ConfidentialTransfer` twice: once from `from` to `to`, then once from `to` back to `from`.
The two steps also cancel out in the [indicators](/cofhe/fhe-library/confidential-contracts/fherc20/overview#what-do-the-erc-20-functions-return). A callback transfer leaves both indicators where they were, except that a first-time recipient moves from `0` to `7984.0000`.
## Related pages
* [Call a token from your contract](/cofhe/fhe-library/confidential-contracts/calling-from-contracts)
* [Transfers and disclosure](/cofhe/fhe-library/confidential-contracts/fherc20/core-features)
* [Best practices](/cofhe/fhe-library/confidential-contracts/fherc20/best-practices)
# Overview
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/overview
What the fhenix-confidential-contracts package provides, which token to build on, and how to install and link it
`fhenix-confidential-contracts` is a Solidity package of confidential ERC-20 tokens built on [`FHE.sol`](/cofhe/fhe-library/reference/fhe-sol/overview). Balances and transfer amounts are stored as encrypted handles (`euint64`), so only the accounts the token grants access to can decrypt them. Read this page to pick a contract, install the package, and deploy a token that needs the shared library.
The source is public at [FhenixProtocol/fhenix-confidential-contracts](https://github.com/FhenixProtocol/fhenix-confidential-contracts/tree/v0.4.0/contracts), under the MIT license. These docs cover version `0.4.0`.
## Which contract do you need?
The package ships two families. FHERC20 tokens hold only encrypted balances. `ERC20Confidential` is a normal public ERC-20 with an encrypted balance added next to it.
| Contract | What a holder has | Use it when |
| - | - | - |
| FHERC20 | An encrypted balance only | You issue a new token that is confidential from the start |
| FHERC20ERC20Wrapper | An encrypted balance, backed by an ERC-20 the wrapper holds | You want a confidential version of a token that already exists, such as USDC |
| FHERC20NativeWrapper | An encrypted balance, backed by ETH the wrapper holds | You want confidential ETH, shielded from ETH or WETH |
| ERC20Confidential | A public ERC-20 balance and an encrypted balance, in one contract | Your token must keep working with every ERC-20 integration, and holders opt in to privacy |
On an FHERC20 token, `balanceOf` and `totalSupply` return an indicator value, not a balance, and `transfer`, `transferFrom`, `approve`, and `allowance` revert with `FHERC20IncompatibleFunction`. The indicator exists so wallets and explorers show activity without seeing amounts. On `ERC20Confidential`, the ERC-20 functions behave normally on the public balance.
Every contract above has an upgradeable variant with the `Upgradeable` suffix, which you set up from an initializer instead of a constructor. If your token already has its own ERC-20 implementation, `ERC20ConfidentialCoreUpgradeable` adds only the confidential layer and reaches your ledger through three hooks: `_ledgerMint`, `_ledgerTransfer`, and `_ledgerBalanceOf`.
## What all of them have in common
* **Amounts are 64-bit.** Encrypted amounts are `euint64`. The wrappers and `ERC20Confidential` cap the confidential side at 6 decimals and convert at a fixed rate, so an 18-decimal token moves in steps of 10^12 base units.
* **Operators replace allowances.** `setOperator(operator, until)` lets `operator` move any amount for you until the `until` timestamp. There is no per-amount allowance, so keep `until` short.
* **Two ways to pass an amount.** A wallet calls the overload that takes `externalEuint64` plus `inputProof`, which it gets from the SDK. Another contract calls the overload that takes `sharedEuint64`, created with `FHE.shareEuint64(amount, token)`.
* **A failed transfer moves zero instead of reverting.** If the encrypted amount exceeds the balance, the transfer completes with an amount of zero. A revert would reveal that the balance was too low. Every transfer returns the amount that actually moved.
* **Leaving the confidential side takes two transactions.** `unshield` burns the encrypted amount and opens a claim. Once the amount is decrypted, `claimUnshielded` pays out the public token.
## Install the package
```bash theme={null}
npm install fhenix-confidential-contracts@0.4.0
```
The package depends on `@fhenixprotocol/cofhe-contracts@1.0.0` exactly, plus `@openzeppelin/contracts` and `@openzeppelin/contracts-upgradeable`. Keep your own `cofhe-contracts` pin on the same version, or npm installs two copies of `FHE.sol`. See the [compatibility page](/cofhe/get-started/introduction/compatibility) for the matching SDK and plugin versions.
Import contracts by their path inside the package:
```solidity ConfidentialToken.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { FHE } from "@fhenixprotocol/cofhe-contracts/FHE.sol";
import { FHERC20 } from "fhenix-confidential-contracts/contracts/FHERC20/FHERC20.sol";
contract ConfidentialToken is FHERC20 {
constructor() FHERC20("Confidential Token", "eTKN", 6, "") {
_mint(msg.sender, FHE.asEuint64(1_000_000 * 10 ** 6));
}
}
```
`FHERC20` takes a name, a symbol, the decimals, and a contract URI. It deploys like any other contract.
To send tokens from a wallet, encrypt the amount with the SDK and name the token as the consuming contract. ethers needs the full signature to choose between the transfer overloads:
```typescript theme={null}
import { Encryptable } from '@cofhe/sdk';
const [amount, inputProof] = await client
.encryptInputs([Encryptable.uint64(250_000_000n)])
.setConsumingContract(await token.getAddress())
.execute();
await token['confidentialTransfer(address,bytes32,bytes)'](recipient, amount, inputProof);
```
`externalEuint64` is `bytes32` in the ABI, which is why the signature reads `bytes32`. See [encrypting inputs](/cofhe/client-sdk/guides/encrypting-inputs) for setting up `client`.
## Link the shared library
The wrappers, `ERC20Confidential`, and their upgradeable variants call into `ERC20ConfidentialLib`, an external library. Keeping that logic in a separately deployed library is what holds these tokens under the 24 KB contract size limit. `FHERC20` and `FHERC20Upgradeable` do not use it.
The library address is written into the token's bytecode when you deploy, not passed to the constructor, so it cannot change afterwards. Deploy the library once on each chain, then link every token on that chain to it:
```typescript deploy.ts theme={null}
const LIB =
'fhenix-confidential-contracts/contracts/ERC20Confidential/ERC20ConfidentialLib.sol:ERC20ConfidentialLib';
const lib = await ethers.deployContract(LIB);
await lib.waitForDeployment();
const Factory = await ethers.getContractFactory('ConfidentialUSDC', {
libraries: { [LIB]: await lib.getAddress() },
});
const token = await Factory.deploy(usdcAddress);
```
Use the fully qualified name exactly as shown. The repository README writes it starting at `contracts/`, which only resolves inside that repository. If you forget the `libraries` option, `getContractFactory` throws `missing links for the following libraries` before anything is sent.
For an upgradeable token, the OpenZeppelin upgrades plugin refuses linked libraries unless you allow them:
```typescript theme={null}
const token = await upgrades.deployProxy(Factory, [], { unsafeAllowLinkedLibraries: true });
```
Beta builds published before `0.4.0` shipped an `ERC20ConfidentialLib` with a different ABI but the same function selectors. A token linked to one of those deploys, then fails at runtime. Deploy the library from `0.4.0`.
Turn the Solidity optimizer on before you deploy a wrapper. With it off, `FHERC20NativeWrapper` and `FHERC20ERC20WrapperUpgradeable` compile past the 24,576-byte limit and the deployment reverts. With `runs: 200`, both come in near 15,000 bytes.
## Upgrading from 0.3
Version `0.4.0` follows the `0.7` CoFHE release and changes every function that takes an encrypted amount:
* `InEuint64` parameters are now `externalEuint64` plus `bytes inputProof`. On the `AndCall` functions, `inputProof` comes before `data`.
* Overloads that took a bare `euint64` from another contract now take `sharedEuint64`, and every transfer returns `sharedEuint64`. A calling contract reads the result with `FHE.receiveEuint64FromCall(result, token)`.
* `IERC7984Receiver.onConfidentialTransferReceived` receives a `sharedEuint64` and returns a `sharedEbool`.
* Wrappers and `ERC20Confidential` must be linked to `ERC20ConfidentialLib`.
* Claims are keyed by a claim ID, not by the ciphertext handle. Read the ID from `getUserClaims` before you call `claimUnshielded`.
A contract that passed a bare handle still compiles against `0.4.0`, because `sharedEuint64` has the same ABI encoding as `euint64`. It fails at runtime with `NotShared`. The [changelog](https://github.com/FhenixProtocol/fhenix-confidential-contracts/blob/v0.4.0/CHANGELOG.md) lists every change. For the client side, see [migrating to 0.7](/cofhe/client-sdk/introduction/migrating-to-0-7).
## Next steps
Tokens with encrypted balances only.
Shield an existing token into a confidential one, and claim it back.
A public ERC-20 with an encrypted balance next to it.
Pass encrypted amounts between your contract and a token.
# Upgradeable tokens
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/confidential-contracts/upgradeable
Deploy FHERC20, the wrappers, and ERC20Confidential behind a proxy, and upgrade them safely
Every confidential token in `fhenix-confidential-contracts` has an upgradeable variant for deployment behind a proxy. The variants share their logic with the constructor-based contracts and differ only in setup: you call an initializer instead of a constructor. This page covers the initializers, the deploy and upgrade calls, and what to check before you upgrade an existing proxy.
## Which variant and initializer
| Contract | Upgradeable variant | Initializer to call |
| - | - | - |
| `FHERC20` | `FHERC20Upgradeable` | `__FHERC20_init(name, symbol, decimals, contractURI)` |
| `FHERC20ERC20Wrapper` | `FHERC20ERC20WrapperUpgradeable` | `__FHERC20_init(...)`, then `__FHERC20ERC20Wrapper_init(underlying)` |
| `FHERC20NativeWrapper` | `FHERC20NativeWrapperUpgradeable` | `__FHERC20_init(...)`, then `__FHERC20NativeWrapper_init(weth)` |
| `ERC20Confidential` | `ERC20ConfidentialUpgradeable` | `__ERC20Confidential_init(name, symbol, decimals)` |
The initializers are `internal` and `onlyInitializing`. You call them from your own `initialize` function marked `initializer`. Disable initializers in the constructor so nobody can initialize the implementation contract itself.
## Write the token
An upgradeable FHERC20:
```solidity ConfidentialTokenUpgradeable.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { FHE } from "@fhenixprotocol/cofhe-contracts/FHE.sol";
import { FHERC20Upgradeable } from "fhenix-confidential-contracts/contracts/FHERC20/FHERC20Upgradeable.sol";
contract ConfidentialTokenUpgradeable is FHERC20Upgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address holder) public initializer {
__FHERC20_init("Confidential Token", "eTKN", 6, "");
_mint(holder, FHE.asEuint64(1_000_000 * 10 ** 6));
}
}
```
A wrapper initializes the FHERC20 part and the wrapper part:
```solidity ConfidentialUSDCUpgradeable.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { FHERC20ERC20WrapperUpgradeable } from "fhenix-confidential-contracts/contracts/FHERC20/extensions/FHERC20ERC20WrapperUpgradeable.sol";
contract ConfidentialUSDCUpgradeable is FHERC20ERC20WrapperUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(IERC20 usdc) public initializer {
__FHERC20_init("Confidential USDC", "eUSDC", 6, "");
__FHERC20ERC20Wrapper_init(usdc);
}
}
```
`ERC20ConfidentialUpgradeable` initializes the public ERC-20 and the confidential layer in one call:
```solidity DualTokenUpgradeable.sol theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import { ERC20ConfidentialUpgradeable } from "fhenix-confidential-contracts/contracts/ERC20Confidential/ERC20ConfidentialUpgradeable.sol";
contract DualTokenUpgradeable is ERC20ConfidentialUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize() public initializer {
__ERC20Confidential_init("Dual Token", "DUAL", 18);
_mint(msg.sender, 1_000_000 ether);
}
}
```
`__ERC20Confidential_init` deploys the token's [indicator token](/cofhe/fhe-library/confidential-contracts/dual-mode/dual-balance-model), so each proxy gets its own.
## Deploy and upgrade
With the OpenZeppelin upgrades plugin for Hardhat, `FHERC20Upgradeable` deploys like any upgradeable contract:
```typescript theme={null}
const Token = await ethers.getContractFactory('ConfidentialTokenUpgradeable');
const token = await upgrades.deployProxy(Token, [holder]);
```
The wrappers and `ERC20ConfidentialUpgradeable` call into `ERC20ConfidentialLib`, so you link it into the factory and allow linked libraries on both the deploy and every later upgrade:
```typescript theme={null}
const LIB =
'fhenix-confidential-contracts/contracts/ERC20Confidential/ERC20ConfidentialLib.sol:ERC20ConfidentialLib';
const Wrapper = await ethers.getContractFactory('ConfidentialUSDCUpgradeable', {
libraries: { [LIB]: libAddress },
});
const wrapper = await upgrades.deployProxy(Wrapper, [usdcAddress], { unsafeAllowLinkedLibraries: true });
await upgrades.upgradeProxy(await wrapper.getAddress(), Wrapper, { unsafeAllowLinkedLibraries: true });
```
Without the flag, the plugin rejects the implementation as not upgrade safe. The flag tells it you have checked the library yourself. `ERC20ConfidentialLib` holds no storage of its own: it runs against the token's storage through `delegatecall`. See [link the shared library](/cofhe/fhe-library/confidential-contracts/overview#link-the-shared-library) for deploying the library.
## Where state lives
State sits in ERC-7201 namespaced storage slots, so it does not collide with the storage of contracts you inherit alongside:
| Namespace | Holds |
| - | - |
| `fherc20.storage.FHERC20` | FHERC20 balances, operators, supply, metadata, and indicators |
| `fherc20.storage.FHERC20ERC20Wrapper` | The underlying token, decimals, and rate |
| `fherc20.storage.FHERC20NativeWrapper` | WETH, decimals, and rate |
| `fherc20.storage.ERC20Confidential` | ERC20Confidential encrypted balances, operators, rate, observer, and supply handle |
| `fherc20.storage.FHERC20WrapperClaimHelper` | Unshield claims, for the wrappers and ERC20Confidential |
The constructor-based contracts use the same slots. The public ERC-20 ledger of `ERC20ConfidentialUpgradeable` lives in OpenZeppelin's `ERC20Upgradeable` storage.
## Before you upgrade an existing proxy
An `FHERC20Upgradeable` proxy from `0.3.x` keeps its balances when you upgrade it to `0.4.0`, because the FHERC20 storage slot did not change. Two cases need action:
* **Wrapper proxies with pending claims.** `0.4.0` keys claims by claim ID instead of by handle, in a layout that is not compatible with the `0.3.x` claim store. Claims still pending at upgrade time cannot be settled afterwards. Have users claim, or claim for them, before you upgrade.
* **`ERC20ConfidentialUpgradeable` proxies from a pre-release build.** `confidentialTotalSupply()` reads a stored handle that older implementations did not write, and returns a zero handle until something refreshes it. Call `syncConfidentialTotalSupply()` once after the upgrade. Anyone can call it.
Callers also need the `0.4.0` interface after the upgrade. The [confidential contracts overview](/cofhe/fhe-library/confidential-contracts/overview) lists what changed for callers since `0.3`.
# Access Control
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/access-control
Understanding and managing permissions for encrypted data in confidential smart contracts
## Motivation
Consider the following scenario: Your contract receives an encrypted input that should remain confidential.
```solidity theme={null}
// Contract A
function submitSecretBid(externalEuint32 bid, bytes calldata inputProof) public {
euint32 handle = FHE.asEuint32(bid, inputProof);
// Perform some operations on the encrypted input
}
```
If there were no access control mechanisms, someone could observe the handle value used above and reuse it by initializing a local variable with the same value.
```solidity theme={null}
// Contract B
function attackBid(euint32 seenHandle) public {
euint32 doubled = FHE.add(seenHandle, seenHandle); // try to compute on someone else's ciphertext
FHE.allowSender(doubled); // and hand the result to the attacker
}
```
To prevent misuse, all FHE operations verify that the caller has explicit permission on every input ciphertext handle.
***
## How access control works
In practice, the code above will revert with an `ACLNotAllowed` error because the calling contract doesn't have permission for that ciphertext handle. Any FHE operation will fail if the caller lacks permission for all input handles.
### Unauthorized operations fail
```solidity theme={null}
FHE.add(notAllowedCt, allowedCt); // -> will revert with ACLNotAllowed
```
By default, newly created ciphertext handles are accessible to the contract that created them, but **only for the duration of the transaction**. Any additional access must be explicitly granted.
### What the ACL does *not* gate
The ACL governs **operation creation**. Computing on a handle, and requesting its decryption. It does not gate the onchain publication of a decrypt result:
```solidity theme={null}
// Anyone can call this: it is signature-gated, not ACL-gated
FHE.publishDecryptResult(seenHandle, plaintext, signature);
```
`publishDecryptResult` (and `verifyDecryptResult`) only check the signature. The TaskManager recovers the signer of an ECDSA signature over that exact `(result, encType, chainId, ctHash)` message, and accepts the result only if the recovered address equals the registered `decryptResultSigner`. That address belongs to [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor). Publication is deliberately permissionless so clients and relayers can settle results onchain.
What protects the value is that an attacker cannot *obtain* that signature. Only Teecryptor can produce it. Its signing key never leaves the enclave, so no other party can sign a result the TaskManager will accept. Teecryptor signs only in response to a decryption request, and that request is ACL-gated. The requester must hold permission on the handle and present a valid ACP. The one exception is a handle marked with `FHE.allowPublic()`, which anyone can request. Without permission, Teecryptor denies the request and produces no signature, so there is nothing valid to publish.
Treat `FHE.allowPublic()` as the real decision point. Once a handle is marked public, anyone can request its decryption and publish the plaintext onchain, permanently.
***
## Granting access
CoFHE provides six methods to grant access to ciphertext handles:
**`FHE.allowThis(CIPHERTEXT_HANDLE)`**
Allows the current contract access to the handle. Use this when you want the contract itself to retain access to a ciphertext beyond the current transaction.
```solidity theme={null}
euint32 secretValue = FHE.asEuint32(input);
FHE.allowThis(secretValue); // Contract can access this later
```
**`FHE.allowSender(CIPHERTEXT_HANDLE)`**
Allows the transaction sender (`msg.sender`) access to the handle. Use this when you want to grant the caller of the function access to the ciphertext.
```solidity theme={null}
euint32 secretValue = FHE.asEuint32(input);
FHE.allowSender(secretValue); // Grant access to msg.sender
```
**`FHE.allow(CIPHERTEXT_HANDLE, ADDRESS)`**
Allows a specific address persistent access to the handle. Use this when you want to grant permanent access to another contract or user.
```solidity theme={null}
euint32 secretValue = FHE.asEuint32(input);
FHE.allow(secretValue, recipientAddress); // Grant access to specific address
```
**`FHE.allowTransient(CIPHERTEXT_HANDLE, ADDRESS)`**
Allows a specific address temporary access to the handle for the duration of the transaction only. Use this for cross-contract calls within the same transaction.
```solidity theme={null}
euint32 secretValue = FHE.asEuint32(input);
FHE.allowTransient(secretValue, otherContract); // Temporary access for this tx
```
**`FHE.allowPublic(CIPHERTEXT_HANDLE)`**
Marks a ciphertext handle as eligible for public decryption. Anyone can then request decryption of this value offchain via `decryptForTx` and publish or verify the result onchain.
Use this when a value is intended to become public, for example, the amount burned when you [unshield an FHERC20 token](/cofhe/fhe-library/confidential-contracts/fherc20/fherc20-wrapper).
```solidity theme={null}
euint32 burnedAmount = FHE.asEuint32(input);
FHE.allowPublic(burnedAmount); // Anyone can decrypt and publish this value
```
`allowPublic` does not reveal the value immediately. It only grants permission for anyone to request decryption. The value is revealed only when someone submits the plaintext and signature onchain via `FHE.publishDecryptResult` or `FHE.verifyDecryptResult`.
***
## Who you trust when you grant access
Access to a handle is full access. A contract that holds it, including the contract that created it, can compute on the value, pass access on to others, or allow public decryption.
Pass a handle only to a contract you trust, and check its upgrade path if it is a proxy. A transient grant needs the same trust as a persistent one.
***
## Decryption and access control
Decryption is a multi-step process: a client requests the plaintext and its signature offchain via `decryptForTx`, then publishes or verifies the result onchain.
Access control governs who can request decryption:
* If the ciphertext was marked with `FHE.allowPublic()`, anyone can request decryption without an ACP (`.withoutACP()`).
* Otherwise, only addresses with explicit permission on the handle can request decryption, and must provide a valid ACP (`.withACP()`).
If the requester does not have permission on the ciphertext handle, the decryption request will be denied by the access control system. Grant appropriate permissions before attempting to decrypt, use `FHE.allowPublic()` for values intended to become public, or `FHE.allow()` / `FHE.allowSender()` for restricted access.
***
## How the ACL is stored
Every blockchain integrating CoFHE includes a deployed `ACL.sol` contract. This contract manages ownership records for each ciphertext, ensuring that only authorized owners can perform operations on their encrypted data.
### ACL storage structure
The ACL contract contains the following mapping which tracks the ownership of each ciphertext handle:
```solidity theme={null}
mapping(uint128 handle => mapping(address account => bool isAllowed)) persistedAllowedPairs;
```
This two-level mapping allows efficient lookup of whether a specific address has permission to access a specific ciphertext handle.
***
## Best practices
Only grant access to addresses that genuinely need it. Avoid using `allowPublic()` unless the data is truly meant to be public.
When calling other contracts within a transaction, use `allowTransient()` instead of permanent access to limit exposure.
Keep track of which addresses have access to which ciphertexts, especially in complex multi-contract systems.
A grant cannot be revoked, and the receiver can pass access on. See [who you trust when you grant access](#who-you-trust-when-you-grant-access).
***
## Practical examples
For detailed examples on how to explicitly manage ciphertext allowances in contracts, see the [ACL Usage Examples](/cofhe/tutorials/acl-usage-examples) guide.
### A token transfer
```solidity theme={null}
function transfer(address to, externalEuint32 inAmount, bytes calldata inputProof) public {
euint32 amount = FHE.asEuint32(inAmount, inputProof);
euint32 fromBalance = _balances[msg.sender];
euint32 toBalance = _balances[to];
euint32 newFromBalance = FHE.sub(fromBalance, amount);
euint32 newToBalance = FHE.add(toBalance, amount);
// Grant contract access to store balances
FHE.allowThis(newFromBalance);
FHE.allowThis(newToBalance);
// Optionally grant users access to their own balances
FHE.allow(newFromBalance, msg.sender);
FHE.allow(newToBalance, to);
_balances[msg.sender] = newFromBalance;
_balances[to] = newToBalance;
}
```
# Common Errors
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/common-errors
Critical limitations and important considerations when working with CoFHE, including common issues and Solidity error references
## Overview
This page documents the limits and error messages you are most likely to meet when working with CoFHE, and what to do about each one.
Always verify you're using compatible component versions. Many errors can be resolved by ensuring you're using the latest versions of all CoFHE components.
Encountering cryptic `execution reverted: 0x...` errors? Use the **@fhenixprotocol/cofhe-errors** package to decode them instantly:
```bash theme={null}
npx cofhe-errors 0x118cdaa7
```
See the [CoFHE Errors Package](/cofhe/fhe-library/reference/cofhe-errors) documentation for usage, and the [Error Reference](/cofhe/fhe-library/reference/cofhe-errors-reference) for the complete list. The package predates the current contracts, so a selector it does not know is not necessarily invalid: check the reference.
## Common issues
### Missing revert data
If you encounter a `Missing revert data` error, verify that you're using the latest `cofhe-contracts` version.
Verify the version of `cofhe-contracts` in your project:
```bash npm theme={null}
npm list @fhenixprotocol/cofhe-contracts
```
```bash yarn theme={null}
yarn list --pattern "@fhenixprotocol/cofhe-contracts"
```
```bash pnpm theme={null}
pnpm list @fhenixprotocol/cofhe-contracts
```
Check the [Compatibility](/cofhe/get-started/introduction/compatibility) page to ensure you're using a supported version.
If your version is outdated, update to the latest compatible version:
```bash npm theme={null}
npm install @fhenixprotocol/cofhe-contracts@latest
```
```bash yarn theme={null}
yarn add @fhenixprotocol/cofhe-contracts@latest
```
```bash pnpm theme={null}
pnpm add @fhenixprotocol/cofhe-contracts@latest
```
This section will be expanded over time as new issues arise. If you encounter an issue not documented here, please report it to the Fhenix team.
## Possible errors from Solidity
The errors below are the ones a contract can hit during normal development. Each comes with its selector so you can match it against an `execution reverted: 0x...` message. Admin, upgrade, and signature-parsing errors are listed on the [Error Reference](/cofhe/fhe-library/reference/cofhe-errors-reference) page only.
### Access control
| Error | Description |
| - | - |
| **ACLNotAllowed** `0x4d13139e` | The caller is not allowed to use the handle. Call `FHE.allowThis` or `FHE.allow` on a value before another transaction, or another contract, uses it |
| **SenderNotAllowed** `0xd0d25976` | The account granting or sharing access does not itself have access to the handle. A contract can only allow what it is allowed to use |
| **DirectAllowForbidden** `0x3809a243` | The ACL was called directly instead of through the TaskManager. Use the `FHE.allow*` helpers |
| **NotShared** `0xa70a958e` | A contract tried to receive a shared value that was not passed to it in the same transaction. Pass values between contracts as `sharedEuintXX` |
| **UnexpectedSharer** `0x3a194b4f` | The shared value came from a different contract than the receiver expected |
| **NotOnAccessList** `0xb688c6f5` | The TaskManager access list is enabled and the calling contract is not on it. The list is disabled on the public testnets |
### Input validation
| Error | Description |
| - | - |
| **InvalidEncryptedInput** `0x67cf3071` | The encrypted input's type does not match the `FHE.asE*` conversion you called. Raised inside your contract by the FHE library |
| **InvalidSigner** `0x7ba5ffb5` | An encrypted input was not signed by the ZK Verifier, most often because it was encrypted for a different chain, contract, or account. Also raised when a decryption result was not signed by the Teecryptor |
| **InvalidSignature** `0x8baa579f` | A signature on an encrypted input batch or decryption result does not recover to any signer |
| **InvalidInputsAmount** `0x9a84351c` | The operation received the wrong number of encrypted inputs |
| **InvalidOperationInputs** `0xb31612aa` | The operation received no encrypted inputs, or plaintext extra inputs it does not take |
| **TooManyInputs** `0x2b0399d5` | The operation received more than three inputs in total |
### Types and security zones
| Error | Description |
| - | - |
| **InvalidTypeOrSecurityZone** `0x52b50ae1` | The two operands differ in type or security zone. Cast one side first |
| **InvalidInputType** `0x884a0e9d` | The condition passed to `select` is not an `ebool` |
| **InvalidInputForFunction** `0x91b4b378` | The operation does not accept this type: `eaddress` in arithmetic or comparison, `ebool` in numeric operations, or a plaintext that does not fit the target type in a plaintext conversion such as `FHE.asEuint8` |
| **InvalidSecurityZone** `0x24cbcf36` | A handle or input's security zone is outside the configured range |
| **SecurityZoneOutOfBounds** `0x8f568bf8` | A negative security zone was passed to a plaintext conversion. Raised inside your contract by the FHE library |
| **UnsupportedType** `0xcabe5ce4` | The requested return type or cast target is not a valid encrypted type id |
### Decryption
| Error | Description |
| - | - |
| **DecryptionResultNotReady** `0x70cf6554` | `getDecryptResult` was called before the result was published. Use `FHE.getDecryptResultSafe` to poll without reverting |
| **DecryptFunctionNotSupported** `0x31a7e7ab` | The `decrypt` function id was submitted as a task. Request decryption through the SDK and verify it onchain with `FHE.verifyDecryptResult` |
| **RandomFunctionNotSupported** `0x98e08ab0` | The `random` function id was submitted as an ordinary task. Use `FHE.randomEuint32` and the other `randomE*` helpers |
| **LengthMismatch** `0xff633a38` | The handle, result, and signature arrays passed to a batch decryption call have different lengths |
| **CofheIsUnavailable** `0xd8aba367` | An admin has disabled the coprocessor. Task creation and result publishing revert until it is re-enabled |
### Access Control Permissions
These four errors come from the Access Control Permission (ACP) check in the ACL. They surface when an ACP presented for decryption is rejected.
| Error | Description |
| - | - |
| **PermissionInvalid\_Expired** `0xed0764a1` | The ACP has expired. Create a new one |
| **PermissionInvalid\_IssuerSignature** `0x4c40eccb` | The issuer signature does not verify |
| **PermissionInvalid\_RecipientSignature** `0x8e143bf7` | The recipient signature on a sharing ACP does not verify |
| **PermissionInvalid\_Disabled** `0xcbd3a966` | The revoker contract reports the ACP as revoked |
### Where to look first
* **Access control errors** almost always mean a missing `FHE.allowThis` or `FHE.allow` in an earlier transaction. See [access control](/cofhe/fhe-library/core-concepts/access-control).
* **Input validation errors** usually mean the encrypted input was produced for a different contract, account, or chain. Re-encrypt with the consuming contract set.
* **Type errors** mean the two sides of an operation do not match. Cast with `FHE.asEuintXX` so both operands share a type and zone.
* **Decryption errors** mean a result was read before it existed, or the wrong entry point was used.
## Troubleshooting tips
When encountering errors:
1. **Check error messages carefully**: The error name and description provide clues about what went wrong
2. **Verify input types**: Ensure encrypted values match expected types
3. **Check permissions**: Verify that `FHE.allowThis()` or `FHE.allowSender()` have been called where necessary
4. **Review component versions**: Ensure all CoFHE components are up to date
5. **Test in mock environment**: Use the mock environment to debug issues without network delays
Many errors can be prevented by following [best practices](/cofhe/fhe-library/introduction/best-practices) and ensuring proper access control management.
## Next steps
* Use the [CoFHE Errors Package](/cofhe/fhe-library/reference/cofhe-errors) to decode error selectors
* View the complete [Error Reference](/cofhe/fhe-library/reference/cofhe-errors-reference)
* Review the [Compatibility](/cofhe/get-started/introduction/compatibility) page for version requirements
* Learn about [access control](/cofhe/fhe-library/core-concepts/access-control) to prevent authorization errors
* Check [best practices](/cofhe/fhe-library/introduction/best-practices) for secure FHE development
# Conditions (if .. else)
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/conditions
Understanding why if..else isn't possible with FHE and exploring the alternatives
## Overview
Writing smart contracts with Fully Homomorphic Encryption (FHE) changes how you handle conditionals. Since all data is encrypted, you can't use traditional `if...else` statementsthere's no way to view the values being compared.
Moreover, conditionals in FHE must evaluate both branches simultaneously. This is similar to constant-time cryptographic programming, where branching can leak information through timing attacksfor example, if one path takes longer to execute, an observer could infer which condition was true.
Using traditional `if...else` on encrypted data might result in **unexpected behavior** and leak information about your encrypted values.
***
## The Select Function
To handle encrypted conditionals, Fhenix uses a concept called a **selector**a function that takes an encrypted condition and two possible values, returning one based on the encrypted result.
In practice, this is done with the `select` function. It behaves like a ternary operator (`condition ? a : b`) but works entirely on encrypted data.
### How It Works
`FHE.select` takes the encrypted `ebool` returned by comparison operations like `gt`. If the condition represents encrypted true, it returns the first value; otherwise, it returns the second valueall without revealing which path was taken.
***
## Quick Start
```solidity Don't Do This theme={null}
euint32 a = FHE.asEuint32(10);
euint32 b = FHE.asEuint32(20);
euint32 max;
// This won't work as expected!
if (a.gt(b)) { // gt returns encrypted boolean (ebool)
max = a; // Traditional if..else leaks information
} else {
max = b;
}
```
```solidity Do This Instead theme={null}
euint32 a = FHE.asEuint32(10);
euint32 b = FHE.asEuint32(20);
// Use select for encrypted conditionals
ebool isHigher = a.gt(b);
euint32 max = FHE.select(isHigher, a, b);
```
***
## Key Points to Remember
All operations take place on encrypted data, so the actual values and comparison results stay concealed from observers.
Traditional `if...else` statements on encrypted data leak information through execution paths and timing.
The `select` function is the only way to handle conditional execution in FHE without leaking information.
Both branches are evaluated, then the correct result is selected based on the encrypted condition.
***
## Common Use Cases
Here are some common scenarios where you'll use `select`:
### 1. Maximum/Minimum Operations
Find the larger or smaller of two encrypted values:
```solidity theme={null}
// Maximum
euint32 max = FHE.select(a.gt(b), a, b);
// Minimum
euint32 min = FHE.select(a.lt(b), a, b);
```
### 2. Conditional Updates
Update a value only when a condition is met:
```solidity theme={null}
ebool shouldUpdate = checkCondition();
euint32 newValue = FHE.select(shouldUpdate, updatedValue, currentValue);
// Store the result (either updated or unchanged)
_storedValue = newValue;
```
### 3. Threshold Checks
Cap values at a certain threshold:
```solidity theme={null}
ebool isAboveThreshold = value.gt(threshold);
euint32 result = FHE.select(isAboveThreshold, threshold, value);
// Result will be capped at threshold if value exceeds it
```
### 4. Conditional Access Control
Grant different permissions based on encrypted conditions:
```solidity theme={null}
ebool hasPermission = userRole.eq(ADMIN_ROLE);
euint32 accessLevel = FHE.select(hasPermission, FULL_ACCESS, LIMITED_ACCESS);
```
### 5. Fee Calculations
Apply different rates based on encrypted criteria:
```solidity theme={null}
ebool isPremiumUser = userTier.gt(STANDARD_TIER);
euint32 feeRate = FHE.select(isPremiumUser, PREMIUM_FEE, STANDARD_FEE);
euint32 totalFee = FHE.mul(amount, feeRate);
```
***
## Best Practices
Never try to implement branching logic with traditional `if...else` statements on encrypted data. Always use `select` to ensure constant-time execution and prevent information leakage.
```solidity theme={null}
// Good
euint32 result = FHE.select(condition, valueA, valueB);
// Bad - don't publish decrypted values for branching logic!
FHE.publishDecryptResult(condition, plaintext, signature); // Don't do this!
if (plaintext > 0) {
result = valueA;
} else {
result = valueB;
}
```
Complex nested conditions should be broken down into simpler operations. Each `select` can only choose between two values, so chain them carefully.
```solidity theme={null}
// For multiple conditions, chain select operations
ebool condition1 = a.gt(b);
ebool condition2 = c.lt(d);
euint32 temp = FHE.select(condition1, valueA, valueB);
euint32 result = FHE.select(condition2, temp, valueC);
```
Every value, comparison, and result remains encrypted throughout the entire process. The blockchain never sees plaintext values.
```solidity theme={null}
ebool condition = encryptedValue.gt(encryptedThreshold); // Encrypted comparison
euint32 result = FHE.select(condition, encA, encB); // Encrypted selection
// Both condition and result remain encrypted
```
Since both branches of a `select` are always evaluated, complex operations in both paths will always execute. Structure your code to minimize unnecessary computations.
```solidity theme={null}
// Both computations happen regardless of condition
euint32 expensiveA = complexOperation(a);
euint32 expensiveB = complexOperation(b);
euint32 result = FHE.select(condition, expensiveA, expensiveB);
// Consider pre-computing when possible
```
***
## Complete Example: Auction Bid
Here's a practical example showing how to handle encrypted bids in an auction:
```solidity theme={null}
contract EncryptedAuction {
euint32 public highestBid;
address public lastBidder;
function placeBid(externalEuint32 encryptedBid, bytes calldata inputProof) public {
euint32 bid = FHE.asEuint32(encryptedBid, inputProof);
// Compare new bid with current highest (encrypted comparison)
ebool isHigher = bid.gt(highestBid);
// Update highest bid using select
euint32 newHighestBid = FHE.select(isHigher, bid, highestBid);
// Grant contract access to the new highest bid
FHE.allowThis(newHighestBid);
highestBid = newHighestBid;
// The contract cannot tell whether this bid won: isHigher is
// encrypted, and plain `if` cannot branch on it. Track bidders,
// and resolve the winner when the result is revealed.
lastBidder = msg.sender;
}
function revealWinner(euint32 ctHash, uint32 plaintext, bytes calldata signature) public {
// Verify and publish the decrypted highest bid onchain
FHE.publishDecryptResult(ctHash, plaintext, signature);
}
}
```
In the example above, the actual bid amounts remain encrypted throughout the auction. To reveal the winner, a client calls `decryptForTx` offchain to obtain the plaintext and its Teecryptor signature, then submits them onchain via `revealWinner` for verification.
***
## Related Topics
* Learn more about comparison operations in [FHE Encrypted Operations](/cofhe/fhe-library/core-concepts/encrypted-operations)
* Understand how to manage access in [Access Control](/cofhe/fhe-library/core-concepts/access-control)
# Data Evaluation
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/data-evaluation
How FHE operations reach CoFHE's offchain engine, and how encrypted values are represented in your contract.
## How your contract requests computation
The chain you deploy to, for example Arbitrum One, cannot run FHE computation natively. CoFHE runs it offchain instead. Your contract records which operations to perform; it does not perform them. This is [symbolic execution](https://en.wikipedia.org/wiki/Symbolic_execution): the contract builds a graph of pending operations, and the FHE Engine evaluates it asynchronously.
### How the request reaches the FHE Engine
Through events. Every `FHE.sol` function that needs FHE computation calls `TaskManager.createTask`, and the TaskManager emits a `TaskCreated` event. CoFHE watches for that event and routes the work to the FHE Engine.
```solidity theme={null}
euint32 res = FHE.sub(first, second);
```
This line emits one `TaskCreated` event describing a subtraction over two operands. The FHE Engine computes the ciphertext later, and your transaction continues without waiting.
### More examples
**Creating a trivially encrypted value:**
```solidity theme={null}
euint8 res = FHE.asEuint8(42);
```
This emits a task saying "create a trivially encrypted ciphertext for the plaintext `42`". See [trivial encryption](/cofhe/fhe-library/core-concepts/trivial-encryption) for when this is safe to use.
**Adding encrypted values:**
```solidity theme={null}
balance = FHE.add(amount, balance);
```
This emits a task saying "compute the encrypted sum of the ciphertexts behind `amount` and `balance`".
How does CoFHE connect `amount` and `balance` to the underlying encrypted data? That is what a handle is for.
***
## How encrypted values are represented
A real FHE ciphertext is far too large to store onchain or emit in an event. So your contract never holds one. It holds a **handle**: a 32-byte reference to a ciphertext that CoFHE stores offchain. Every encrypted type in `FHE.sol` is a `bytes32` handle:
```solidity theme={null}
type ebool is bytes32;
type euint8 is bytes32;
type euint16 is bytes32;
type euint32 is bytes32;
type euint64 is bytes32;
type euint128 is bytes32;
type eaddress is bytes32;
```
The `external*` input types (`externalEuint32` and friends) are `bytes32` too. The handle width does not depend on the encrypted type: an `ebool` handle and an `euint128` handle are both 32 bytes.
Handles changed from `uint256` to `bytes32` in `@fhenixprotocol/cofhe-contracts` v0.1.0. This changed the compiled ABI of every function that accepts or returns an encrypted type. Solidity code still compiles, because the `euintNN` wrappers hide the change, but stale ABIs and generated bindings break at runtime. Regenerate them.
So when your contract evaluates this:
```solidity theme={null}
ebool isBigger = FHE.gt(newBid, currentBid);
```
`FHE.sol` submits a task meaning "compare the ciphertexts behind `0xab12...` and `0xcd34...`". The handle of the result lands in `isBigger`, typed `ebool`.
Wondering what to do with `ebool isBigger`? Read [conditionals](/cofhe/fhe-library/core-concepts/conditions). You cannot branch on it with `if`.
### Working with a raw handle
Solidity user-defined value types do not convert implicitly, so `FHE.sol` gives you explicit accessors:
```solidity theme={null}
euint64 balance = FHE.asEuint64(100);
bytes32 raw = FHE.unwrap(balance); // or balance.unwrap()
euint64 restored = FHE.wrapEuint64(raw);
bool ready = FHE.isInitialized(balance); // false when the handle is bytes32(0)
```
`isInitialized` is the check you want before using a handle read from storage. An unwritten storage slot yields `bytes32(0)`, which is not a valid handle.
### What the 32 bytes contain
A handle is not an opaque counter. The TaskManager derives it from the operation itself and packs two metadata fields into the low bytes:
| Bytes | Field | Meaning |
| - | - | - |
| 0-29 | Truncated `keccak256` | Hash of the operand handles concatenated with the operation's function ID. |
| 30 | Trivial flag and type code | High bit set when the value is trivially encrypted. Low 7 bits hold the ciphertext type. |
| 31 | Security zone | The security zone the ciphertext belongs to. |
The type codes in byte 30 are:
| Type | Code |
| - | - |
| `ebool` | 0 |
| `euint8` | 2 |
| `euint16` | 3 |
| `euint32` | 4 |
| `euint64` | 5 |
| `euint128` | 6 |
| `eaddress` | 7 |
Because the type and security zone travel inside the handle, the TaskManager validates an operation without a storage lookup. It reads both fields off the operand handles and reverts on a mismatch, for example `InvalidInputForFunction` when you pass an `eaddress` to an arithmetic operation.
Two ABI types describe the same 32 bytes. Contract functions that expose an encrypted type encode it as `bytes32`. The `ITaskManager` functions, the `InEuintNN` input structs, and the TaskManager events (`TaskCreated`, `DecryptionResult`, `InputVerified`) all use `uint256 ctHash`. If you are writing an offchain decoder, decode against the signature you are actually calling, and cast between the two as needed.
***
## How handles are known before the result exists
The handle does not depend on the ciphertext's value. It is derived from the operation that will produce it, so the TaskManager can compute it synchronously and hand it back in the same call.
`TaskManager.calcPlaceholderKey` hashes the operand handles together with the operation's function ID, then overwrites the low two bytes with the result type and security zone. Nothing in that preimage requires the answer.
**Example:**
```solidity theme={null}
euint64 num = FHE.asEuint64(31);
euint64 meaning = FHE.add(num, FHE.asEuint64(11));
```
The handle of `num` derives from "trivially encrypt `31` as `euint64`". The handle of `meaning` derives from "add these two operand handles". The FHE Engine fills in the ciphertexts afterwards.
***
## Key concepts
FHE operations call `TaskManager.createTask`, which emits a task event for the offchain FHE Engine. The contract records the operation instead of running it.
Every encrypted type is a `bytes32` handle referencing a ciphertext held offchain. The low two bytes carry the ciphertext type and security zone.
The FHE Engine computes offchain. Handles are derived from the operation, not the result, so your contract gets one in the same transaction.
The same operation over the same operands always yields the same handle. Handles are not secrets; access control is enforced by the ACL.
# Decryption Operations
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/decryption-operations
Understanding how to decrypt encrypted data in FHE smart contracts
## Overview
Decryption is the process of converting encrypted data back into its original form. In the context of Fully Homomorphic Encryption (FHE), decryption allows for the retrieval of results after performing computations on encrypted data.
Decryption in CoFHE is a multi-step process that involves both offchain and onchain components:
1. A client requests decryption offchain and receives the plaintext along with a signature from [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor).
2. The plaintext and signature are submitted onchain, where the contract publishes or verifies the result.
Learn more about how CoFHE decrypts in the [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) guide.
***
## Decryption Methods: Transaction vs View
CoFHE provides two primary ways to perform decryption, each suited for different use cases:
### 1. Decrypt for Transaction (`decryptForTx`)
The client calls `decryptForTx(ctHash)` offchain to obtain the plaintext and a Teecryptor signature. These are then submitted onchain via `FHE.publishDecryptResult` or `FHE.verifyDecryptResult`, making the result verifiable by the contract.
**Common examples:**
* **Unshield a confidential token**: reveal the encrypted amount so the contract can finalize the public transfer.
* **Finalize a private auction / game move**: bids or moves are submitted encrypted, and the winner is revealed later in a verifiable way.
### 2. Decrypt for View (`decryptForView`)
The client calls `decryptForView` offchain to obtain the plaintext for display in a UI. No onchain transaction or signature is needed.
**Common examples:**
* Displaying a user's confidential balance in a wallet UI.
* Showing the current state of an encrypted value without revealing it onchain.
Use `decryptForTx` when you need to act on the decrypted value in a smart contract. Use `decryptForView` when you only need to display the value in a UI.
### 3. Client-Published Decryption (Signature-Verified)
The client decrypts offchain via `decryptForTx`, receives the plaintext result along with an **ECDSA signature** from Teecryptor, and then publishes the result onchain by calling `FHE.publishDecryptResult()`. The TaskManager verifies the signature onchain before storing the result.
This combines the best of both worlds: the client controls when the result lands onchain, while the contract can still use the decrypted value.
The signature proves the result came from Teecryptor, whose address the TaskManager holds as the only authorized `decryptResultSigner`. No trust in the publisher is required. Anyone holding a valid signature can submit it.
### Comparison Table
| Method | Visibility | Gas Cost | Smart Contract Usable | Best For |
| - | - | - | - | - |
| **`decryptForTx`** | Public (once published onchain) | Gas for the publish/verify tx | Yes | Public results, contract logic |
| **`decryptForView`** | Private (offchain only) | None | No | UI display, confidential data |
| **Client-Published (signature)** | Public (onchain) | Medium | Yes | Client-driven settlement, permissionless delivery |
***
## The Decryption Flow
### Step 1: Grant decryption permissions (onchain)
Before anyone can request decryption, the ciphertext handle must have the appropriate ACL permissions. Use one of the following in your contract:
* `FHE.allowPublic(ctHash)`: anyone can request decryption (common for unshield flows)
* `FHE.allow(ctHash, address)`: only a specific address can request decryption
* `FHE.allowSender(ctHash)`: only `msg.sender` can request decryption
```solidity theme={null}
// Example: allow anyone to decrypt when the auction closes
function closeBidding() external onlyAuctioneer {
FHE.allowPublic(highestBid);
auctionClosed = true;
}
```
See [Access Control](/cofhe/fhe-library/core-concepts/access-control) for the full list of permission methods, including `FHE.allowPublic()`.
### Step 2: Request decryption offchain (client-side)
The client calls `decryptForTx(ctHash)` to obtain the plaintext and a Teecryptor signature. Choose the ACP mode that matches the contract's ACL policy:
```typescript No ACP (allowPublic) theme={null}
const decryptResult = await client
.decryptForTx(ctHash)
.withoutACP()
.execute();
// decryptResult.ctHash is the ciphertext handle
// decryptResult.decryptedValue is the plaintext (bigint)
// decryptResult.signature is the Teecryptor signature
```
```typescript With ACP (restricted access) theme={null}
const decryptResult = await client
.decryptForTx(ctHash)
.withACP()
.execute();
```
`decryptForTx` always returns the plaintext as a `bigint`. Your contract determines whether that value is interpreted as `uint32`, `uint64`, etc.
### Step 3: Publish or verify onchain
Submit the plaintext and signature to your contract. You have two options:
#### Option A: `FHE.publishDecryptResult`
Publishes the decrypted value onchain, making it available for any contract to read.
```solidity Solidity theme={null}
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
function revealWinner(euint64 ctHash, uint64 plaintext, bytes calldata signature) external onlyAuctioneer {
FHE.publishDecryptResult(ctHash, plaintext, signature);
winningBid = plaintext;
emit RevealedWinningBid(highestBidder, plaintext);
}
```
```typescript TypeScript theme={null}
const tx = await myContract.revealWinner(
decryptResult.ctHash,
decryptResult.decryptedValue,
decryptResult.signature
);
await tx.wait();
```
#### Option B: `FHE.verifyDecryptResult`
Verifies the signature without publishing the result globally. Use this when your contract only needs to confirm the plaintext is authentic.
```solidity theme={null}
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
function unshield(bytes32 ctHash, uint32 plaintext, bytes calldata signature) external {
require(FHE.verifyDecryptResult(ctHash, plaintext, signature), "Invalid decrypt signature");
// ...continue with protocol logic...
}
```
***
## Full Example Contract
Here's a complete example showing the new decryption flow in an auction contract:
```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
contract EncryptedAuction {
euint64 public highestBid;
address public highestBidder;
address public auctioneer;
bool public auctionClosed;
uint64 public winningBid;
event BidPlaced(address indexed bidder);
event AuctionClosed();
event RevealedWinningBid(address indexed winner, uint64 amount);
modifier onlyAuctioneer() {
require(msg.sender == auctioneer, "Only auctioneer can call this");
_;
}
constructor() {
auctioneer = msg.sender;
}
// Place an encrypted bid
function placeBid(externalEuint64 encryptedBid, bytes calldata inputProof) external {
require(!auctionClosed, "Auction is closed");
euint64 bid = FHE.asEuint64(encryptedBid, inputProof);
ebool isHigher = bid.gt(highestBid);
// Update highest bid if this bid is higher
euint64 newHighestBid = FHE.select(isHigher, bid, highestBid);
FHE.allowThis(newHighestBid);
highestBid = newHighestBid;
highestBidder = msg.sender;
emit BidPlaced(msg.sender);
}
// Close the auction and allow public decryption of the winning bid
function closeBidding() external onlyAuctioneer {
require(!auctionClosed, "Auction already closed");
FHE.allowPublic(highestBid);
auctionClosed = true;
emit AuctionClosed();
}
// Reveal the winner by publishing the decrypted result with proof
function revealWinner(euint64 ctHash, uint64 plaintext, bytes calldata signature) external {
require(auctionClosed, "Auction must be closed first");
FHE.publishDecryptResult(ctHash, plaintext, signature);
winningBid = plaintext;
emit RevealedWinningBid(highestBidder, plaintext);
}
}
```
The client-side flow to reveal the winner:
```typescript theme={null}
// 1. Read the encrypted highest bid from the contract
const ctHash = await auctionContract.highestBid();
// 2. Request decryption offchain (no ACP needed since allowPublic was used)
const decryptResult = await client
.decryptForTx(ctHash)
.withoutACP()
.execute();
// 3. Submit the result onchain
const tx = await auctionContract.revealWinner(
decryptResult.ctHash,
decryptResult.decryptedValue,
decryptResult.signature
);
await tx.wait();
```
***
## Batch Client-Published Decryption
`FHE.publishDecryptResultBatch` takes typed arrays: one overload per encrypted type (`ebool[]`, `euint8[]` through `euint128[]`, and `eaddress[]`), each paired with its plaintext type.
```solidity theme={null}
function publishMultipleResults(
euint64[] memory handles,
uint64[] memory results,
bytes[] memory signatures
) external {
FHE.publishDecryptResultBatch(handles, results, signatures);
}
```
***
## Signature Verification Functions
When using client-published decryption, two verification functions are available:
| Function | Behavior on Invalid Signature |
| - | - |
| `FHE.verifyDecryptResult(ctHash, result, signature)` | Reverts |
| `FHE.verifyDecryptResultSafe(ctHash, result, signature)` | Returns `false` |
Both functions accept type-specific overloads for `ebool`, `euint8`, `euint16`, `euint32`, `euint64`, `euint128`, and `eaddress`.
***
## Best Practices
When a value is intended to become public (e.g. unshielding, auction reveals), use `FHE.allowPublic()` so anyone can trigger the decryption without needing an ACP.
Ensure only authorized parties can request decryption. Use `FHE.allow()` or `FHE.allowSender()` for restricted access. See [Access Control](/cofhe/fhe-library/core-concepts/access-control).
Use `FHE.publishDecryptResult` when you want the result stored publicly onchain. Use `FHE.verifyDecryptResult` when you only need to confirm the plaintext is authentic without publishing it.
If you only need to display a value in your UI and don't need an onchain-verifiable signature, use `decryptForView` instead of `decryptForTx` to avoid unnecessary onchain transactions.
***
## Common Pitfalls
* **Missing ACL permissions**: If no `allow*` was called for the ciphertext handle, decryption requests will be denied. Make sure to grant permissions before the client requests decryption.
* **ACP mode must be selected**: When using `decryptForTx`, you must call exactly one of `.withACP(...)` or `.withoutACP()` before `.execute()`.
* **Wrong chain/account**: ACPs are scoped to `chainId + account`. If you get an ACL or ACP error, double-check you're connected to the expected chain and account.
* **Type mismatch**: `decryptedValue` is always a `bigint`. If your Solidity function expects a smaller integer type (e.g. `uint32`), make sure the value is within range.
***
## Related Topics
* Learn about access control requirements in [Access Control](/cofhe/fhe-library/core-concepts/access-control)
* Understand asynchronous operations in [Data Evaluation](/cofhe/fhe-library/core-concepts/data-evaluation)
* Explore the decryption request flow in [Decryption Request Flow](/cofhe/deep-dive/data-flows/decryption-request-flow)
# FHE Encrypted Operations
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/encrypted-operations
Complete guide to FHE types and operations for confidential smart contracts
## Overview
The library exposes utility functions for FHE operations. The goal of the library is to let you write smart contracts that operate on confidential data.
## Types
The library provides a type system that is checked both at compile time and at run time. The structure and operations related to these types are described in this section.
We currently support encrypted integers of bit length up to 128 bits and special types such as `ebool` and `eaddress`.
The encrypted integers behave as much as possible as Solidity's integer types. However, behavior such as "revert on overflow" is not supported as this would leak some information about the encrypted integers. Therefore, arithmetic on `euint` types is [unchecked](https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic), i.e. there is wrap-around on overflow.
In the back-end, encrypted integers are FHE ciphertexts. The library abstracts away the ciphertexts and presents pointers to ciphertexts, or ciphertext handles, to the smart contract developer. The `euint`, `ebool` and `eaddress` types are *wrappers* over these handles.
### Supported Types
| Name | Bit Size | Usage |
| - | - | - |
| `euint8` | 8 | Compute |
| `euint16` | 16 | Compute |
| `euint32` | 32 | Compute |
| `euint64` | 64 | Compute |
| `euint128` | 128 | Compute |
| `ebool` | 8 | Compute |
| `eaddress` | 160 | Compute |
| Name | Bit Size | Usage |
| - | - | - |
| `externalEuint8` | 8 | Input |
| `externalEuint16` | 16 | Input |
| `externalEuint32` | 32 | Input |
| `externalEuint64` | 64 | Input |
| `externalEuint128` | 128 | Input |
| `externalEbool` | 8 | Input |
| `externalEaddress` | 160 | Input |
| Name | Bit Size | Usage |
| - | - | - |
| `sharedEuint8` | 8 | Contract to contract |
| `sharedEuint16` | 16 | Contract to contract |
| `sharedEuint32` | 32 | Contract to contract |
| `sharedEuint64` | 64 | Contract to contract |
| `sharedEuint128` | 128 | Contract to contract |
| `sharedEbool` | 8 | Contract to contract |
| `sharedEaddress` | 160 | Contract to contract |
Each family records where a value came from. A **compute** type is yours, held in storage or produced by an operation. An **input** type arrived from a user offchain and travels with a `bytes` proof that authenticates it. A **shared** type was handed over by another contract during this transaction. See [inputs](/cofhe/fhe-library/core-concepts/inputs) for how to convert between them.
The `ebool` type is not a real boolean type. It is implemented as a `euint8` for compatibility with FHE operations.
***
## Operations
There are two ways to perform operations with FHE.sol:
### Using Direct Function Calls
Direct function calls are the most straightforward way to perform operations with FHE.sol. For example, if you want to add two encrypted 8-bit integers (euint8), you can do so as follows:
```solidity theme={null}
euint8 result = FHE.add(lhs, rhs);
```
Here, `lhs` and `rhs` are your `euint8` variables, and `result` will store the outcome of the addition.
### Using Library Bindings
FHE.sol also provides library bindings, allowing for a more natural syntax. To use this, you first need to include the library for your specific data type. For `euint8`, the usage would look like this:
```solidity theme={null}
euint8 result = lhs.add(rhs);
```
In this example, `lhs.add(rhs)` performs the addition using the library function implicitly.
***
## Supported Operations
Complete documentation of every function in FHE.sol (including inputs and outputs) can be found in the [FHE.sol reference](/cofhe/fhe-library/reference/fhe-sol/overview).
All operations supported by FHE.sol are listed in the table below. Note that all functions are supported in both direct function calls and library bindings.
| Name | FHE.sol function | Operator | euint8 | euint16 | euint32 | euint64 | euint128 | ebool | eaddress |
| - | - | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| Addition | `add` | `+` | Yes | Yes | Yes | Yes | Yes | No | No |
| Subtraction | `sub` | `-` | Yes | Yes | Yes | Yes | Yes | No | No |
| Multiplication | `mul` | `*` | Yes | Yes | Yes | Yes | Yes | No | No |
| Bitwise And | `and` | `&` | Yes | Yes | Yes | Yes | Yes | Yes | No |
| Bitwise Or | `or` | `\|` | Yes | Yes | Yes | Yes | Yes | Yes | No |
| Bitwise Xor | `xor` | `^` | Yes | Yes | Yes | Yes | Yes | Yes | No |
| Division | `div` | `/` | Yes | Yes | Yes | Yes | Yes | No | No |
| Remainder | `rem` | `%` | Yes | Yes | Yes | Yes | Yes | No | No |
| Square | `square` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Shift Right | `shr` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Shift Left | `shl` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Rotate Right | `ror` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Rotate Left | `rol` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Equal | `eq` | n/a | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Not equal | `ne` | n/a | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Greater than or equal | `gte` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Greater than | `gt` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Less than or equal | `lte` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Less than | `lt` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Min | `min` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Max | `max` | n/a | Yes | Yes | Yes | Yes | Yes | No | No |
| Not | `not` | n/a | Yes | Yes | Yes | Yes | Yes | Yes | No |
| Select | `select` | n/a | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
**Division and remainder by `0`**: Neither operation reverts. `div` returns the maximum value of the operand type (encrypted 255 for `euint8`), and `rem` returns the dividend unchanged. The FHE Engine cannot inspect the encrypted divisor, so it cannot raise an error without leaking whether the divisor is zero.
***
## Operation Examples
### Arithmetic Operations
```solidity theme={null}
// Addition
euint32 sum = FHE.add(a, b);
// or using library binding
euint32 sum = a.add(b);
// Multiplication
euint32 product = FHE.mul(a, b);
```
### Comparison Operations
```solidity theme={null}
// Returns an encrypted boolean (ebool)
ebool isGreater = FHE.gt(amount, threshold);
ebool isEqual = FHE.eq(value1, value2);
```
### Bitwise Operations
```solidity theme={null}
// Bitwise AND
euint16 masked = FHE.and(flags, mask);
// Bitwise shifts
euint32 shifted = FHE.shl(value, 4); // Shift left by 4 bits
```
### Control Flow
```solidity theme={null}
// Select between two values based on encrypted condition
euint32 result = FHE.select(condition, valueIfTrue, valueIfFalse);
```
Learn more about using encrypted conditionals in the [Select vs If-Else](/cofhe/fhe-library/core-concepts/conditions) guide.
# Gas Costs and Benchmarks
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/gas-and-benchmarks
The gas an FHE operation and a confidential transfer take, and where to find operation latency benchmarks
An FHE operation costs gas onchain to request it. The encrypted math itself runs offchain in the [compute pipeline](/cofhe/deep-dive/cofhe-components/compute-pipeline) and costs no gas.
## Gas per confidential transfer
Whole transactions on the reference [FHERC20](/cofhe/fhe-library/confidential-contracts/fherc20/overview) token, with an encrypted input amount where the call takes one:
| Action | Gas |
| - | - |
| `confidentialTransfer` to an existing holder | 421,500 |
| `confidentialTransfer` to a new holder | 474,400 |
| confidentialTransferFrom by an operator | 424,800 |
| `setOperator`, a one-time approval | 46,700 |
## Gas per operation
The [gas per operation reference](/cofhe/fhe-library/reference/gas-costs) lists the expected gas of every `FHE.sol` operation. Most operations take 40,000 to 55,000 gas as the first FHE call in a transaction, and about 20,000 less as a later call.
## Performance benchmarks
The compute pipeline executes operations with [TFHE-rs](https://github.com/zama-ai/tfhe-rs), Zama's FHE library. For operation-level latency, use Zama's published numbers: [TFHE-rs benchmarks](https://docs.zama.ai/tfhe-rs/tfhe-rs/1.5/get-started/benchmarks).
Read them as the cost of the raw operation on reference hardware. End-to-end latency in CoFHE adds the steps around the operation: event pickup, validation, and commitment posting.
# Handles, wrap, and unwrap
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/handles
What a ciphertext handle is, and what wrapping one does and does not give you
Every encrypted value in a contract is a **handle**: a `bytes32` identifier for a ciphertext that CoFHE holds offchain. `euint32`, `ebool`, and the rest are Solidity user-defined value types over that `bytes32`.
```solidity theme={null}
type euint32 is bytes32;
```
`FHE.unwrap` and `FHE.wrapEuintXX` move between the two spellings. They are the only FHE functions that do no work at all.
## What the two functions do
```solidity theme={null}
bytes32 handle = FHE.unwrap(myValue); // euint32 -> bytes32
euint32 value = FHE.wrapEuint32(handle); // bytes32 -> euint32
```
Both are `internal pure`. They perform no cryptography, make no call to the TaskManager, touch no storage, and cost nothing beyond the surrounding code. They change how Solidity types the same 32 bytes, and nothing else.
There is one `unwrap` for every encrypted type, and one `wrap` per type because the return type has to differ: `wrapEbool`, `wrapEuint8`, `wrapEuint16`, `wrapEuint32`, `wrapEuint64`, `wrapEuint128`, and `wrapEaddress`.
The bindings give you the same thing in dot form, so `myValue.unwrap()` reads better inside an expression. See [bindings](/cofhe/fhe-library/reference/fhe-sol/bindings).
## Wrapping does not grant permission
**Wrapping a handle grants you nothing.**
The type records what kind of value this is. The [ACL](/cofhe/fhe-library/core-concepts/access-control) decides whether you may use it. Those are separate systems, and `wrap` only touches the first one.
You can wrap any 32 bytes you like. Nothing reverts, because nothing is checked:
```solidity theme={null}
euint32 notYours = FHE.wrapEuint32(someHandleYouFoundInAnEvent);
```
That line succeeds. The first FHE operation on `notYours` is where it fails, because the TaskManager checks whether **this contract** is allowed on that handle:
```solidity theme={null}
euint32 doubled = FHE.add(notYours, notYours); // reverts: not allowed
```
So `wrap` is a cast, not an acquisition. Holding a typed value is not the same as being permitted to compute on it.
## Handles are public
A handle is not a secret. It sits in contract storage, travels in calldata, and shows up in event logs. Anyone can read one.
That is fine, because a handle reveals nothing about the plaintext. It is an identifier, not the ciphertext and not the value. Knowing that Alice's balance is handle `0x9f3c…` tells you nothing about the balance.
What a handle **does** give its holder is the ability to name that ciphertext in a call. That is why the ACL exists, and why the next section matters.
## When to unwrap
Reach for `unwrap` when you need the value to be plain `bytes32` because something else demands it:
* Storing a handle in a generic `bytes32` slot or struct field.
* Emitting a handle in an event so a client can read it back.
* Using a handle as a mapping key.
* Comparing two handles for identity, which asks "is this the same ciphertext", not "are these values equal". For an encrypted comparison use `FHE.eq`.
```solidity theme={null}
event BalanceUpdated(address indexed user, bytes32 handle);
emit BalanceUpdated(msg.sender, FHE.unwrap(_balances[msg.sender]));
```
## When to wrap
Reach for `wrapEuintXX` when you are recovering a handle **your own contract** already owns and stored as `bytes32`:
```solidity theme={null}
mapping(address => bytes32) private _rawBalances;
function balanceOf(address user) internal view returns (euint32) {
return FHE.wrapEuint32(_rawBalances[user]);
}
```
This is safe because the contract is already allowed on the handle. Wrapping only restores the type the storage slot lost.
## When not to wrap
Do not wrap a handle that arrived from outside the contract.
A function that accepts a `bytes32` (or a bare `euintXX`) from a caller and computes on it can be turned into a decryption oracle.
FHE operations check the permission of the **contract performing them**, not of whoever called it. A contract is always allowed on its own stored state. So an attacker passes a handle read from your storage or an event. The operation passes the ACL check because *you* hold the value, and the function hands back something derived from a ciphertext they were never meant to read.
Use a type that carries provenance instead. There is one for every way a value can reach you, so a wrapped `bytes32` parameter is never the right answer:
| Where the value came from | Parameter type | How you convert it |
| - | - | - |
| A user, offchain | `externalEuintXX` plus a `bytes` proof | `FHE.asEuintXX(handle, proof)` |
| Another contract, as an argument | `sharedEuintXX` | `FHE.receiveEuintXXParam(shared)`, which checks the sharer is `msg.sender` |
| Another contract, as a return value | `sharedEuintXX` | `FHE.receiveEuintXXFromCall(shared, callee)`, which checks the sharer is the contract you called |
| Your own storage or computation | `euintXX` | `wrapEuintXX` if you stored it as `bytes32` |
Each conversion authenticates the value as part of converting it, which is exactly what wrapping a raw handle skips. The two `receive` forms are not interchangeable: pick by how the value arrived, because naming the wrong party silently weakens the check rather than failing.
All of these are covered in [inputs](/cofhe/fhe-library/core-concepts/inputs).
## Checking a handle is real
`wrap` accepts anything, so a wrapped value can be meaningless. `FHE.isInitialized` tells you whether a handle is non-zero, which catches an unset storage slot:
```solidity theme={null}
if (!FHE.isInitialized(balance)) {
// never written, treat as zero
}
```
It does not tell you the handle refers to a ciphertext that exists, or that you are allowed on it. Only the operation itself can tell you that.
## Related
* [Inputs](/cofhe/fhe-library/core-concepts/inputs): how values arrive from users and from other contracts.
* [Access control](/cofhe/fhe-library/core-concepts/access-control): what the ACL permits, and how to grant it.
* [Utility functions](/cofhe/fhe-library/reference/fhe-sol/utility): the reference entries for `unwrap`, `wrap`, and `isInitialized`.
# Inputs
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/inputs
Learn how to handle encrypted user inputs in confidential smart contracts
## Overview
One of the key aspects of writing confidential smart contracts is receiving encrypted inputs from users:
```solidity theme={null}
function transfer(
address to,
externalEuint32 inAmount, // <------ encrypted input here
bytes calldata inputProof // <------ the proof that authenticates it
) public virtual returns (euint32 transferred) {
euint32 amount = FHE.asEuint32(inAmount, inputProof);
}
```
Notice in the example above the distinction between **`externalEuint32`** and **`euint32`**.
## Input Types Conversion
The **input types** `externalEuintXX` (and `externalEbool`, `externalEaddress`) represent **user input**. Each one is a ciphertext handle that travels with a separate `bytes` proof, and the pair is what lets the contract authenticate the value. For more on that, read about the [ZK-Verifier](/cofhe/deep-dive/cofhe-components/zk-verifier).
Before you can use an encrypted input, convert it to a regular **encrypted type**, passing the proof alongside the handle:
```solidity theme={null}
euint32 amount = FHE.asEuint32(inAmount, inputProof);
```
One signature covers every encrypted input in a call, so the proof is shared. It follows the handle it authenticates rather than sitting last in the parameter list.
Avoid storing `externalEuintXX` values in contract state. They are unverified until you convert them, so storing one keeps a handle the contract has not authenticated. Always convert with `FHE.asE...()` first.
Now that `amount` is of type `euint32`, you can store or manipulate it:
```solidity theme={null}
toBalance = FHE.sub(toBalance, amount);
```
Read more about the available FHE types and operations in the [FHE Encrypted Operations](/cofhe/fhe-library/core-concepts/encrypted-operations) guide.
## Full Example
Here's a complete example showing how to handle encrypted inputs in a transfer function:
```solidity theme={null}
function transfer(
address to,
externalEuint32 inAmount,
bytes calldata inputProof
) public virtual returns (euint32 transferred) {
euint32 amount = FHE.asEuint32(inAmount, inputProof);
toBalance = _balances[to];
fromBalance = _balances[msg.sender];
_updateBalance(to, FHE.add(toBalance, amount));
_updateBalance(from, FHE.sub(fromBalance, amount));
}
```
For the example above to work correctly, you will also need to manage access to the newly created ciphertexts in the `_updateBalance()` function. Learn more about access control in the [ACL Mechanism](/cofhe/fhe-library/core-concepts/access-control) guide.
## Passing encrypted values between contracts
`externalEuintXX` is for values arriving from a user. A value arriving from **another contract** uses `sharedEuintXX` instead.
The distinction matters for security. FHE operations check the permission of the contract performing them, not of whoever called it. A function that accepts a bare `euintXX` from outside can therefore be handed any handle that contract is allowed on, including one read out of its own storage. It can then be made to return something derived from it.
`sharedEuintXX` closes that. The sharer grants access and records itself in the same step, and the receiver checks who handed the value over:
```solidity theme={null}
// Sender: grants access and directs the value at one receiver
token.pull(FHE.shareEuint64(amount, address(token)));
// Receiver: unwraps it, checking the sharer is the caller
function pull(sharedEuint64 shared) external {
euint64 amount = FHE.receiveEuint64Param(shared);
...
}
```
Pick the receive form by how the value reached you:
| How it arrived | Use | Sharer is checked against |
| - | - | - |
| An argument to your function | `receiveEuintXXParam(shared)` | your caller (`msg.sender`) |
| The return value of a call you made | `receiveEuintXXFromCall(shared, callee)` | the contract you called |
For `receiveEuintXXFromCall`, `callee` must be the address called in that same expression. Naming a merely trusted address checks who *created* the share rather than who *handed it over*.
A share is single-use and lasts one transaction, so it cannot be stored, replayed, or rebuilt from an event. To keep a received value, call `FHE.allowThis` on the unwrapped `euintXX`. Expect `NotShared` when nothing was shared with you, `UnexpectedSharer` when the share came from someone other than the party you named, and `SenderNotAllowed` when the sharer does not hold the handle.
The old spelling, an `FHE.allowTransient` grant plus a bare `euintXX` parameter, still compiles and still runs. The compiler will not find these for you, so they have to be found by search. See [migrating to 0.7](/cofhe/client-sdk/introduction/migrating-to-0-7) for the full pass.
## Additional Examples
### Voting in a Poll
```solidity theme={null}
function castEncryptedVote(address poll, externalEbool encryptedVote, bytes calldata inputProof) public {
_submitVote(poll, FHE.asEbool(encryptedVote, inputProof));
}
```
### Setting Encrypted User Preferences
```solidity theme={null}
function updateUserSetting(address user, externalEuint8 encryptedSetting, bytes calldata inputProof) public {
_applyUserSetting(user, FHE.asEuint8(encryptedSetting, inputProof));
}
```
# Randomness
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/randomness
Generating encrypted random values in confidential smart contracts
## Overview
The FHE library provides cryptographically secure random number generation that produces encrypted random values. The values are generated in encrypted form, so no one can see the plaintext until it is explicitly decrypted.
## Random Functions
| Function | Return Type |
| - | - |
| `FHE.randomEuint8()` | `euint8` |
| `FHE.randomEuint16()` | `euint16` |
| `FHE.randomEuint32()` | `euint32` |
| `FHE.randomEuint64()` | `euint64` |
| `FHE.randomEuint128()` | `euint128` |
## Usage
```solidity theme={null}
import { FHE, euint8, euint32, ebool } from "@fhenixprotocol/cofhe-contracts/FHE.sol";
contract RandomExample {
euint32 private storedRandom;
// Basic random generation
function generateRandom() public {
euint32 random = FHE.randomEuint32();
FHE.allowThis(random);
storedRandom = random;
}
// Random in range [0, 99]
function randomInRange() public returns (euint32) {
euint32 random = FHE.randomEuint32();
euint32 result = FHE.rem(random, FHE.asEuint32(100));
FHE.allowThis(result);
return result;
}
// Efficient random boolean (coin flip)
function randomBool() public returns (ebool) {
euint8 random = FHE.randomEuint8();
// AND with 1 forces the value into {0,1}, ensuring a uniform 50/50 distribution.
ebool result = FHE.asEbool(FHE.and(random, FHE.asEuint8(1)));
FHE.allowThis(result);
return result;
}
}
```
Call `FHE.allowThis()` on random values if you need to store or use them later in your contract.
# Require
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/require
Understanding require statements with encrypted data in FHE
## Overview
The `require` statement in FHE contracts works similarly to [Conditions (if .. else)](/cofhe/fhe-library/core-concepts/conditions) because both involve conditional logic on encrypted data. Just like you can't use traditional `if...else` statements with encrypted values, you also can't use standard `require` statements that depend on encrypted conditions.
Traditional `require` statements that check encrypted conditions will leak information about your encrypted values through execution paths.
***
## Why Require is Like Conditions
Both `require` and conditional statements face the same fundamental challenge in FHE:
The condition being checked is encrypted, so the contract can't directly evaluate it to decide whether to revert.
If a transaction reverts based on an encrypted condition, observers can infer information about the encrypted data.
***
## The Solution
Instead of using `require` with encrypted conditions, you should:
1. **Use `FHE.select`** to handle the conditional logic (see [Conditions (if .. else)](/cofhe/fhe-library/core-concepts/conditions))
2. **Only use `require` for non-encrypted conditions** like access control checks, address validation, or other plaintext values
```solidity theme={null}
// Good - require on plaintext condition
require(msg.sender == owner, "Not authorized");
// Good - use select for encrypted logic
euint32 result = FHE.select(encryptedCondition, valueA, valueB);
// Bad - require on encrypted condition is not possible!
// FHE.decrypt was removed — decryption is now offchain only
// require(FHE.decrypt(encryptedValue.gt(threshold)), "Value too low");
```
***
## Learn More
For detailed guidance on handling conditional logic with encrypted data, see the [Conditions (if .. else)](/cofhe/fhe-library/core-concepts/conditions) page.
# Trivial Encryption
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/core-concepts/trivial-encryption
Understanding the difference between trivially encrypted numbers and encrypted inputs
## Overview
In FHE-enabled smart contracts, you often need to perform operations between encrypted values and regular plaintext values. **Trivial encryption** is the operation of converting a plaintext value into an encrypted format that can interact with other encrypted data.
This conversion is done using the `FHE.asEuint` family of functions, which take standard Solidity types and transform them into their encrypted counterparts.
```solidity theme={null}
uint16 number = 2;
euint16 encrypted_number = FHE.asEuint16(number);
```
## Privacy Considerations
**Trivially encrypted values are not confidential** - they are merely a tool to enable interaction between encrypted and non-encrypted types. The original plaintext value remains visible to anyone observing the blockchain, just in a different format.
This is a crucial concept to understand when developing confidential contracts. Any value that is trivially encrypted (e.g. `encrypted_number`) should be treated as public information, even though it uses the same data type as truly encrypted values.
`euints` are only confidential when they are formed from encrypted `inEuint` inputs, which are encrypted offchain. Learn more in the [Data Evaluation](/cofhe/fhe-library/core-concepts/data-evaluation) guide.
When two trivially-encrypted numbers are combined in an FHE operation, the result is still not confidential, because an observer can keep track of the calculations.
## Example
```solidity theme={null}
function doSomeCalculations(externalEuint16 input, bytes calldata inputProof) public {
// public
euint16 number2 = FHE.asEuint16(2);
euint16 number3 = FHE.asEuint16(3);
euint16 number5 = FHE.add(number2, number3);
// confidential
euint16 encInput = FHE.asEuint16(input, inputProof);
euint16 encMul = FHE.mul(encInput, number5);
// public
ebool eFalse = FHE.asEbool(false);
// Observer knows that result is encInput, but not what the value is
euint16 result = FHE.select(eFalse, encMul, encInput);
}
```
# Encrypted Auction Example
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/examples/auction-example
A complete example of building a confidential auction system using FHE
## Overview
This example demonstrates how to build a fully confidential auction system where bids remain encrypted throughout the bidding process. Only when the auction closes can the winner be revealed, ensuring that bidders cannot see or react to each other's bids.
### What You'll Learn
In this example, you'll see practical implementations of:
* **Encrypted input handling** - Processing encrypted bid amounts
* **Encrypted comparisons** - Finding the highest bid without revealing values
* **Conditional logic with `select`** - Updating the highest bidder based on encrypted conditions
* **Access control management** - Properly managing permissions for encrypted data
* **Decrypt-with-proof pattern** - Using `decryptForTx` offchain and `publishDecryptResult` onchain to reveal the winner
***
## How It Works
The auction follows this flow:
The auctioneer deploys the contract, which initializes the auction with zero bid and address values, both encrypted.
Participants submit bids by sending plaintext amounts that are immediately encrypted. Each bid is compared against the current highest bid using encrypted comparison (`FHE.gt`), and the highest bid and bidder are updated accordingly.
The auctioneer closes the auction and calls `FHE.allowPublic` on the highest bid and bidder, making them eligible for public decryption.
Anyone can call `decryptForTx` offchain to obtain the plaintext values and their signatures from [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor).
The decrypted values and signatures are submitted onchain via `revealWinner`, which calls `FHE.publishDecryptResult` to verify the proofs and store the results.
***
## Key Concepts Demonstrated
### 1. Encrypted State Variables
The contract stores the highest bid and bidder as encrypted values:
```solidity theme={null}
euint64 private highestBid; // Encrypted bid amount
eaddress private highestBidder; // Encrypted bidder address
```
These values remain encrypted throughout the entire auction, preventing anyone from seeing the current highest bid.
### 2. Encrypted Comparisons and Updates
When a new bid comes in, the contract uses encrypted operations to update the highest bid:
```solidity theme={null}
euint64 eAmount = FHE.asEuint64(amount); // Encrypt the bid
ebool isHigher = FHE.gt(eAmount, highestBid); // Compare encrypted values
highestBid = FHE.max(eAmount, highestBid); // Take the maximum
highestBidder = FHE.select(isHigher, newBidder, currentBidder); // Update bidder
```
### 3. Decrypt-with-Proof Pattern
The contract demonstrates the new decryption flow:
**Step 1:** Close auction and allow public decryption (onchain)
```solidity theme={null}
FHE.allowPublic(highestBid);
FHE.allowPublic(highestBidder);
```
**Step 2:** Request decryption offchain (client-side)
```typescript theme={null}
const bidResult = await client.decryptForTx(bidCtHash).withoutACP().execute();
const bidderResult = await client.decryptForTx(bidderCtHash).withoutACP().execute();
```
**Step 3:** Publish results onchain with proof
```solidity theme={null}
FHE.publishDecryptResult(highestBid, plaintext, signature);
FHE.publishDecryptResult(highestBidder, plaintextAddress, bidderSignature);
```
***
## Complete Contract Code
Here's the full implementation of the encrypted auction contract:
```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
contract AuctionExample {
address private auctioneer;
euint64 private highestBid;
eaddress private highestBidder;
uint64 public winningBid;
address public winningBidder;
bool public auctionClosed;
event BidPlaced(address indexed bidder);
event AuctionClosed();
event RevealedWinningBid(address winner, uint64 amount);
modifier onlyAuctioneer() {
require(
msg.sender == auctioneer,
"Only the auctioneer can call this function"
);
_;
}
constructor() {
auctioneer = msg.sender; // Set deployer as auctioneer
auctionClosed = false;
highestBid = FHE.asEuint64(0);
highestBidder = FHE.asEaddress(address(0));
// Preserve ownership for further access
FHE.allowThis(highestBid);
FHE.allowThis(highestBidder);
}
function bid(uint64 amount) external {
require(!auctionClosed, "Auction is closed");
euint64 eAmount = FHE.asEuint64(amount);
ebool isHigher = FHE.gt(eAmount, highestBid);
highestBid = FHE.max(eAmount, highestBid);
highestBidder = FHE.select(
isHigher,
FHE.asEaddress(msg.sender), // Encrypt the sender's address
highestBidder
);
// Preserve ownership for further access
FHE.allowThis(highestBid);
FHE.allowThis(highestBidder);
emit BidPlaced(msg.sender);
}
// Close the auction and allow public decryption
function closeBidding() external onlyAuctioneer {
require(!auctionClosed, "Auction is already closed");
FHE.allowPublic(highestBid);
FHE.allowPublic(highestBidder);
auctionClosed = true;
emit AuctionClosed();
}
// Reveal the winner by publishing decrypted results with proof
function revealWinner(
euint64 bidCtHash,
uint64 bidPlaintext,
bytes calldata bidSignature,
eaddress bidderCtHash,
address bidderPlaintext,
bytes calldata bidderSignature
) external {
require(auctionClosed, "Auction isn't closed");
FHE.publishDecryptResult(bidCtHash, bidPlaintext, bidSignature);
FHE.publishDecryptResult(bidderCtHash, bidderPlaintext, bidderSignature);
winningBid = bidPlaintext;
winningBidder = bidderPlaintext;
emit RevealedWinningBid(bidderPlaintext, bidPlaintext);
}
}
```
***
## Code Walkthrough
### Constructor
The constructor initializes the auction with encrypted zero values:
```solidity theme={null}
constructor() {
auctioneer = msg.sender;
auctionClosed = false;
highestBid = FHE.asEuint64(0);
highestBidder = FHE.asEaddress(address(0));
// Grant contract access to these encrypted values
FHE.allowThis(highestBid);
FHE.allowThis(highestBidder);
}
```
The `FHE.allowThis()` calls are crucial - they grant the contract permission to access these encrypted values in future transactions.
### Bidding Function
The `bid()` function handles incoming bids:
```solidity theme={null}
function bid(uint64 amount) external {
require(!auctionClosed, "Auction is closed");
// 1. Encrypt the bid amount
euint64 eAmount = FHE.asEuint64(amount);
// 2. Check if this bid is higher (encrypted comparison)
ebool isHigher = FHE.gt(eAmount, highestBid);
// 3. Update highest bid using max
highestBid = FHE.max(eAmount, highestBid);
// 4. Update highest bidder using select
highestBidder = FHE.select(
isHigher,
FHE.asEaddress(msg.sender),
highestBidder
);
// 5. Grant contract access to new encrypted values
FHE.allowThis(highestBid);
FHE.allowThis(highestBidder);
emit BidPlaced(msg.sender);
}
```
Notice how the contract never reveals the current highest bid to bidders. All comparisons and updates happen on encrypted data, maintaining complete confidentiality throughout the bidding process.
### Closing the Auction
The auctioneer closes the auction and allows public decryption of the winning bid and bidder:
```solidity theme={null}
function closeBidding() external onlyAuctioneer {
require(!auctionClosed, "Auction is already closed");
// Allow anyone to request decryption of the results
FHE.allowPublic(highestBid);
FHE.allowPublic(highestBidder);
auctionClosed = true;
emit AuctionClosed();
}
```
Since `FHE.allowPublic` is used, anyone can request decryption offchain without needing an ACP. The values are not revealed until someone submits the proof onchain.
### Revealing the Winner
The `revealWinner` function accepts the decrypted values and their Teecryptor signatures, then publishes them onchain:
```solidity theme={null}
function revealWinner(
euint64 bidCtHash,
uint64 bidPlaintext,
bytes calldata bidSignature,
eaddress bidderCtHash,
address bidderPlaintext,
bytes calldata bidderSignature
) external {
require(auctionClosed, "Auction isn't closed");
// Verify and publish both decrypted results
FHE.publishDecryptResult(bidCtHash, bidPlaintext, bidSignature);
FHE.publishDecryptResult(bidderCtHash, bidderPlaintext, bidderSignature);
winningBid = bidPlaintext;
winningBidder = bidderPlaintext;
emit RevealedWinningBid(bidderPlaintext, bidPlaintext);
}
```
`FHE.publishDecryptResult` verifies the Teecryptor signature before accepting the plaintext. If the signature is invalid, the transaction reverts.
***
## Usage Flow
### 1. Deploy the Contract
```typescript theme={null}
const auction = await AuctionExample.deploy();
await auction.waitForDeployment();
```
### 2. Place Bids
```typescript theme={null}
await auction.connect(bidder1).bid(1000);
await auction.connect(bidder2).bid(1500);
await auction.connect(bidder3).bid(1200);
```
### 3. Close the Auction
```typescript theme={null}
await auction.connect(auctioneer).closeBidding();
```
### 4. Decrypt Offchain and Reveal the Winner
```typescript theme={null}
// Read the encrypted handles from the contract
const bidCtHash = await auction.highestBid();
const bidderCtHash = await auction.highestBidder();
// Request decryption offchain (no ACP needed since allowPublic was used)
const bidResult = await client
.decryptForTx(bidCtHash)
.withoutACP()
.execute();
const bidderResult = await client
.decryptForTx(bidderCtHash)
.withoutACP()
.execute();
// Submit the proofs onchain to reveal the winner
await auction.revealWinner(
bidResult.ctHash,
bidResult.decryptedValue,
bidResult.signature,
bidderResult.ctHash,
bidderResult.decryptedValue,
bidderResult.signature
);
// Check the results
const winner = await auction.winningBidder();
const amount = await auction.winningBid();
console.log(`Winner: ${winner}, Bid: ${amount}`);
```
***
## Key Takeaways
Bids remain completely encrypted during the auction. No one can see the current highest bid or react to other bids.
The contract uses `FHE.gt()`, `FHE.max()`, and `FHE.select()` to update the highest bid without decrypting values.
Every encrypted value created must have permissions granted via `FHE.allowThis()` for the contract to access it later. Use `FHE.allowPublic()` when values are ready to be revealed.
Decryption happens offchain via `decryptForTx`, and results are verified onchain via `FHE.publishDecryptResult` with a Teecryptor signature.
***
## Related Examples
* Learn about conditional logic in [Conditions](/cofhe/fhe-library/core-concepts/conditions)
* Understand decryption methods in [Decryption Operations](/cofhe/fhe-library/core-concepts/decryption-operations)
* Explore access control in [Access Control](/cofhe/fhe-library/core-concepts/access-control)
# Best Practices
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/introduction/best-practices
Key best practices for developing secure and efficient FHE smart contracts with CoFHE
## Overview
This guide outlines key best practices for developing with CoFHE, based on recommendations from our development team. Following these practices will help you build secure, efficient, and maintainable FHE-enabled smart contracts.
## Prerequisites
Before reading this guide, you should:
* Have completed the [Quick Start](/cofhe/fhe-library/introduction/quick-start) setup
* Understand basic FHE concepts and encrypted types
* Be familiar with Solidity smart contract development
## Security Considerations
### Publish Decrypted Data Carefully
Decryption is a multi-step process: the client requests a plaintext + signature offchain via `decryptForTx`, then publishes or verifies the result onchain using `FHE.publishDecryptResult` or `FHE.verifyDecryptResult`. Once published, the plaintext is visible to everyone on the blockchain.
**Key principles:**
* **Evaluate information leakage**: Before publishing a decrypted value onchain, consider what information you're exposing and what an observer might learn from it
* **Minimize published values**: Only publish decrypted results when your protocol truly requires the plaintext onchain. Use `decryptForView` if you only need to display the value in a UI
* **Use `verifyDecryptResult` when possible**: If your contract only needs to confirm a value without storing it publicly, prefer `FHE.verifyDecryptResult` over `FHE.publishDecryptResult`
Publishing a decrypted value onchain makes it permanently visible to all observers. Always consider whether you need `decryptForTx` (onchain proof) or `decryptForView` (UI-only) for your use case.
### Always update permissions
Remember to call `FHE.allowThis()` after modifying any encrypted value that needs to be accessed later:
```solidity theme={null}
counter = FHE.add(counter, FHE.asEuint32(1));
FHE.allowThis(counter);
```
Without calling `FHE.allowThis()`, your contract won't be able to access the encrypted value in subsequent operations. This is a common source of errors in FHE development.
### Avoid code branching based on encrypted data
**Remember: there is no secure code branching with FHE.** Decrypting to make branching decisions is generally a bad practice and can leak information.
**Best practices:**
* **Use constant-time algorithms**: Design your code to follow the same execution path regardless of encrypted values
* **Prefer FHE.select over conditional logic**: Use built-in selection operations rather than decrypting for if/else decisions
**Example:**
```solidity Don't Do This theme={null}
// Don't reveal encrypted values to make branching decisions
FHE.publishDecryptResult(condition, plaintext, signature);
if (plaintext > 0) {
result = a;
} else {
result = b;
}
```
```solidity Recommended Approach theme={null}
// Use FHE.select instead
result = FHE.select(condition, a, b);
```
Since conditional branching doesn't work with encrypted values, always use `FHE.select()` for conditional logic. This ensures your code follows a constant execution path regardless of the encrypted values.
## Performance Optimization
### Optimize computational efficiency
FHE operations are computationally expensive. Optimize your contracts to minimize overhead:
**Key strategies:**
* **Minimize FHE operations**: Each operation adds computational overhead, so reduce the number of operations where possible
* **Use the minimum bit-width necessary**: Choose the smallest integer type that can safely represent your data
```solidity Less efficient theme={null}
euint64 counter; // Using 64 bits when 32 would suffice
```
```solidity More efficient theme={null}
euint32 counter; // Using 32 bits when that's sufficient
```
### Reuse Encrypted Constants
Encrypt constant values once and reuse them to save gas:
```solidity theme={null}
// Good practice: Encrypt once, reuse many times
euint32 ONE = FHE.asEuint32(1);
FHE.allowThis(ONE);
// Later in the code
counter = FHE.add(counter, ONE);
counter = FHE.add(counter, ONE); // Reusing the same encrypted constant
```
Reusing encrypted constants is a gas optimization technique. Encrypt frequently used values (like 0, 1, or common thresholds) once at contract initialization and reuse them throughout your contract's lifetime.
### Plan for Asynchronous Operations
CoFHE operations are asynchronous by nature. Design your application to handle this gracefully:
**UI considerations:**
* **Implement loading indicators**: Show spinners, progress bars, or status messages to inform users when operations are in progress
* **Use progress indicators**: Provide feedback during encryption and decryption operations
* **Consider state management**: Design your application to handle pending states gracefully
**Example UI pattern:**
Use `.onStep(callback)` to track encryption progress. The callback fires at the start and end of each step, receiving the current `EncryptStep` enum value and a context object with `isStart`, `isEnd`, and `duration` (milliseconds, only meaningful on `isEnd`).
```typescript theme={null}
import { createCofheClient, createCofheConfig } from '@cofhe/sdk/web';
import { Encryptable, EncryptStep } from '@cofhe/sdk';
import { chains } from '@cofhe/sdk/chains';
const [isEncrypting, setIsEncrypting] = useState(false);
const [currentStep, setCurrentStep] = useState(null);
const handleEncrypt = async () => {
setIsEncrypting(true);
try {
const result = await cofheClient
.encryptInputs([Encryptable.uint32(5n)])
.onStep((step, ctx) => {
if (ctx?.isStart) setCurrentStep(step);
if (ctx?.isEnd) console.log(`${step} done in ${ctx.duration}ms`);
})
.execute();
// Handle result
} finally {
setIsEncrypting(false);
setCurrentStep(null);
}
};
```
The `EncryptStep` enum values fire in order: `InitTfhe` to `FetchKeys` to `Pack` to `Prove` to `Verify`.
CoFHE operations may take time to complete, especially on testnets. Always provide user feedback during these operations to improve user experience.
## Development Workflow
### Start with Mock Environment
Begin development using mock contracts for faster iteration:
* **Faster feedback loop**: Mock environment provides immediate results without network delays
* **Easier debugging**: Plaintext values are visible in mock contracts, making debugging simpler
* **No external dependencies**: Test locally without connecting to testnets
### Test Thoroughly
Write comprehensive tests covering:
* **Both environments**: Test in mock and testnet environments
* **Edge cases**: Handle zero values, maximum values, and boundary conditions
* **Error scenarios**: Test what happens when operations fail or permissions are missing
### Gas Optimization
Be aware of gas costs:
* **Mock environments simulate higher costs**: Gas costs in mock environments are higher than production
* **Test on testnet for accurate estimates**: Always test gas consumption on testnet before deployment
* **Optimize before deployment**: Review and optimize your contract's gas usage
## Summary
Following these best practices will help you:
* **Build secure contracts**: Proper permission management and avoiding information leakage
* **Optimize performance**: Minimize operations and reuse encrypted constants
* **Improve user experience**: Handle asynchronous operations gracefully
* **Develop efficiently**: Use mock environments for rapid iteration
By following these best practices, you'll create more secure, efficient, and maintainable FHE-enabled smart contracts.
## Next Steps
* Learn about [encrypted data types](/cofhe/fhe-library) and their use cases
* Explore [FHE operations](/cofhe/fhe-library) available in the library
* Review [access control mechanisms](/cofhe/fhe-library) for managing permissions
* Check out [common pitfalls](/cofhe/fhe-library) to avoid
# Overview
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/introduction/overview
A Solidity framework for secure, private computation on encrypted data within smart contracts
## Overview
The CoFHE library is a Solidity framework that enables secure, private computation on encrypted data within smart contracts. This library allows developers to perform operations on encrypted values without revealing the underlying plaintext data, preserving privacy while maintaining the transparency and trustlessness of blockchain technology.
***
## Core Components
The FHE library consists of the following key components:
### 1. Encrypted Data Types
The library supports multiple encrypted data types, each representing an encrypted version of a standard Solidity type:
| Type | Description | Plaintext Equivalent |
| - | - | - |
| `ebool` | Encrypted boolean value | `bool` |
| `euint8` | Encrypted 8-bit unsigned integer | `uint8` |
| `euint16` | Encrypted 16-bit unsigned integer | `uint16` |
| `euint32` | Encrypted 32-bit unsigned integer | `uint32` |
| `euint64` | Encrypted 64-bit unsigned integer | `uint64` |
| `euint128` | Encrypted 128-bit unsigned integer | `uint128` |
| `eaddress` | Encrypted Ethereum address | `address` |
### 2. Encrypted Input Structures
`ICofhe.sol` defines various input structures that enable secure data submission:
* **`EncryptedInput`**: The core structure containing:
* Ciphertext hash: A unique hash representing the encrypted data, used to reference it across the system.
* Security zone parameter: Defines the trust context or boundary in which the encrypted data is valid and accessible.
* Type indicator: Specifies the data type of the encrypted value (e.g. euint8, euint16) to ensure correct handling.
* Cryptographic signature: A signature proving that the data and its metadata were generated and verified by an authorized entity.
* **`UnsignedEncryptedInput`**: The same shape without the signature, used when a batch of inputs is verified together under one signature.
* **Type-specific input types:**
* `externalEuint8`, `externalEuint16`, `externalEuint32`
* `externalEuint64`, `externalEuint128`
* `externalEbool`, `externalEaddress`
Each is a `bytes32` handle rather than a struct. It arrives with a separate `bytes` proof, and `FHE.asEuintXX(handle, proof)` converts the pair.
* **Type-specific shared types:** `sharedEuint8` through `sharedEaddress`, for encrypted values handed from one contract to another. See [inputs](/cofhe/fhe-library/core-concepts/inputs).
***
### 3. Core Functionality (FHE.sol)
The `FHE` library provides a comprehensive set of operations for encrypted data manipulation:
#### 1. Arithmetic Operations
Enables basic math (FHE.add) directly on encrypted integers.
#### 2. Bitwise Operations
Supports bitwise logic (AND, OR, XOR, shifts) on encrypted data.
#### 3. Comparison Operations
Performs encrypted comparisons (eq, gt, lt, etc.) that return an eboolan encrypted boolean value that contains the result of the comparison.
#### 4. Control Flow
Includes conditionals like `select` to allowing encrypted branching without revealing decision paths.
#### 5. Data and Access Management
Provides functions for sealing outputs, decrypting values securely, and managing user access via ACPs, ensuring only authorized parties can access decrypted data.
***
### 4. Task Management
The library interacts with a `TaskManager` contract that coordinates:
* **Execution** of FHE operations
* **Access control** for encrypted data
* **Decryption request** processing
# Quick Start
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/introduction/quick-start
Set up your local development environment for building FHE smart contracts with CoFHE
## Overview
This guide helps you set up your local development environment for building FHE (Fully Homomorphic Encryption) smart contracts with CoFHE. The Fhenix development starter kit provides everything you need to develop, test, and deploy FHE contracts both locally and on test networks.
## Supported networks
CoFHE launches on two mainnet networks. Go-live is in progress: the contracts are deployed, and intake opens when it completes.
* **Ethereum**
* **Arbitrum One**
Until then, develop and deploy on the testnets:
* **Ethereum Sepolia** - Main Ethereum testnet for FHE development
* **Arbitrum Sepolia** - Layer 2 testnet with lower gas costs
* **Base Sepolia** - Coinbase's Layer 2 testnet
You can develop and test locally using mock contracts, then deploy to any of these testnets when ready.
The Fhenix development environment consists of several key components:
* **cofhe-hardhat-starter**: Template Hardhat project to clone and use as a starting point
* **cofhe-hardhat-plugin**: Hardhat plugin that deploys mock contracts and exposes utilities
* **@cofhe/sdk**: JavaScript library for interacting with FHE contracts and the CoFHE coprocessor
* **cofhe-mock-contracts**: Mock contracts that mimic CoFHE coprocessor behavior for local testing
* **cofhe-contracts**: Solidity libraries and smart contracts for FHE operations
## Prerequisites
Before starting, ensure you have:
* **Node.js** (v20 or later)
* **pnpm** (recommended package manager)
* Basic familiarity with Hardhat and Solidity
If you don't have pnpm installed, you can install it globally with `npm install -g pnpm`.
## Installation
Clone the `cofhe-hardhat-starter` repository:
```bash theme={null}
git clone https://github.com/fhenixprotocol/cofhe-hardhat-starter.git
cd cofhe-hardhat-starter
```
You should now have the starter project in your local directory.
Install all required dependencies using pnpm:
```bash theme={null}
pnpm install
```
All dependencies are installed and ready to use.
## Project Structure
The starter kit provides a well-organized directory structure to help you get started quickly:
* **`contracts/`**: Contains all your Solidity smart contract source files
* `Counter.sol`: An example FHE-enabled counter contract demonstrating basic FHE operations
* **`test/`**: Houses tests that utilize `@cofhe/sdk` and utilities to interact with FHE-enabled contracts
* **`tasks/`**: Tasks to deploy and interact with the Counter contract on Arbitrum Sepolia
* **`hardhat.config.ts`**: Imports the CoFHE Hardhat plugin to deploy mock contracts
## Local Development Workflow
### Writing FHE Smart Contracts
FHE contracts use special encrypted types and operations from the FHE library. Here's a basic example:
```solidity contracts/Counter.sol theme={null}
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
contract Counter {
euint32 public count; // Encrypted uint32
function increment() public {
count = FHE.add(count, FHE.asEuint32(1));
FHE.allowThis(count);
FHE.allowSender(count);
}
}
```
Key FHE concepts you'll use:
* `euint32`, `ebool` - Encrypted data types
* `FHE.add`, `FHE.sub` - FHE operations on encrypted values
* `FHE.allowThis`, `FHE.allowSender` - Access control management
### Testing with Mock Environment
For rapid development, use the mock environment to test your contracts:
```bash theme={null}
pnpm test
```
This runs your tests with mock FHE operations, allowing quick iteration without external dependencies.
```typescript test/Counter.test.ts theme={null}
it('Should increment the counter', async function () {
const { counter, bob } = await loadFixture(deployCounterFixture)
// Check initial value
const count = await counter.count()
await mock_expectPlaintext(bob.provider, count, 0n)
// Increment counter
await counter.connect(bob).increment()
// Check new value
const count2 = await counter.count()
await mock_expectPlaintext(bob.provider, count2, 1n)
})
```
```typescript Full Client SDK Flow theme={null}
it('Full Client SDK flow', async function () {
const [bob] = await hre.ethers.getSigners()
const CounterFactory = await hre.ethers.getContractFactory('Counter')
const counter = await CounterFactory.connect(bob).deploy()
// Create a connected client (batteries included)
const client = await hre.cofhe.createClientWithBatteries(bob)
// Encrypt a value
const [encryptedInput] = await client
.encryptInputs([Encryptable.uint32(5n)])
.onStep((step) => console.log(`Encrypt step - ${step}`))
.execute()
// Use the encrypted input to reset the counter
await counter.connect(bob).reset(encryptedInput)
// Fetch the hash of the encrypted counter value
const encryptedValue = await counter.count()
// Decrypt the value (view decryption)
const result = await client
.decryptForView(encryptedValue, FheTypes.Uint32)
.withACP()
.execute()
// Check the decrypted result
expect(result).equal(5n)
})
```
### Deploying to testnet
When ready for more realistic testing, deploy to a Sepolia testnet:
Create a `.env` file with your private key and RPC URLs:
```bash theme={null}
PRIVATE_KEY=your_private_key_here
SEPOLIA_RPC_URL=your_sepolia_rpc_url
ARBITRUM_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc_url
```
Never commit your `.env` file to version control. Add it to your `.gitignore` file.
Deploy your contract to the testnet:
```bash Ethereum Sepolia theme={null}
pnpm eth-sepolia:deploy-counter
```
```bash Arbitrum Sepolia theme={null}
pnpm arb-sepolia:deploy-counter
```
```bash Base Sepolia theme={null}
pnpm base-sepolia:deploy-counter
```
The plugin ships presets for the first two networks. Base Sepolia has none, so the starter defines that network in its own `hardhat.config.ts`; the [compatibility page](/cofhe/get-started/introduction/compatibility#supported-networks) shows the block to copy into your own project.
Use tasks to interact with your deployed contract:
```bash Ethereum Sepolia theme={null}
pnpm eth-sepolia:increment-counter
```
```bash Arbitrum Sepolia theme={null}
pnpm arb-sepolia:increment-counter
```
```bash Base Sepolia theme={null}
pnpm base-sepolia:increment-counter
```
## Key Components
### cofhe-hardhat-plugin
This plugin provides essential tools for developing FHE contracts:
* **Network Configuration**: Automatically configures supported networks
* **Testing Utilities**: Helpers for testing FHE contracts
* **Mock Integration**: Sets up mock contracts for local testing
### Client SDK (`@cofhe/sdk`)
The JavaScript library for working with FHE contracts:
* **Encrypt/Decrypt**: Encrypt data to send to contracts and decrypt results
* **Querying**: Fetch encrypted values from FHE contracts
* **Authentication**: Uses [Access Control Permissions](/cofhe/client-sdk/guides/acps) (ACPs) to authenticate connected users when requesting confidential data
### cofhe-mock-contracts
These contracts provide mock implementations for FHE functionality:
* Allows testing without actual FHE operations
* Simulates the behavior of the real FHE environment
* Stores plaintext values onchain for testing purposes
In the mock environment, gas costs are higher than in production due to the additional operations needed to simulate FHE behavior. This is especially noticeable when logging is enabled.
### cofhe-contracts
Package of Solidity libraries and smart contracts for FHE operations:
* **FHE.sol Library**: The only import you need to start using FHE functionality in your contracts
* Complete API reference for all available functions and types
## Development Environments
CoFHE supports multiple development environments:
### MOCK Environment
* Fastest development cycle
* No external dependencies
* Uses mock contracts to simulate FHE operations
### Sepolia testnet
* Public testnet for real FHE operations
* Requires ETH from the Sepolia faucet
* Available on Ethereum Sepolia, Arbitrum Sepolia, and Base Sepolia
Environment detection happens automatically in most cases. When testing on the Hardhat network with mocks deployed (e.g., in unit tests), `hre.cofhe.createClientWithBatteries` will detect the Hardhat network and configure the client for the mock environment. When using tasks that connect to networks like `arb-sepolia`, the environment will automatically be set to testnet mode. This environment setting is crucial as it determines how the SDK handles encryption and decryption operations.
## Creating Custom Tasks
You can create custom Hardhat tasks for your contracts in the `tasks/` directory:
```typescript tasks/increment-counter.ts theme={null}
task("increment-counter", "Increment the counter on the deployed contract")
.setAction(async (_, hre: HardhatRuntimeEnvironment) => {
const { ethers, network } = hre;
// Get the signer
const [signer] = await ethers.getSigners();
console.log(`Using account: ${signer.address}`);
await hre.cofhe.createClientWithBatteries(signer);
// Interact with your contract
// ...
})
```
## Development Guidelines
### Start with Mock Environment
Begin development using mock contracts for faster iteration and debugging. This approach eliminates external dependencies and provides immediate feedback.
### Test Thoroughly
Write comprehensive tests covering both mock and testnet environments. Ensure your application behaves consistently across environments and handles edge cases properly.
### Permission Management
Always set proper permissions with `FHE.allowThis()` and `FHE.allowSender()` to control which contracts and addresses can access encrypted data. Proper permission management is crucial for maintaining privacy and security in FHE applications.
### Error Handling
Handle errors from FHE operations explicitly. Be prepared for potential decryption delays and use appropriate retry mechanisms.
### Gas Optimization
Be aware that FHE operations cost more gas than standard operations. Mock environments will simulate higher gas consumption than actual production environments. For accurate gas estimation, always test on the testnet before deployment.
## Next Steps
Now that you have your development environment set up, you can:
* Explore the [FHE Library documentation](/cofhe/fhe-library) to learn about encrypted data types and operations
* Review the [Counter contract example](https://github.com/FhenixProtocol/cofhe-hardhat-starter/blob/main/contracts/Counter.sol) to see FHE in action
* Check out the [Client SDK documentation](/cofhe/client-sdk/introduction/overview) for client-side integration
* Learn about [access control mechanisms](/cofhe/fhe-library) for managing encrypted data permissions
## Resources
* [Fhenix documentation](/cofhe)
* [Client SDK GitHub](https://github.com/FhenixProtocol/cofhesdk)
* [CoFHE Contracts GitHub](https://github.com/FhenixProtocol/cofhe-contracts)
* [cofhe-hardhat-starter GitHub](https://github.com/fhenixprotocol/cofhe-hardhat-starter)
# CoFHE Errors Package
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/cofhe-errors
Decode 'execution reverted' selectors from CoFHE contracts into error names and signatures with @fhenixprotocol/cofhe-errors
## Overview
When working with Solidity smart contracts, you've probably encountered cryptic error messages like:
```text theme={null}
Error: execution reverted: 0x118cdaa7
Transaction reverted without a reason string
execution reverted
```
You only get a 4-byte error selector (`0x118cdaa7` here) with no readable message. The `@fhenixprotocol/cofhe-errors` package maps a selector back to the error name, signature, and source contract.
* 53 custom errors from 13 contracts, as of package version 1.0.2
* Complete error signatures with parameter types
* Source contract information
* CLI lookup tool
* JavaScript/TypeScript API
Version 1.0.2 of the package was published in January 2026 and predates the current contracts. Its database does not include errors added since, such as `NotOnAccessList`, `DecryptFunctionNotSupported`, `LengthMismatch`, `NotShared`, `UnexpectedSharer`, and the ACPShareRegistry errors. It still lists `OnlyAggregatorAllowed`, which the TaskManager no longer raises. When the CLI reports `Error not found for selector`, look the selector up on the [error reference](/cofhe/fhe-library/reference/cofhe-errors-reference), which is generated from the deployed contracts.
## Quick start
No installation is required. Use `npx` to decode an error:
```bash theme={null}
npx cofhe-errors 0x118cdaa7
```
**Output:**
```text theme={null}
Name: OwnableUnauthorizedAccount
Selector: 0x118cdaa7
Signature: OwnableUnauthorizedAccount(address)
Source: ACL
Inputs: address
```
## Installation (optional)
Only install if you need to import the error database programmatically in your code:
```bash npm theme={null}
npm install @fhenixprotocol/cofhe-errors
```
```bash yarn theme={null}
yarn add @fhenixprotocol/cofhe-errors
```
```bash pnpm theme={null}
pnpm add @fhenixprotocol/cofhe-errors
```
## CLI usage
### Decode an error selector
When you see "execution reverted: 0x...", decode it:
```bash theme={null}
npx cofhe-errors 0x118cdaa7
```
### Search errors by name
Find errors when you know part of the error name:
```bash theme={null}
npx cofhe-errors --name Permission
```
**Output:**
```text theme={null}
0x4c40eccb PermissionInvalid_IssuerSignature (ACL)
0x8e143bf7 PermissionInvalid_RecipientSignature (ACL)
0xcbd3a966 PermissionInvalid_Disabled (ACL)
0xed0764a1 PermissionInvalid_Expired (ACL)
```
### List all known errors
Browse the complete error database:
```bash theme={null}
npx cofhe-errors --list
```
### JSON output
Get JSON output for scripting and automation:
```bash theme={null}
npx cofhe-errors --json 0x118cdaa7
```
**Output:**
```json theme={null}
{
"name": "OwnableUnauthorizedAccount",
"selector": "0x118cdaa7",
"signature": "OwnableUnauthorizedAccount(address)",
"source": "ACL",
"inputs": ["account"],
"inputTypes": ["address"]
}
```
## Programmatic API
### JavaScript/TypeScript usage
```javascript theme={null}
const errors = require('@fhenixprotocol/cofhe-errors/errors.json');
// Find error by selector
const error = errors.find(e => e.selector === '0x118cdaa7');
console.log(`Error: ${error.name}`);
console.log(`Signature: ${error.signature}`);
// Search by name
const permissionErrors = errors.filter(e =>
e.name.toLowerCase().includes('permission')
);
// Get all errors from a specific contract
const aclErrors = errors.filter(e => e.source === 'ACL');
```
### TypeScript with types
```typescript theme={null}
import errors from '@fhenixprotocol/cofhe-errors/errors.json';
interface SolidityError {
name: string;
selector: string;
signature: string;
source: string;
inputs: string[];
inputTypes: string[];
}
const error = errors.find(e => e.selector === '0x118cdaa7') as SolidityError;
```
### Error monitoring example
```javascript theme={null}
const errors = require('@fhenixprotocol/cofhe-errors/errors.json');
// Decode error from blockchain event
function decodeError(errorData) {
const selector = errorData.slice(0, 10); // First 4 bytes
const error = errors.find(e => e.selector === selector);
if (error) {
console.log(`Error: ${error.name}`);
console.log(`Contract: ${error.source}`);
console.log(`Signature: ${error.signature}`);
}
}
```
## Error coverage
Package version 1.0.2 includes errors from the following contracts. The counts reflect the contracts at the time of that release, not the contracts deployed today.
| Contract | Error Count |
| - | - |
| TaskManager | 16 |
| ACL | 8 |
| SafeCast | 4 |
| ERC1967Utils | 4 |
| Errors | 4 |
| Ownable2StepUpgradeable | 4 |
| ECDSA | 3 |
| Strings | 3 |
| Common | 2 |
| UUPSUpgradeable | 2 |
| Address | 1 |
| FHE | 1 |
| PlaintextsStorage | 1 |
## How error selectors work
Solidity custom errors use a 4-byte selector computed as:
```text theme={null}
selector = keccak256("ErrorName(type1,type2,...)").slice(0, 4)
```
**Example:**
```solidity theme={null}
error OwnableUnauthorizedAccount(address account);
```
Becomes:
* Signature: `OwnableUnauthorizedAccount(address)`
* Selector: `keccak256("OwnableUnauthorizedAccount(address)") = 0x118cdaa7...`
* First 4 bytes: `0x118cdaa7`
## Next steps
* View the [error reference](/cofhe/fhe-library/reference/cofhe-errors-reference), generated from the deployed contracts
* Learn about [common errors](/cofhe/fhe-library/core-concepts/common-errors) and troubleshooting
* Review [best practices](/cofhe/fhe-library/introduction/best-practices) for FHE development
# Error Reference
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/cofhe-errors-reference
Every custom error the live CoFHE contracts and the FHE library can raise, with selector, signature, and meaning
This page lists every custom error the CoFHE host-chain contracts (TaskManager, ACL, PlaintextsStorage, and ACPShareRegistry) and `FHE.sol` can raise. Each entry gives the 4-byte selector that appears in an `execution reverted: 0x...` message, the full signature, and what triggers it.
The list is generated from the contract sources as deployed on the public testnets, checked against Sepolia on 2026-09-01, and from `@fhenixprotocol/cofhe-contracts` 0.2.0.
The `@fhenixprotocol/cofhe-errors` package (1.0.2) predates these contracts. It cannot decode errors added since, such as `NotOnAccessList` or `NotShared`, and it still lists `OnlyAggregatorAllowed`, which the TaskManager no longer raises. Treat this page as the source of truth and see the [package page](/cofhe/fhe-library/reference/cofhe-errors) for what it does cover.
## Errors by contract
### TaskManager
The TaskManager validates every FHE operation before queueing it. Most errors a contract hits during development come from here.
| Selector | Error | Meaning |
| - | - | - |
| `0x4d13139e` | `ACLNotAllowed(uint256,address)` | The caller is not allowed to use the handle. Grant access with `FHE.allow`, `FHE.allowThis`, or `FHE.allowSender` before the value is used in an operation. |
| `0xd8aba367` | `CofheIsUnavailable()` | An admin has disabled the coprocessor. Task creation and decryption result publishing revert until it is re-enabled. |
| `0x31a7e7ab` | `DecryptFunctionNotSupported()` | The `decrypt` function id was submitted as a task. Decryption is requested offchain through the SDK and verified onchain with `FHE.verifyDecryptResult`. |
| `0x70cf6554` | `DecryptionResultNotReady(uint256)` | `getDecryptResult` was called before the decryption result was published for this handle. Use `FHE.getDecryptResultSafe` to check readiness without reverting. |
| `0xe6c4247b` | `InvalidAddress()` | An admin setter or access list update received the zero address. |
| `0x91b4b378` | `InvalidInputForFunction(string,uint8)` | The operation does not accept this input type. Arithmetic and comparison functions reject `eaddress`, numeric functions also reject `ebool`, and `trivialEncrypt` rejects a plaintext value that does not fit the target type. |
| `0x884a0e9d` | `InvalidInputType(uint8,uint8)` | The condition passed to `select` is not an `ebool`. |
| `0x9a84351c` | `InvalidInputsAmount(string,uint256,uint256)` | The operation received the wrong number of encrypted inputs: one for unary operations, two for binary operations, three for `select`. |
| `0xb31612aa` | `InvalidOperationInputs(string)` | No encrypted inputs were given for an operation that needs them, or plaintext extra inputs were given for an operation other than `trivialEncrypt` or `cast`. |
| `0x24cbcf36` | `InvalidSecurityZone(int32,int32,int32)` | A handle, input, or requested zone falls outside the configured security zone range. The last two values are the allowed minimum and maximum. |
| `0x8baa579f` | `InvalidSignature()` | A decryption result or encrypted input batch carried a signature that does not recover to any signer. |
| `0x7ba5ffb5` | `InvalidSigner(address,address)` | The signature recovered to the first address, but the second address was expected: the Teecryptor for decryption results, the ZK Verifier for encrypted inputs. A common cause is an input encrypted for a different chain, contract, or account. |
| `0x52b50ae1` | `InvalidTypeOrSecurityZone(string)` | The two operands of a binary operation, or the two branches of `select`, differ in type or security zone. |
| `0xff633a38` | `LengthMismatch()` | The handle, result, and signature arrays passed to batch decryption result publishing or verification have different lengths. |
| `0xb688c6f5` | `NotOnAccessList(address)` | The access list is enabled and the calling contract is not on it. The list is disabled on the public testnets, so this error appears only on a network that has enabled it. |
| `0x98e08ab0` | `RandomFunctionNotSupported()` | The `random` function id was submitted as an ordinary task. Random values use their own entry point, reached through `FHE.randomEuint8` and the other `randomE*` functions. |
| `0x2b0399d5` | `TooManyInputs(string,uint256,uint256)` | The operation received more than three inputs in total, counting encrypted handles and plaintext extra inputs. |
| `0xcabe5ce4` | `UnsupportedType(uint256)` | The requested return type, cast target, or plaintext conversion target is not a valid encrypted type id. |
`InvalidBytesLength` and `OnlyOwnerAllowed` are declared in the TaskManager source but never raised.
### ACL
The ACL records which accounts may use each handle. The first six errors come from the ACL itself. The four `PermissionInvalid_*` errors come from the Access Control Permission (ACP) check that guards its view functions.
| Selector | Error | Meaning |
| - | - | - |
| `0xd1860468` | `AlreadyDelegated()` | The same delegation already exists for this delegator, delegatee, and delegatee contract. |
| `0x3809a243` | `DirectAllowForbidden(address)` | The ACL was called directly. Every ACL write goes through the TaskManager, which is what the `FHE.allow*` helpers do. |
| `0xa70a958e` | `NotShared(uint256,address)` | `receiveCtHash` found no share for this handle and receiver in the current transaction. The sharing contract must pass the value with `sharedEuintXX` in the same call. |
| `0xcbd3a966` | `PermissionInvalid_Disabled()` | The revoker contract named in the ACP reports it as revoked. |
| `0xed0764a1` | `PermissionInvalid_Expired()` | The Access Control Permission (ACP) has expired. Create a new one with a later expiration. |
| `0x4c40eccb` | `PermissionInvalid_IssuerSignature()` | The issuer signature on the ACP does not verify against the issuer address. |
| `0x8e143bf7` | `PermissionInvalid_RecipientSignature()` | The recipient signature on a sharing ACP does not verify against the recipient address. |
| `0x30dc9203` | `SenderCannotBeDelegateeAddress()` | `delegateAccount` was given the TaskManager itself as the delegatee contract. |
| `0xd0d25976` | `SenderNotAllowed(address)` | The account granting or sharing access does not itself have access to the handle. A contract can only allow handles it is allowed to use. |
| `0x3a194b4f` | `UnexpectedSharer(address,address)` | The share for this handle came from a different contract than the receiver expected. The first address is the expected sharer, the second the actual one. |
### ACPShareRegistry
The ACPShareRegistry holds sharing ACPs posted onchain for their recipient to pick up.
| Selector | Error | Meaning |
| - | - | - |
| `0x70a4b574` | `AlreadyShared()` | An identical share is already posted for this recipient. |
| `0x393d68fd` | `IssuerSignatureMissing()` | The ACP has no issuer signature. |
| `0x54ec5063` | `NotIssuer()` | `share` was called by an account other than the ACP issuer. |
| `0x5f3677bc` | `NotIssuerOrRecipient()` | `removeShare` was called by someone other than the share's issuer or recipient. |
| `0xc664e64a` | `RecipientMissing()` | The sharing ACP has no recipient. |
| `0xaea18555` | `SealingKeyMustBeEmpty()` | A sharing ACP posted onchain must not carry a sealing key. |
| `0xc98374b2` | `ShareExpired()` | The ACP had already expired when it was posted. |
| `0x3d03aecf` | `UnknownShare()` | `removeShare` was given a share id that does not exist. |
### PlaintextsStorage
PlaintextsStorage holds published decryption results. Only the TaskManager writes to it.
| Selector | Error | Meaning |
| - | - | - |
| `0xdce3ec0a` | `OnlyTaskManagerAllowed(address)` | A write to PlaintextsStorage came from an address other than the TaskManager. |
### FHE library
These errors are raised inside your own contract by `FHE.sol` and `ICofhe.sol`, before any call reaches the TaskManager.
| Selector | Error | Meaning |
| - | - | - |
| `0x67cf3071` | `InvalidEncryptedInput(uint8,uint8)` | The encrypted input's type (first value) does not match the conversion you called (second value), for example an `externalEuint64` passed to `FHE.asEuint32`. |
| `0x8f568bf8` | `SecurityZoneOutOfBounds(int32)` | A negative security zone was passed to a plaintext conversion such as `FHE.asEuint32(value, securityZone)`. |
`InvalidHexCharacter(bytes1)` (`0x01d4fab6`) is declared in `FHE.sol` but never raised.
## Inherited OpenZeppelin errors
The host-chain contracts are UUPS proxies administered with OpenZeppelin roles, so their ABIs also carry these errors. They surface only on admin, upgrade, or signature-parsing paths. `NotLegacyOwner` is a CoFHE addition used once, during the migration from the previous owner-based admin.
| Selector | Error | Raised by | Meaning |
| - | - | - | - |
| `0x6697b232` | `AccessControlBadConfirmation()` | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `renounceRole` was called with an account other than the caller. |
| `0x19ca5ebb` | `AccessControlEnforcedDefaultAdminDelay(uint48)` | TaskManager, ACL, PlaintextsStorage | A default admin transfer was accepted before its delay elapsed. The value is the schedule. |
| `0x3fc3c27a` | `AccessControlEnforcedDefaultAdminRules()` | TaskManager, ACL, PlaintextsStorage | A default admin role change bypassed the two-step transfer rules. |
| `0xc22c8022` | `AccessControlInvalidDefaultAdmin(address)` | TaskManager, ACL, PlaintextsStorage | The default admin was set to the zero address. |
| `0xe2517d3f` | `AccessControlUnauthorizedAccount(address,bytes32)` | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | An admin function was called by an account without the required role. The second value is the role id. |
| `0x9996b315` | `AddressEmptyCode(address)` | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | A delegated call targeted an address with no code. |
| `0xf645eedf` | `ECDSAInvalidSignature()` | TaskManager | A signature recovered to the zero address. |
| `0xfce698f7` | `ECDSAInvalidSignatureLength(uint256)` | TaskManager | A signature is not 65 bytes long. |
| `0xd78bce0c` | `ECDSAInvalidSignatureS(bytes32)` | TaskManager | A signature has an `s` value in the upper half of the curve order. |
| `0x4c9c8ce3` | `ERC1967InvalidImplementation(address)` | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | The new implementation address has no code. |
| `0xb398979f` | `ERC1967NonPayable()` | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | An upgrade sent ETH without an initialization call. |
| `0xd6bda275` | `FailedCall()` | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | A low-level call made during an upgrade reverted without data. |
| `0xf92ee8a9` | `InvalidInitialization()` | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | An initializer ran outside an initialization phase, or ran twice. |
| `0xd7e6bcf8` | `NotInitializing()` | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | An `onlyInitializing` function was called outside an initializer. |
| `0x4953fb7f` | `NotLegacyOwner(address,address)` | TaskManager, ACL, PlaintextsStorage | The one-time migration from the previous owner-based admin to roles was called by an account other than the previous owner. |
| `0x6dfcc650` | `SafeCastOverflowedUintDowncast(uint8,uint256)` | TaskManager, ACL, PlaintextsStorage | A default admin delay did not fit in 48 bits. |
| `0xe07c8dba` | `UUPSUnauthorizedCallContext()` | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | An upgrade function was called on the implementation directly instead of through the proxy. |
| `0xaa1d49a4` | `UUPSUnsupportedProxiableUUID(bytes32)` | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | The new implementation does not report the ERC-1967 implementation slot. |
## All errors alphabetically
57 errors in total.
| Selector | Error | Contract | Signature |
| - | - | - | - |
| `0x6697b232` | AccessControlBadConfirmation | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `AccessControlBadConfirmation()` |
| `0x19ca5ebb` | AccessControlEnforcedDefaultAdminDelay | TaskManager, ACL, PlaintextsStorage | `AccessControlEnforcedDefaultAdminDelay(uint48)` |
| `0x3fc3c27a` | AccessControlEnforcedDefaultAdminRules | TaskManager, ACL, PlaintextsStorage | `AccessControlEnforcedDefaultAdminRules()` |
| `0xc22c8022` | AccessControlInvalidDefaultAdmin | TaskManager, ACL, PlaintextsStorage | `AccessControlInvalidDefaultAdmin(address)` |
| `0xe2517d3f` | AccessControlUnauthorizedAccount | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `AccessControlUnauthorizedAccount(address,bytes32)` |
| `0x4d13139e` | ACLNotAllowed | TaskManager | `ACLNotAllowed(uint256,address)` |
| `0x9996b315` | AddressEmptyCode | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `AddressEmptyCode(address)` |
| `0xd1860468` | AlreadyDelegated | ACL | `AlreadyDelegated()` |
| `0x70a4b574` | AlreadyShared | ACPShareRegistry | `AlreadyShared()` |
| `0xd8aba367` | CofheIsUnavailable | TaskManager | `CofheIsUnavailable()` |
| `0x31a7e7ab` | DecryptFunctionNotSupported | TaskManager | `DecryptFunctionNotSupported()` |
| `0x70cf6554` | DecryptionResultNotReady | TaskManager | `DecryptionResultNotReady(uint256)` |
| `0x3809a243` | DirectAllowForbidden | ACL | `DirectAllowForbidden(address)` |
| `0xf645eedf` | ECDSAInvalidSignature | TaskManager | `ECDSAInvalidSignature()` |
| `0xfce698f7` | ECDSAInvalidSignatureLength | TaskManager | `ECDSAInvalidSignatureLength(uint256)` |
| `0xd78bce0c` | ECDSAInvalidSignatureS | TaskManager | `ECDSAInvalidSignatureS(bytes32)` |
| `0x4c9c8ce3` | ERC1967InvalidImplementation | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `ERC1967InvalidImplementation(address)` |
| `0xb398979f` | ERC1967NonPayable | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `ERC1967NonPayable()` |
| `0xd6bda275` | FailedCall | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `FailedCall()` |
| `0xe6c4247b` | InvalidAddress | TaskManager | `InvalidAddress()` |
| `0x67cf3071` | InvalidEncryptedInput | FHE library | `InvalidEncryptedInput(uint8,uint8)` |
| `0xf92ee8a9` | InvalidInitialization | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `InvalidInitialization()` |
| `0x91b4b378` | InvalidInputForFunction | TaskManager | `InvalidInputForFunction(string,uint8)` |
| `0x9a84351c` | InvalidInputsAmount | TaskManager | `InvalidInputsAmount(string,uint256,uint256)` |
| `0x884a0e9d` | InvalidInputType | TaskManager | `InvalidInputType(uint8,uint8)` |
| `0xb31612aa` | InvalidOperationInputs | TaskManager | `InvalidOperationInputs(string)` |
| `0x24cbcf36` | InvalidSecurityZone | TaskManager | `InvalidSecurityZone(int32,int32,int32)` |
| `0x8baa579f` | InvalidSignature | TaskManager | `InvalidSignature()` |
| `0x7ba5ffb5` | InvalidSigner | TaskManager | `InvalidSigner(address,address)` |
| `0x52b50ae1` | InvalidTypeOrSecurityZone | TaskManager | `InvalidTypeOrSecurityZone(string)` |
| `0x393d68fd` | IssuerSignatureMissing | ACPShareRegistry | `IssuerSignatureMissing()` |
| `0xff633a38` | LengthMismatch | TaskManager | `LengthMismatch()` |
| `0xd7e6bcf8` | NotInitializing | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `NotInitializing()` |
| `0x54ec5063` | NotIssuer | ACPShareRegistry | `NotIssuer()` |
| `0x5f3677bc` | NotIssuerOrRecipient | ACPShareRegistry | `NotIssuerOrRecipient()` |
| `0x4953fb7f` | NotLegacyOwner | TaskManager, ACL, PlaintextsStorage | `NotLegacyOwner(address,address)` |
| `0xb688c6f5` | NotOnAccessList | TaskManager | `NotOnAccessList(address)` |
| `0xa70a958e` | NotShared | ACL | `NotShared(uint256,address)` |
| `0xdce3ec0a` | OnlyTaskManagerAllowed | PlaintextsStorage | `OnlyTaskManagerAllowed(address)` |
| `0xcbd3a966` | PermissionInvalid\_Disabled | ACL | `PermissionInvalid_Disabled()` |
| `0xed0764a1` | PermissionInvalid\_Expired | ACL | `PermissionInvalid_Expired()` |
| `0x4c40eccb` | PermissionInvalid\_IssuerSignature | ACL | `PermissionInvalid_IssuerSignature()` |
| `0x8e143bf7` | PermissionInvalid\_RecipientSignature | ACL | `PermissionInvalid_RecipientSignature()` |
| `0x98e08ab0` | RandomFunctionNotSupported | TaskManager | `RandomFunctionNotSupported()` |
| `0xc664e64a` | RecipientMissing | ACPShareRegistry | `RecipientMissing()` |
| `0x6dfcc650` | SafeCastOverflowedUintDowncast | TaskManager, ACL, PlaintextsStorage | `SafeCastOverflowedUintDowncast(uint8,uint256)` |
| `0xaea18555` | SealingKeyMustBeEmpty | ACPShareRegistry | `SealingKeyMustBeEmpty()` |
| `0x8f568bf8` | SecurityZoneOutOfBounds | FHE library | `SecurityZoneOutOfBounds(int32)` |
| `0x30dc9203` | SenderCannotBeDelegateeAddress | ACL | `SenderCannotBeDelegateeAddress()` |
| `0xd0d25976` | SenderNotAllowed | ACL | `SenderNotAllowed(address)` |
| `0xc98374b2` | ShareExpired | ACPShareRegistry | `ShareExpired()` |
| `0x2b0399d5` | TooManyInputs | TaskManager | `TooManyInputs(string,uint256,uint256)` |
| `0x3a194b4f` | UnexpectedSharer | ACL | `UnexpectedSharer(address,address)` |
| `0x3d03aecf` | UnknownShare | ACPShareRegistry | `UnknownShare()` |
| `0xcabe5ce4` | UnsupportedType | TaskManager | `UnsupportedType(uint256)` |
| `0xe07c8dba` | UUPSUnauthorizedCallContext | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `UUPSUnauthorizedCallContext()` |
| `0xaa1d49a4` | UUPSUnsupportedProxiableUUID | TaskManager, ACL, PlaintextsStorage, ACPShareRegistry | `UUPSUnsupportedProxiableUUID(bytes32)` |
# Access Control
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/fhe-sol/access-control
allow, allowPublic, allowThis, allowSender, allowTransient, isAllowed, isPubliclyAllowed: grant and check access to encrypted values
All access control functions work on `ebool | euint8 | euint16 | euint32 | euint64 | euint128 | eaddress`.
***
## allow
Grants permission to a specific address.
```solidity theme={null}
FHE.allow(encryptedValue, userAddress);
```
## allowThis
Grants permission to the current contract (`address(this)`).
Call `allowThis` after modifying encrypted state variables so the contract can use them in future transactions.
```solidity theme={null}
euint32 counter = FHE.add(counter, FHE.asEuint32(1));
FHE.allowThis(counter);
```
## allowPublic
Grants public permission. Anyone can request decryption offchain via `decryptForTx` without an ACP.
Once called, the value can be decrypted by anyone. Only use this when you intend to reveal the value publicly (e.g., after an auction closes or when unwrapping tokens).
```solidity theme={null}
FHE.allowPublic(highestBid);
```
## allowSender
Grants permission to `msg.sender`.
```solidity theme={null}
FHE.allowSender(encryptedValue);
```
## allowTransient
Grants temporary permission to a specific address for the current transaction only.
```solidity theme={null}
FHE.allowTransient(encryptedValue, otherContract);
```
***
## isAllowed
Checks if an address has permission.
```solidity theme={null}
bool hasAccess = FHE.isAllowed(encryptedValue, userAddress);
```
## isPubliclyAllowed
Checks if the value has been granted public access via `allowPublic`.
```solidity theme={null}
bool isPublic = FHE.isPubliclyAllowed(encryptedValue);
```
# Arithmetic Operations
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/fhe-sol/arithmetic
add, sub, mul, div, rem, and square: encrypted math operations
All arithmetic operations work on `euint8 | euint16 | euint32 | euint64 | euint128`. Both operands must be the same type.
## add
```solidity theme={null}
euint32 sum = FHE.add(a, b);
```
## sub
```solidity theme={null}
euint32 diff = FHE.sub(a, b);
```
## mul
```solidity theme={null}
euint32 product = FHE.mul(a, b);
```
## div
Division by zero does not revert. The FHE Engine cannot inspect the encrypted divisor, so `div` returns the maximum value of the operand type instead (255 for `euint8`). If that result is wrong for your contract, test the divisor with `eq` and pick a fallback with `select`.
```solidity theme={null}
euint32 quotient = FHE.div(a, b);
```
## rem
Remainder by zero does not revert. `rem` returns the dividend unchanged, so `63 % 0` yields `63`. The mock contracts behind the Hardhat and Foundry plugins differ here: they revert with a Solidity division panic. A local test of `rem` by zero therefore fails while the same call succeeds on a live network.
```solidity theme={null}
euint32 remainder = FHE.rem(a, b);
```
## square
```solidity theme={null}
euint32 squared = FHE.square(a);
```
# Bindings (Dot Notation)
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/fhe-sol/bindings
Use encrypted types with dot notation instead of FHE.* prefix
The FHE library provides binding libraries that enable dot notation for all operations. Import them alongside FHE.sol, with no extra setup required.
```solidity theme={null}
// Without bindings
euint8 sum = FHE.add(a, b);
// With bindings
euint8 sum = a.add(b);
```
## Full Example
```solidity theme={null}
// Two inputs share one batch signature, so convert them together.
// Calling asEuint8(hash, signature) per value would verify each hash
// against a signature that covers both, and revert.
externalEuint8[] memory inputs = new externalEuint8[](2);
inputs[0] = encryptedInputA;
inputs[1] = encryptedInputB;
euint8[] memory values = FHE.asEuint8s(inputs, inputProof);
euint8 a = values[0];
euint8 b = values[1];
// Arithmetic
euint8 sum = a.add(b);
euint8 diff = a.sub(b);
euint8 product = a.mul(b);
euint8 quotient = a.div(b);
euint8 remainder = a.rem(b);
euint8 squared = a.square();
// Bitwise
euint8 bitwiseAnd = a.and(b);
euint8 bitwiseOr = a.or(b);
euint8 bitwiseXor = a.xor(b);
euint8 bitwiseNot = a.not();
euint8 shiftLeft = a.shl(b);
euint8 shiftRight = a.shr(b);
euint8 rotateLeft = a.rol(b);
euint8 rotateRight = a.ror(b);
// Comparison
ebool isEqual = a.eq(b);
ebool isNotEqual = a.ne(b);
ebool isLessThan = a.lt(b);
ebool isLessEqual = a.lte(b);
ebool isGreaterThan = a.gt(b);
ebool isGreaterEqual = a.gte(b);
// Min/Max
euint8 minimum = a.min(b);
euint8 maximum = a.max(b);
// Type conversion
ebool converted = a.toBool();
euint16 toU16 = a.toU16();
euint32 toU32 = a.toU32();
euint64 toU64 = a.toU64();
euint128 toU128 = a.toU128();
// Utility
bool initialized = a.isInitialized();
bytes32 handle = a.unwrap();
// Access control
a.allow(address);
a.allowThis();
a.allowPublic();
a.allowSender();
a.allowTransient(address);
bool hasAccess = a.isAllowed(address);
```
# Bitwise Operations
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/fhe-sol/bitwise
and, or, xor, not, shl, shr, rol, ror: encrypted bitwise operations
Bitwise logic operations work on `ebool | euint8 | euint16 | euint32 | euint64 | euint128`. Shift and rotate operations work on `euint8 | euint16 | euint32 | euint64 | euint128` only.
## Logic
### and
```solidity theme={null}
euint8 result = FHE.and(a, b);
ebool result = FHE.and(x, y);
```
### or
```solidity theme={null}
euint8 result = FHE.or(a, b);
ebool result = FHE.or(x, y);
```
### xor
```solidity theme={null}
euint8 result = FHE.xor(a, b);
ebool result = FHE.xor(x, y);
```
### not
```solidity theme={null}
euint8 result = FHE.not(a);
ebool result = FHE.not(x);
```
## Shift & Rotate
### shl
Shift left.
```solidity theme={null}
euint32 result = FHE.shl(value, shiftAmount);
```
### shr
Shift right.
```solidity theme={null}
euint32 result = FHE.shr(value, shiftAmount);
```
### rol
Rotate left.
```solidity theme={null}
euint32 result = FHE.rol(value, rotateAmount);
```
### ror
Rotate right.
```solidity theme={null}
euint32 result = FHE.ror(value, rotateAmount);
```
# Comparison Operations
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/fhe-sol/comparison
eq, ne, lt, lte, gt, gte, min, max: encrypted comparison and selection
All comparison operations return `ebool`. Equality checks (`eq`, `ne`) work on all encrypted types including `eaddress`. Ordering checks (`lt`, `lte`, `gt`, `gte`) and `min`/`max` work on `euint8 | euint16 | euint32 | euint64 | euint128`.
## Equality
### eq
```solidity theme={null}
ebool isEqual = FHE.eq(a, b);
ebool isEqual = FHE.eq(address1, address2); // eaddress supported
```
### ne
```solidity theme={null}
ebool isNotEqual = FHE.ne(a, b);
```
## Ordering
### lt
```solidity theme={null}
ebool isLess = FHE.lt(a, b);
```
### lte
```solidity theme={null}
ebool isLessOrEqual = FHE.lte(a, b);
```
### gt
```solidity theme={null}
ebool isGreater = FHE.gt(a, b);
```
### gte
```solidity theme={null}
ebool isGreaterOrEqual = FHE.gte(a, b);
```
## Min / Max
### min
```solidity theme={null}
euint32 minimum = FHE.min(a, b);
```
### max
```solidity theme={null}
euint32 maximum = FHE.max(a, b);
```
## Select
Conditionally selects between two encrypted values based on an encrypted boolean. Works on all encrypted types including `eaddress`.
Use `select` instead of `if/else` statements when working with encrypted values. Conditional branching doesn't work with encrypted data.
```solidity theme={null}
euint8 result = FHE.select(condition, a, b);
eaddress result = FHE.select(condition, addr1, addr2);
```
# Decryption
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/fhe-sol/decryption
publishDecryptResult, verifyDecryptResult, getDecryptResult: onchain decryption result handling
Decryption in CoFHE is a two-phase process:
1. **Onchain**: Mark a value as decryptable with `FHE.allowPublic(ctHash)`
2. **Offchain**: Client calls `decryptForTx(ctHash)` via the SDK to get `{ plaintext, signature }`
3. **Onchain**: Submit the result via `publishDecryptResult` or `verifyDecryptResult`
***
## Publishing Results
### publishDecryptResult
Publishes a decrypted result onchain by verifying the signature from [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor). The plaintext is stored and can be read via `getDecryptResultSafe`.
Ciphertext handle
Decrypted value from `decryptForTx`
Teecryptor signature returned by `decryptForTx`
Reverts if the signature is invalid. The value must have been granted public access via `allowPublic` before decryption was requested offchain.
```solidity theme={null}
FHE.publishDecryptResult(encryptedBid, bidPlaintext, bidSignature);
```
### publishDecryptResultBatch
Publishes multiple results in a single call. Typed overloads exist for every encrypted type:
| Handle array type | Result array type |
| - | - |
| `ebool[]` | `bool[]` |
| `euint8[]` | `uint8[]` |
| `euint16[]` | `uint16[]` |
| `euint32[]` | `uint32[]` |
| `euint64[]` | `uint64[]` |
| `euint128[]` | `uint128[]` |
| `eaddress[]` | `address[]` |
```solidity theme={null}
euint64[] memory handles = new euint64[](2);
handles[0] = encryptedBid1;
handles[1] = encryptedBid2;
uint64[] memory values = new uint64[](2);
values[0] = bid1Plaintext;
values[1] = bid2Plaintext;
bytes[] memory sigs = new bytes[](2);
sigs[0] = sig1;
sigs[1] = sig2;
FHE.publishDecryptResultBatch(handles, values, sigs);
```
The compiler resolves the overload from the handle array's element type. There is also a "raw" overload that takes `uint256[]` ctHashes if you only have raw handles to hand.
***
## Verifying Results
### verifyDecryptResult
Verifies a Teecryptor signature **without** storing the plaintext onchain. Returns `bool`.
Use this when you only need to act on the decrypted value within the transaction without making it permanently public.
```solidity theme={null}
require(
FHE.verifyDecryptResult(encryptedAmount, amount, signature),
"Invalid decrypt proof"
);
```
### verifyDecryptResultSafe
Like `verifyDecryptResult`, but returns `false` instead of reverting on invalid signature.
```solidity theme={null}
bool valid = FHE.verifyDecryptResultSafe(encryptedAmount, amount, signature);
if (valid) {
// proceed
}
```
### verifyDecryptResultBatch
Verifies multiple signatures in a single call. Returns `true` only if every entry is valid; reverts if any signature fails to recover (consistent with the single-entry `verifyDecryptResult`). Typed overloads exist for every encrypted type, same handle/result type table as [`publishDecryptResultBatch`](#publishdecryptresultbatch).
```solidity theme={null}
bool allValid = FHE.verifyDecryptResultBatch(handles, values, sigs);
```
### verifyDecryptResultBatchSafe
Returns a `bool[]` indicating which entries are valid instead of reverting. Typed overloads cover every encrypted type, same handle/result type table as [`publishDecryptResultBatch`](#publishdecryptresultbatch).
```solidity theme={null}
bool[] memory results = FHE.verifyDecryptResultBatchSafe(handles, values, sigs);
```
The batch verify functions (`verifyDecryptResultBatch`, `verifyDecryptResultBatchSafe`) were added in `cofhe-contracts@v0.1.2`. Earlier versions only exposed the single-entry `verifyDecryptResult` / `verifyDecryptResultSafe`.
***
## Reading Results
### getDecryptResult
Retrieves a published decryption result. Reverts if not yet available.
```solidity theme={null}
uint256 result = FHE.getDecryptResult(ctHash);
```
### getDecryptResultSafe
Non-reverting version. Returns a tuple with the result and a boolean flag.
```solidity theme={null}
(uint256 result, bool decrypted) = FHE.getDecryptResultSafe(ctHash);
if (decrypted) {
// use result
}
```
# FHE.sol Overview
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/fhe-sol/overview
Encrypted data types, imports, and general usage of the FHE Solidity library
The FHE library provides functions for working with Fully Homomorphic Encryption (FHE) in Solidity smart contracts. It enables computations on encrypted data without decrypting it, ensuring privacy throughout your contract's execution.
## Import
```solidity theme={null}
import '@fhenixprotocol/cofhe-contracts/FHE.sol';
```
All functions are prefixed with `FHE.` when called. For example: `FHE.add(a, b)` or `FHE.allowPublic(value)`.
## Encrypted Data Types
| Type | Description |
| - | - |
| `ebool` | Encrypted boolean value |
| `euint8` | Encrypted 8-bit unsigned integer |
| `euint16` | Encrypted 16-bit unsigned integer |
| `euint32` | Encrypted 32-bit unsigned integer |
| `euint64` | Encrypted 64-bit unsigned integer |
| `euint128` | Encrypted 128-bit unsigned integer |
| `eaddress` | Encrypted Ethereum address |
## Input Types
Each encrypted type has a corresponding input type, used when receiving encrypted data from the client SDK. An input arrives as a handle plus a `bytes` proof, and `FHE.asEuintXX(handle, proof)` converts the pair into the encrypted type:
| Input Type | Encrypted Type |
| - | - |
| `externalEbool` | `ebool` |
| `externalEuint8` | `euint8` |
| `externalEuint16` | `euint16` |
| `externalEuint32` | `euint32` |
| `externalEuint64` | `euint64` |
| `externalEuint128` | `euint128` |
| `externalEaddress` | `eaddress` |
## Shared Types
A value handed over by another contract arrives as a shared type instead. `FHE.shareEuintXX(value, receiver)` produces one, and the receiver unwraps it with `FHE.receiveEuintXXParam` or `FHE.receiveEuintXXFromCall`:
| Shared Type | Encrypted Type |
| - | - |
| `sharedEbool` | `ebool` |
| `sharedEuint8` | `euint8` |
| `sharedEuint16` | `euint16` |
| `sharedEuint32` | `euint32` |
| `sharedEuint64` | `euint64` |
| `sharedEuint128` | `euint128` |
| `sharedEaddress` | `eaddress` |
See [inputs](/cofhe/fhe-library/core-concepts/inputs) for when to use each.
## Security Considerations
1. **Initialization**: All FHE functions check if their inputs are initialized and set them to 0 if not.
2. **Decryption**: Decryption is a two-phase process. Mark values with `allowPublic` onchain, decrypt offchain via the Client SDK, then publish/verify the result with `publishDecryptResult` or `verifyDecryptResult`. Only reveal values when absolutely necessary.
3. **Security Zones**: Some functions accept a `securityZone` parameter to isolate different encrypted computations. FHE operations can only be performed between ciphertexts that share the same security zone.
4. **Access Control**: The library provides fine-grained access control through the `allow*` functions. Always set proper permissions before accessing encrypted values.
5. **Type Safety**: Ensure encrypted values use compatible types when performing operations. Type mismatches will cause errors.
# Type Conversion
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/fhe-sol/type-conversion
Convert between plaintext, encrypted input structs, and encrypted types
## From Plaintext (Trivial Encryption)
Convert plaintext values to encrypted types. An optional `securityZone` parameter isolates encrypted computations.
### asEbool
```solidity theme={null}
ebool encrypted = FHE.asEbool(true);
ebool encryptedWithZone = FHE.asEbool(true, 1);
```
### asEuint8 / asEuint16 / asEuint32 / asEuint64 / asEuint128
```solidity theme={null}
euint8 encrypted = FHE.asEuint8(42);
euint16 encrypted = FHE.asEuint16(1000);
euint32 encrypted = FHE.asEuint32(50000);
euint64 encrypted = FHE.asEuint64(1000000000);
euint128 encrypted = FHE.asEuint128(1000000000000000000);
// With security zone
euint32 encrypted = FHE.asEuint32(50000, 1);
```
### asEaddress
```solidity theme={null}
eaddress encrypted = FHE.asEaddress(0x1234567890123456789012345678901234567890);
```
***
## From encrypted inputs
Convert client-side encrypted inputs (received as function parameters) into encrypted types. Each input arrives as an `externalE*` handle plus a `bytes` proof that authenticates it.
```solidity theme={null}
function deposit(externalEuint64 encryptedAmount, bytes calldata inputProof) external {
euint64 amount = FHE.asEuint64(encryptedAmount, inputProof);
}
```
All input types follow the same pattern:
```solidity theme={null}
ebool val = FHE.asEbool(externalEboolValue, inputProof);
euint8 val = FHE.asEuint8(externalEuint8Value, inputProof);
euint16 val = FHE.asEuint16(externalEuint16Value, inputProof);
euint32 val = FHE.asEuint32(externalEuint32Value, inputProof);
euint64 val = FHE.asEuint64(externalEuint64Value, inputProof);
euint128 val = FHE.asEuint128(externalEuint128Value, inputProof);
eaddress val = FHE.asEaddress(externalEaddressValue, inputProof);
```
One proof covers every encrypted input in a call. When a function receives more than one, convert them together with the plural form (`FHE.asEuint32s(handles, inputProof)`) instead of calling the single form per value.
***
## Between encrypted types
Cast between `ebool`, the `euint` types, and `eaddress`. Every direction is supported except into `eaddress`: there is no cast from another encrypted type into an address. Create an `eaddress` from a plaintext address or an encrypted input instead.
```solidity theme={null}
// ebool from integers
ebool result = FHE.asEbool(euint8Value);
ebool result = FHE.asEbool(euint16Value);
ebool result = FHE.asEbool(euint32Value);
ebool result = FHE.asEbool(euint64Value);
ebool result = FHE.asEbool(euint128Value);
ebool result = FHE.asEbool(eaddressValue);
// euint8 from other types
euint8 result = FHE.asEuint8(eboolValue);
euint8 result = FHE.asEuint8(euint16Value);
euint8 result = FHE.asEuint8(euint32Value);
euint8 result = FHE.asEuint8(euint64Value);
euint8 result = FHE.asEuint8(euint128Value);
euint8 result = FHE.asEuint8(eaddressValue);
// euint16 from other types
euint16 result = FHE.asEuint16(eboolValue);
euint16 result = FHE.asEuint16(euint8Value);
euint16 result = FHE.asEuint16(euint32Value);
euint16 result = FHE.asEuint16(euint64Value);
euint16 result = FHE.asEuint16(euint128Value);
euint16 result = FHE.asEuint16(eaddressValue);
// euint32 from other types
euint32 result = FHE.asEuint32(eboolValue);
euint32 result = FHE.asEuint32(euint8Value);
euint32 result = FHE.asEuint32(euint16Value);
euint32 result = FHE.asEuint32(euint64Value);
euint32 result = FHE.asEuint32(euint128Value);
euint32 result = FHE.asEuint32(eaddressValue);
// euint64 from other types
euint64 result = FHE.asEuint64(eboolValue);
euint64 result = FHE.asEuint64(euint8Value);
euint64 result = FHE.asEuint64(euint16Value);
euint64 result = FHE.asEuint64(euint32Value);
euint64 result = FHE.asEuint64(euint128Value);
euint64 result = FHE.asEuint64(eaddressValue);
// euint128 from other types
euint128 result = FHE.asEuint128(eboolValue);
euint128 result = FHE.asEuint128(euint8Value);
euint128 result = FHE.asEuint128(euint16Value);
euint128 result = FHE.asEuint128(euint32Value);
euint128 result = FHE.asEuint128(euint64Value);
euint128 result = FHE.asEuint128(eaddressValue);
```
# Utility Functions
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/fhe-sol/utility
isInitialized, unwrap, wrap: handle inspection and conversion utilities
## isInitialized
Checks whether an encrypted value has been initialized (i.e., is not a zero/empty handle). Works on all encrypted types.
```solidity theme={null}
if (FHE.isInitialized(encryptedBalance)) {
// safe to use
}
```
## unwrap
Extracts the raw `bytes32` handle from a typed encrypted value.
```solidity theme={null}
bytes32 handle = FHE.unwrap(encryptedValue);
```
## wrap
Wraps a raw `bytes32` handle into a typed encrypted value. One function per type:
```solidity theme={null}
ebool val = FHE.wrapEbool(handle);
euint8 val = FHE.wrapEuint8(handle);
euint16 val = FHE.wrapEuint16(handle);
euint32 val = FHE.wrapEuint32(handle);
euint64 val = FHE.wrapEuint64(handle);
euint128 val = FHE.wrapEuint128(handle);
eaddress val = FHE.wrapEaddress(handle);
```
Use `wrap` when you have a raw handle from storage or an event and need to convert it back to a typed encrypted value.
**Renamed in `cofhe-contracts@v0.1.3`.** These functions used to be exposed as `FHE.asEbool(bytes32)`, `FHE.asEuint*(bytes32)`, and `FHE.asEaddress(bytes32)`. They were renamed to `wrap*` to remove a Solidity overload ambiguity with `asEuint*(0)` (where the integer literal `0` could resolve to either the plaintext-trivial-encryption overload or the raw-handle overload). If you're upgrading from `<=0.1.2`, search for `FHE.asE*()` call sites and rewrite them as `FHE.wrap*`.
## Random Number Generation
Generate random encrypted values. An optional `securityZone` parameter is supported.
```solidity theme={null}
euint8 rand = FHE.randomEuint8();
euint16 rand = FHE.randomEuint16();
euint32 rand = FHE.randomEuint32();
euint64 rand = FHE.randomEuint64();
euint128 rand = FHE.randomEuint128();
// With security zone
euint32 rand = FHE.randomEuint32(1);
```
# Gas per Operation
Source: https://cofhe-docs.fhenix.zone/cofhe/fhe-library/reference/gas-costs
The expected gas of every FHE.sol operation
Every value is measured on the deployed contracts and rounded to the nearest 100 gas.
**The values are for the first FHE call in a transaction. Every later FHE call in the same transaction costs about 20,000 gas less.**
`X` stands for the encrypted type: `asEuint32`, `asEbool`, `shareEuint64`, and so on. The seven encrypted types are `ebool`, `euint8` through `euint128`, and `eaddress`; "All" means all seven. The type barely changes the price, so each value is the highest across the supported types.
## Arithmetic
| Operation | Gas | Types |
| - | - | - |
| `add` | 51,600 | `euint8` to `euint128` |
| `sub` | 51,500 | `euint8` to `euint128` |
| `mul` | 52,600 | `euint8` to `euint128` |
| `div` | 52,300 | `euint8` to `euint128` |
| `rem` | 52,500 | `euint8` to `euint128` |
| `square` | 48,900 | `euint8` to `euint128` |
| `min` | 53,600 | `euint8` to `euint128` |
| `max` | 53,800 | `euint8` to `euint128` |
## Comparison
| Operation | Gas | Types |
| - | - | - |
| `eq` | 52,700 | All |
| `ne` | 53,000 | All |
| `lt` | 53,000 | `euint8` to `euint128` |
| `lte` | 52,800 | `euint8` to `euint128` |
| `gt` | 53,300 | `euint8` to `euint128` |
| `gte` | 52,800 | `euint8` to `euint128` |
## Bitwise and shift
| Operation | Gas | Types |
| - | - | - |
| `and` | 51,700 | `ebool`, `euint8` to `euint128` |
| `or` | 51,900 | `ebool`, `euint8` to `euint128` |
| `xor` | 51,500 | `ebool`, `euint8` to `euint128` |
| `not` | 45,700 | `ebool`, `euint8` to `euint128` |
| `shl` | 52,800 | `euint8` to `euint128` |
| `shr` | 52,900 | `euint8` to `euint128` |
| `rol` | 54,500 | `euint8` to `euint128` |
| `ror` | 54,600 | `euint8` to `euint128` |
## Select
| Operation | Gas | Types |
| - | - | - |
| `select` | 55,900 | All |
## Encrypt: create a ciphertext
| Operation | Gas | Types |
| - | - | - |
| [Trivial encrypt](/cofhe/fhe-library/core-concepts/trivial-encryption) `asX(plaintext)` | 45,400 | All |
| Encrypted input `asX(InEuintX)` | 39,800 | All |
| Encrypted input from bytes `asX(bytes)` | 40,400 | All |
| Random `randomX()` | 40,900 | `euint8` to `euint128` |
## Cast between encrypted types
| Operation | Gas | Types |
| - | - | - |
| Cast `asX(eY)`, target not `ebool` | 45,900 | Any to `euint8` to `euint128` |
| Cast to `ebool`, `asEbool(eY)` | 75,800 | Numeric types and `eaddress` to `ebool` |
A cast to `ebool` costs more because `FHE.sol` implements it as `ne(value, asX(0))`: two TaskManager tasks instead of one.
## Access control
All access-control operations support all seven types and cost the same for each. The two `is*` functions are `view`; they cost gas only when a contract calls them inside a transaction.
| Operation | Gas |
| - | - |
| `allow` | 44,700 |
| `allowThis` | 26,300 |
| `allowSender` | 44,700 |
| allowTransient | 23,000 |
| `allowGlobal` | 44,100 |
| `allowPublic` | 44,100 |
| `isAllowed` | 22,200 |
| isPubliclyAllowed | 21,900 |
## Sharing
| Operation | Gas |
| - | - |
| `shareX` | 25,900 |
| receiveXParam | 6,300 |
| receiveXFromCall | 6,300 |
## Decryption results
The four `verify*` and `get*` functions are `view`; they cost gas only when a contract calls them inside a transaction.
| Operation | Gas |
| - | - |
| publishDecryptResult | 71,400 |
| verifyDecryptResult | 15,100 |
| verifyDecryptResultSafe | 15,700 |
| getDecryptResult | 24,300 |
| getDecryptResultSafe | 23,200 |
# Fhenix Toolkit for Claude Code
Source: https://cofhe-docs.fhenix.zone/cofhe/get-started/build-with-ai/ai-assistant
Install the fhenix-toolkit plugin so Claude Code writes, tests, and reviews CoFHE code against the current FHE.sol and @cofhe/sdk
[fhenix-toolkit](https://github.com/FhenixProtocol/fhenix-toolkit) is a Claude Code plugin maintained by Fhenix. It teaches the assistant the rules that make confidential contracts work. Those include `FHE.select` instead of branching on an `ebool`, the access control calls a stored value needs, which decryption flow fits a feature, and how to test against the mocks. Use it when you build with `FHE.sol` or `@cofhe/sdk` in Claude Code.
The plugin carries concepts, decision trees, and a catalog of known mistakes. Function signatures, SDK methods, error codes, and deployed addresses are looked up live from the public Fhenix repositories when they are needed. The guidance therefore tracks new releases instead of freezing at one API version.
## Install
In a Claude Code terminal session:
```text theme={null}
/plugin marketplace add FhenixProtocol/fhenix-toolkit
/plugin install fhenix-toolkit
```
In the Claude Code desktop app, open Customize, then Personal Plugins, add the marketplace `FhenixProtocol/fhenix-toolkit`, sync, and add the Fhenix Toolkit plugin.
Each skill activates on its own when the context matches, so there is nothing to invoke by hand. Try:
* "Help me write a confidential ERC-20."
* "Audit this `FHE.sol` contract."
* "Write Foundry tests for this encrypted auction."
When the plugin is active, Claude refuses to write `if (FHE.gt(a, b))` and routes the comparison through `FHE.select`. That is the quickest way to confirm it loaded.
## What the plugin does
| Skill | Activates on | What it brings |
| - | - | - |
| `fhenix-contracts` | Solidity files that import `FHE.sol` or use `euint*`, `ebool`, or `eaddress` | The hard rules (`FHE.select`, the allow cascade, no encrypted `mul` or `div` where a shift will do), the access control verbs, the decryption flow choices, and a picker for confidential token standards |
| `fhenix-sdk` | Files that import `@cofhe/sdk` or use the `useCofhe*` hooks | The client initialization recipe, `decryptForView` versus `decryptForTx`, the Access Control Permission (ACP) lifecycle, and the encrypted input cast |
| `fhenix-review` | Pull request reviews and "is this safe" questions | A catalog of more than thirty known mistakes, a security checklist, and an audit lens for access control and decryption flows |
| `fhenix-tests` | Test files that import `FHE.sol` or `@cofhe/sdk` | Foundry mocks versus the Hardhat plugin, encrypted input fixtures, decryption flow tests, and the places where mocks and production diverge |
The `fhe-reviewer` subagent backs `fhenix-review` for large diffs and pre-launch audits. It loads the full catalog into a fresh context, walks the code function by function, and returns a prioritized report.
## Using other assistants
The skills are Markdown files under `plugins/fhenix-toolkit/skills/` in the repository. If you work in Cursor, ChatGPT, or another assistant, load the `SKILL.md` for the area you are working in as context, along with its `references/` folder. Every page on this site also carries a menu to open it in ChatGPT, Claude, or Cursor. That is the fastest way to give an assistant the current documentation for one topic.
For prompting techniques that work with any assistant, see [build with AI](/cofhe/get-started/build-with-ai/build-with-ai).
## Status and feedback
The plugin is public and pre-1.0. Coverage gaps are tracked in the repository's `docs/known-flaws.md`. Report problems or suggest additions by opening an issue on the [fhenix-toolkit repository](https://github.com/FhenixProtocol/fhenix-toolkit/issues).
# Overview
Source: https://cofhe-docs.fhenix.zone/cofhe/get-started/build-with-ai/build-with-ai
Learn how to use AI coding assistants effectively when building with Fhenix
This guide helps developers leverage AI tools effectively when building with Fhenix. Whether you're using Cursor, GitHub Copilot, or other AI assistants, these strategies will help you get better results and integrate AI smoothly into your FHE development process.
## Understanding Context Windows
### Why Context Matters
AI coding assistants have what's called a "context window" - the amount of text they can "see" and consider when generating responses. Think of it as the AI's working memory:
* Most modern AI assistants can process thousands of tokens (roughly 4-5 words per token)
* Everything you share and everything the AI responds with consumes this limited space
* Once the context window fills up, parts of your conversational history may be lost
This is why providing relevant context upfront is crucial - the AI can only work with what it can "see" in its current context window.
### Optimizing for Context Windows
To get the most out of AI assistants when building with Fhenix:
* **Prioritize relevant information**: Focus on sharing the most important details about your FHE use case first
* **Remove unnecessary content**: Avoid pasting irrelevant code or documentation
* **Structure your requests**: Use clear sections and formatting to make information easy to process
* **Reference Fhenix docs**: Share specific documentation links or code snippets relevant to your task
* **Create a project summary**: For larger projects, maintain a central documentation file that summarizes key FHE patterns and encryption strategies
## Setting Up AI Tools
### Configuring Cursor Rules
Cursor Rules allow you to provide consistent context to Cursor AI, making it more effective at understanding your Fhenix codebase and providing relevant suggestions.
### Creating Cursor Rules
1. Open the Command Palette in Cursor:
* Mac: `Cmd + Shift + P`
* Windows/Linux: `Ctrl + Shift + P`
2. Search for "Cursor Rules" and select the option to create or edit rules
3. Add project-specific rules that help Cursor understand your Fhenix project:
* Specify that you're using Fhenix for confidential smart contracts
* Include your preferred Solidity version and patterns
* Note any specific FHE operations you'll be using
4. Save your rules file and Cursor will apply these rules to its AI suggestions
## Creating Project Documentation
A comprehensive instructions file helps AI tools understand your Fhenix project better. This should be created early in your project and updated regularly.
**Ready-to-Use Prompt for Creating Instructions.md:**
```text theme={null}
Create a detailed instructions.md file for my Fhenix project with the following sections:
1. Overview: Summarize the project goals, problem statements, and core FHE functionality
2. Tech Stack: List all technologies, libraries, frameworks with versions (including Fhenix SDK version)
3. Project Structure: Document the file organization with explanations
4. FHE Patterns: Document encryption/decryption patterns and FHE operations used
5. Coding Standards: Document style conventions, linting rules, and patterns
6. User Stories: Key functionality from the user perspective
7. APIs and Integrations: External services and how they connect
```
## Effective Prompting Strategies
### Be Specific and Direct
Start with clear commands and be specific about what you want. AI tools respond best to clear, direct instructions.
**Example:**
Weak: "Help me with my Fhenix code"
Better: "Create a confidential voting smart contract using Fhenix FHE that encrypts votes and allows tallying without revealing individual votes"
### Provide Context for Complex Tasks
**Ready-to-Use Prompt:**
```text theme={null}
I'm working on a Fhenix project for [your use case]. I need your help with:
1. Problem: [describe specific FHE/encryption issue]
2. Current approach: [explain what you've tried]
3. Constraints: [mention any technical limitations or requirements]
4. Expected outcome: [describe what success looks like]
Here's the relevant Fhenix documentation: [link or paste relevant docs]
```
### Ask for Iterations
Start simple and refine through iterations rather than trying to get everything perfect in one go.
**Ready-to-Use Prompt:**
```text theme={null}
Let's approach this FHE implementation step by step:
1. First, implement a basic version of [feature] with minimal encryption functionality
2. Then, we'll review and identify areas for improvement
3. Next, let's add error handling and edge cases
4. Finally, we'll optimize for gas costs and performance
Please start with step 1 now.
```
## Working with Fhenix
### Leveraging Fhenix Documentation
When building with Fhenix, it's important to provide AI assistants with the right context about FHE operations and patterns.
**Example FHE Implementation Prompt:**
```text theme={null}
I'm implementing a confidential token transfer using Fhenix. Here's what I need:
1. Encrypt token amounts before transfer
2. Allow transfers between encrypted balances
3. Prevent front-running by keeping amounts private
4. Include proper error handling for encryption failures
Based on Fhenix documentation, please show me:
- How to encrypt amounts using the Fhenix SDK
- How to perform encrypted operations
- How to handle decryption for authorized users
```
### Component Integration Example
**Ready-to-Use Prompt for Confidential Balance Display:**
```text theme={null}
I need to implement a new feature in my Fhenix project that:
1. Shows the connected wallet's encrypted balance
2. Allows users to decrypt their own balance
3. Updates when the balance changes
4. Handles loading and error states appropriately
5. Follows our project's coding standards
Please update our instructions.md to reflect this new implementation.
```
## Debugging with AI
### Effective Debugging Prompts
**Ready-to-Use Prompt for Bug Analysis:**
```text theme={null}
I'm encountering an issue with my FHE implementation:
1. Expected behavior: [what should happen]
2. Actual behavior: [what's happening instead]
3. Error messages: [include any errors]
4. Relevant code: [paste the problematic code]
Please analyze this situation step by step and help me:
1. Identify potential causes of this issue (especially related to encryption/decryption)
2. Suggest debugging steps to isolate the problem
3. Propose possible solutions
```
**Ready-to-Use Prompt for Adding Debug Logs:**
```text theme={null}
I need to debug the following FHE function. Please add comprehensive logging statements that will help me trace:
1. Input values and their types (encrypted vs plaintext)
2. Function execution flow
3. Intermediate state changes
4. Output values or errors
5. Encryption/decryption operations
Here's my code:
[paste your code]
```
### When You're Stuck
If you're uncertain how to proceed:
**Ready-to-Use Clarification Prompt:**
```text theme={null}
I'm unsure how to proceed with [specific FHE task]. Here's what I know:
1. [context about the problem]
2. [what you've tried]
3. [specific areas where you need guidance about FHE]
What additional information would help you provide better assistance?
```
## Advanced Prompting Techniques
Modern AI assistants have capabilities that you can leverage with these advanced techniques:
**1. Step-by-step reasoning:** Ask the AI to work through FHE problems systematically
```text theme={null}
Please analyze this FHE encryption code step by step and identify potential security issues or gas optimizations.
```
**2. Format specification:** Request specific formats for clarity
```text theme={null}
Please structure your response as a tutorial with:
- Code examples for encrypting and decrypting data
- Explanations of each FHE operation
- Best practices for gas optimization
```
**3. Length guidance:** Indicate whether you want brief or detailed responses
```text theme={null}
Please provide a concise explanation in 2-3 paragraphs about how FHE maintains privacy during computation.
```
**4. Clarify ambiguities:** Help resolve unclear points when you receive multiple options
```text theme={null}
I notice you suggested two approaches for encryption. To clarify, I'd prefer to use the first approach with Solidity native types.
```
## Best Practices Summary
* **Understand context limitations**: Recognize that AI tools have finite context windows and prioritize information accordingly
* **Provide relevant context**: Share Fhenix-specific code snippets, encryption patterns, and project details that matter for your specific question
* **Be specific in requests**: Clear, direct instructions about FHE operations yield better results than vague questions
* **Break complex tasks into steps**: Iterative approaches often work better for complex FHE implementations
* **Request explanations**: Ask the AI to explain generated code or FHE concepts you don't understand
* **Use formatting for clarity**: Structure your prompts with clear sections and formatting
* **Reference Fhenix documentation**: When working with FHE, share relevant documentation links or examples
* **Test and validate**: Always review and test AI-generated FHE code before implementing in production
* **Build on previous context**: Refer to earlier parts of your conversation when iterating on FHE implementations
* **Provide feedback**: Let the AI know what worked and what didn't to improve future responses
# Compatibility
Source: https://cofhe-docs.fhenix.zone/cofhe/get-started/introduction/compatibility
Which package versions, toolchains, and networks work together
Use this page to pick versions before you install anything. It lists the current version of every CoFHE package, the toolchain each one requires, and the networks the SDK ships chain definitions for.
SDK versions `0.7` and later are not compatible with `0.6` contracts or `0.6` plugins. Upgrading from `0.6` is a breaking change that touches both your Solidity and your client code. See [upgrading from 0.6](#upgrading-from-0-6) before you start.
## Which versions to install
Every `@cofhe/*` package is released from the same monorepo at the same version, and each one pins the others exactly. Keep them all on the same version. Mixing them is the most common cause of a broken install.
| Package | Version | What it does |
| - | - | - |
| `@cofhe/sdk` | [`0.8.0`](https://github.com/FhenixProtocol/cofhesdk/releases/tag/@cofhe/sdk@0.8.0) | Client library for encrypting inputs, decrypting results, and signing ACPs |
| `@cofhe/react` | `0.8.0` | React hooks built on the SDK |
| `@cofhe/abi` | `0.8.0` | ABI definitions for the CoFHE contracts |
| `@cofhe/hardhat-plugin` | `0.8.0` | Mock deployment and test helpers for Hardhat 2 |
| `@cofhe/hardhat-3-plugin` | `0.8.0` | Mock deployment and test helpers for Hardhat 3 |
| `@cofhe/foundry-plugin` | `0.8.0` | Mock deployment and test helpers for Foundry |
| `@cofhe/mock-contracts` | `0.8.0` | Mock CoFHE contracts for local testing |
Solidity dependencies version separately from the SDK:
| Package | Version | What it does |
| - | - | - |
| `@fhenixprotocol/cofhe-contracts` | [`1.0.0`](https://github.com/FhenixProtocol/cofhe-contracts/tree/v1.0.0) | `FHE.sol` and the onchain CoFHE interfaces |
| `@fhenixprotocol/cofhe-errors` | `1.0.2` | Shared error definitions for the CoFHE contracts |
| `fhenix-confidential-contracts` | `0.4.0` | Confidential token primitives, including FHERC20 |
`fhenix-confidential-contracts` is published unscoped. There is no `@fhenixprotocol/fhenix-confidential-contracts` package, so installing that name fails.
## What each package requires
These are the peer dependencies declared by the published packages.
| Package | Required | Optional |
| - | - | - |
| `@cofhe/sdk` | `viem` ^2.38.6 | `ethers` ^5 or ^6, `@wagmi/core` ^2, `hardhat` ^2, `@nomicfoundation/hardhat-ethers` ^3 |
| `@cofhe/hardhat-plugin` | `hardhat` ^2, `@nomicfoundation/hardhat-ethers` ^3, `ethers` ^5 or ^6, `@fhenixprotocol/cofhe-contracts` >=1.0.0, `@openzeppelin/contracts` ^5 | None |
| `@cofhe/hardhat-3-plugin` | `hardhat` ^3 | None |
| `@cofhe/foundry-plugin` | `@fhenixprotocol/cofhe-contracts` 1.0.0, `@openzeppelin/contracts` 5.4.0 | None |
`viem` is the only client library the SDK always needs. The optional entries are adapters: install `ethers` or `@wagmi/core` only if you want to pass those objects to the SDK.
Pick your Hardhat plugin by the Hardhat version you already run: `@cofhe/hardhat-plugin` for Hardhat 2, `@cofhe/hardhat-3-plugin` for Hardhat 3. They are not interchangeable.
## Supported networks
The SDK ships chain definitions for these networks, exported from `@cofhe/sdk/chains`:
| Network | Chain ID | SDK export | Hardhat preset |
| - | - | - | - |
| Ethereum Sepolia | `11155111` | `sepolia` | `eth-sepolia` |
| Arbitrum Sepolia | `421614` | `arbSepolia` | `arb-sepolia` |
| Base Sepolia | `84532` | `baseSepolia` | None |
| Local CoFHE | `420105` | `localcofhe` | `localcofhe` |
| Hardhat (mock) | `31337` | `hardhat` | Built into Hardhat |
The SDK export name and the Hardhat preset name are different strings for the same network. The SDK calls Ethereum Sepolia `sepolia`; both Hardhat plugins register the preset as `eth-sepolia`. Use the SDK name in client code and the preset name in `hardhat.config.ts`.
Base Sepolia has no Hardhat preset in either plugin. To deploy there, add the network yourself:
```typescript hardhat.config.ts theme={null}
networks: {
'base-sepolia': {
url: process.env.BASE_SEPOLIA_RPC_URL ?? 'https://sepolia.base.org',
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 84532,
},
},
```
## Upgrading from 0.7
Move `@fhenixprotocol/cofhe-contracts` to `1.0.0` and every `@cofhe/*` package to `0.8.0` in the same change. The `0.8.0` plugins require contracts `1.0.0` or later, and contracts `1.0.0` needs the `0.8.0` mocks in tests, so the two upgrades only work together.
Two SDK changes can break your client code:
* `TASK_MANAGER_ADDRESS` is removed from `@cofhe/sdk`. Contracts `1.0.0` resolves the TaskManager through the CoFHEAddressBook, and the SDK follows: call `getTaskManagerAddress(publicClient)` instead.
* A custom `KeysStorage` implementation needs updating: the CRS is now keyed by chain and security zone, so `getCrs` and `setCrs` take the zone.
Contract code compiles unchanged against `1.0.0`: the `FHE.sol` API only gains functions. The `0.8.0` release changes nothing in ACP signing, so ACPs signed under `0.7` stay valid.
## Upgrading from 0.6
Upgrade to `0.6.1` first, then to `0.8.0`. The `0.7` release changes four things that will break a `0.6` project:
* Permits are renamed to ACPs. The `@cofhe/sdk/permits` entry point is now `@cofhe/sdk/acps`.
* The `InEuintXX` input types are gone, along with `FHE.asEuint32(InEuint32)` and `ITaskManager.verifyInput`.
* Signing moved from one signature per ciphertext to one signature per batch, which requires a `setConsumingContract()` call.
* Encrypted values passed between contracts now use the `sharedEuintXX` types instead of bare `euintXX`.
ACPs signed under `0.6` stop working. Your users have to sign again after you upgrade.
The [official migration guide](https://cofhesdk.fhenix.io/migrating-to-0-7-0) covers each change with before and after code. It also ships an agent skill that performs most of the mechanical edits for you.
## Troubleshooting
`0.7` signs one batch rather than one ciphertext, and the input types changed. A `0.6` client against `0.7` contracts fails verification. Upgrade both sides together.
## Next steps
* Follow the [quick start guide](/cofhe/fhe-library/introduction/quick-start) to set up a development environment.
* Read the [best practices](/cofhe/fhe-library/introduction/best-practices) for building with CoFHE.
* Browse the [`FHE.sol` reference](/cofhe/fhe-library/reference/fhe-sol/overview) for the contract API.
* Browse the [SDK reference](/cofhe/client-sdk/reference/sdk-reference), plus the [Hardhat](/cofhe/client-sdk/reference/hardhat-reference) and [Foundry](/cofhe/client-sdk/reference/foundry-reference) plugin references, for the client side.
# Fhenix
Source: https://cofhe-docs.fhenix.zone/cofhe/get-started/introduction/fhenix
The missing infrastructure for Confidential DeFi
## **Introduction**
Blockchains are great for transparency, security and trust, but that transparency comes at a cost: **everything is public**. Every transaction, smart contract interaction, and account balance is out in the open, which isn't ideal for things like finance, healthcare, or any use case that deal with sensitive data.
**Fully Homomorphic Encryption (FHE) fixes this.** Instead of exposing raw data onchain, FHE allows computations to happen **directly on encrypted data**. The blockchain never sees the actual inputs or outputs, only encrypted values, yet the results are still valid when decrypted by an authorized recipient.
This means smart contracts can run just like they do now, but with **built-in confidentiality**, without compromising decentralization or security.
***
## **The Blockchain Transparency Problem**
Blockchain is often praised for its **decentralization, immutability, and transparency**, but transparency is a double-edged sword.
### **Why Transparency is a Problem**
In public blockchains like Ethereum, every transaction, smart contract interaction, and account balance is **completely visible** to anyone. This radical transparency, while crucial for establishing trust and enabling verification, creates significant privacy challenges. FHE solves this fundamental tradeoff by allowing data to remain **fully encrypted** while still maintaining the network's ability to verify its accuracy and authenticity. This means sensitive information can be processed and validated without ever being exposed, combining the best of both worlds - **bulletproof privacy with trustless verification**.
**Real-world consequences of blockchain transparency:** \\
* **Front-running and MEV**: traders can analyze mempools and exploit pending transactions before they are executed. \\
* **Confidentiality leaks**: sensitive financial transactions, payroll information, or business logic are exposed. \\
* **Enterprise adoption hurdles**: companies are reluctant to use public blockchains if competitors can access proprietary data.
These challenges can all be mitigated by using FHE in your smart contracts.
***
## **What is FHE?**
**FHE** is a cryptographic technique that allows computations to be performed on encrypted data **without decrypting it**. Most cryptographic techniques secure data only until it needs to be used. FHE keeps it hidden even while processing, preventing leaks at every step.
### **How FHE Works**
1. A user encrypts their data into ciphertext.
2. The blockchain performs computations directly on the ciphertext.
3. The result remains encrypted and can only be decrypted by the authorized user.
**The Holy Grail of Cryptography**
* Data remains private throughout the entire computation process.
* Smart contracts can execute logic on encrypted inputs and return encrypted outputs.
* Users control their data without relying on trusted third parties.
# Support & Feedback
Source: https://cofhe-docs.fhenix.zone/cofhe/get-started/introduction/support-and-feedback
Get help, report issues, and provide feedback for the CoFHE project
## Overview
We believe in building together as a community. Our team is genuinely excited to hear from you - whether you've hit a roadblock, have brilliant ideas to share, or just want to chat about the future of FHE technology. No question is too small, no feedback too minor. Your experience matters to us, and your insights drive our innovation. Let's collaborate to make CoFHE the best it can be!
Your feedback helps us build better tools and documentation for everyone. We appreciate your contributions to the CoFHE community!
## Support Channels
### Documentation
If you find issues with our documentation, have suggestions for improvements, or want to request new content, visit our [Documentation Repository](https://github.com/FhenixProtocol/cofhe-docs/issues).
Create a new issue describing the problem or enhancement:
1. Navigate to the [Documentation Repository Issues](https://github.com/FhenixProtocol/cofhe-docs/issues)
2. Click "New Issue"
3. Use labels like "documentation", "bug", or "enhancement" to help us categorize your feedback
4. Provide detailed information about what needs to be addressed
If you'd like to contribute directly, we welcome pull requests:
1. Fork the repository
2. Make your changes
3. Submit a PR with a clear description of the improvements
We appreciate detailed reports that help us understand exactly what needs to be addressed. Include examples, screenshots, or code snippets when relevant.
### Community Support
Need technical assistance or general help? Our community channels are ready to support you.
#### Discord
Join our [Discord community](https://discord.gg/FuVgxrvJMY) where you'll find:
**Developers Corner:**
* `#dev-updates` - Stay informed about the latest development updates
* `#dev-general` - General development discussions
* `#verified-devs` - Channel for verified developers
* `#tech-questions` - Technical support and questions
**Support:**
* `#feedback-and-suggestions` - Share your ideas and feedback
* `#support-ticket` - Get dedicated support for specific issues
* `#spam-and-bug-reports` - Report bugs or suspicious activity
Connect with our active community of developers and team members ready to help you succeed with CoFHE!
#### Telegram
Connect with us on [Telegram](https://t.me/+OEO4CItQYh8xYzNh) for:
* **General** - General discussions
* **Dev Help** - Technical support and questions
Get quick answers to your questions and connect with the Fhenix community.
## How to Get Help
When seeking help, provide as much context as possible:
1. **Describe your issue clearly**: What are you trying to accomplish?
2. **Include error messages**: Copy the full error message or stack trace
3. **Share relevant code**: Include code snippets that demonstrate the problem
4. **Specify your environment**: Mention versions of CoFHE components you're using
5. **Explain what you've tried**: Let us know what troubleshooting steps you've already taken
The more information you provide, the faster we can help you resolve your issue. Don't hesitate to ask - we're here to help!
## Providing Feedback
Your feedback is invaluable to us. Whether you have:
* **Feature requests**: Ideas for new functionality
* **Documentation improvements**: Suggestions for clearer explanations
* **Bug reports**: Issues you've encountered
* **General feedback**: Thoughts on your experience with CoFHE
We want to hear from you! Use the appropriate channel (Discord, GitHub Issues, or Telegram) to share your thoughts.
Thank you for being part of our community! Your feedback helps us build better tools and documentation for everyone.
## Next Steps
* Join our [Discord community](https://discord.gg/FuVgxrvJMY) to connect with other developers
* Check the [Compatibility](/cofhe/get-started/introduction/compatibility) page if you're experiencing version-related issues
* Review [Common Errors](/cofhe/fhe-library/core-concepts/common-errors) for troubleshooting guidance
* Explore our [documentation](/cofhe/fhe-library/introduction/overview) to learn more about CoFHE
# What is CoFHE?
Source: https://cofhe-docs.fhenix.zone/cofhe/get-started/introduction/what-is-cofhe
A high-level introduction to CoFHE, Fhenix's Fully Homomorphic Encryption coprocessor
[FHE](/cofhe/get-started/introduction/fhenix) explains *why* confidential smart contracts matter. **CoFHE is how Fhenix makes them practical.**
**CoFHE is an FHE coprocessor that lets any blockchain run computations on encrypted data.**
This makes confidentiality another Solidity feature. There's no migration to a specialized FHE chain, no new toolchain, and no cryptography to implement yourself. CoFHE handles the heavy FHE math offchain; your app encrypts locally and your contract only ever touches lightweight *handles* to encrypted values, so code stays familiar. Values are encrypted. Everything else feels like ordinary development.
## Why a coprocessor?
The coprocessor model adds confidentiality without changing how applications are built, same Solidity, same chains, same tooling, with encrypted values as another type to work with.
* **No migration**: CoFHE attaches to existing blockchains. Confidential contracts deploy to the networks already in use, not a dedicated FHE L1.
* **Familiar code**: contracts pass around lightweight *handles* (references to ciphertexts) rather than the ciphertexts themselves, so they read like normal Solidity. FHE operations add some gas overhead, but the onchain footprint stays small and predictable.
* **No cryptography to implement**: the heavy FHE math runs offchain in the [compute pipeline](/cofhe/deep-dive/cofhe-components/compute-pipeline). A contract calls `FHE.add`; CoFHE does the rest.
* **Trust-minimized by default**: no machine holds the decryption key whole. Its shares sit with independent partners, and each partner releases its share only to a [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor) enclave that the TEE (Trusted Execution Environment) hardware has attested. Teecryptor reassembles the key in memory and never stores it.
## What CoFHE lets you build
Because computation happens on encrypted values, you can build applications where sensitive data never appears in plaintext onchain:
* **Confidential balances and transfers**: token amounts and balances stay hidden while transfers still settle correctly.
* **Private state in contracts**: per-user values (counters, scores, bids, positions) that no one, not even the contract or CoFHE, can read in the clear.
* **Sealed inputs**: users submit encrypted inputs (votes, bids, orders) that are computed on without ever being revealed.
* **Selective disclosure**: results are decrypted only for authorized parties, gated by signed ACPs.
## How it works
Every CoFHE application follows the same three-phase lifecycle: **encrypt to compute to decrypt.**
The [`@cofhe/sdk`](/cofhe/client-sdk/introduction/overview) encrypts the user's plaintext in the client. It attaches a zero-knowledge proof that the input is well-formed, then submits both to CoFHE. The blockchain only ever receives an encrypted handle, never the raw value.
The smart contract uses [`FHE.sol`](/cofhe/fhe-library/introduction/overview) to operate on encrypted handles, adding, comparing, selecting, as if they were ordinary numbers. Each operation deterministically derives a new result handle and is recorded onchain. The compute pipeline picks up that record, and the FHE Engine computes the matching ciphertext offchain. Nothing returns to the contract, and plaintext is never exposed at any point.
When an authorized user wants a result, they present a signed [Access Control Permission](/cofhe/client-sdk/guides/acps). Teecryptor checks that permission against the onchain access list, then decrypts inside its enclave. It either seals the value so only that user can read it (for display), or returns a signed plaintext a contract can verify (for onchain use).
Color marks the zone. Your app and the Client SDK run client side, your contract on the host chain, and CoFHE offchain. Hover any component or message in the [explorable version](/cofhe/get-started/introduction/what-is-cofhe-explorable).
## The pieces
Developers only interact directly with **two** parts of CoFHE; the rest runs behind the scenes.
### What you touch
| Component | Where | Role |
| - | - | - |
| **[`@cofhe/sdk`](/cofhe/client-sdk/introduction/overview)** | Client | Encrypt inputs, manage ACPs, decrypt outputs |
| **[`FHE.sol`](/cofhe/fhe-library/introduction/overview)** | Onchain | Solidity API for operating on encrypted handles |
### What runs behind the scenes
| Component | Role |
| - | - |
| **TaskManager** | Onchain gateway that validates FHE requests and enforces access control |
| **ZK Verifier** | Verifies the proof on every encrypted input, inside a hardware-attested TEE |
| **Compute pipeline** | Picks up onchain task events, validates and orders them, executes the FHE operations on encrypted data, and commits results |
| **Teecryptor** | Decrypts inside a hardware-attested TEE, after checking permissions and commitments |
| **CommitmentRegistry** | Records a commitment for every ciphertext, so integrity is verified before any decryption |
For a component-by-component breakdown, see the [CoFHE Architecture deep dive](/cofhe/deep-dive/cofhe-components/overview).
## How CoFHE keeps data safe
* **Encrypted end-to-end**: values are encrypted client-side and stay encrypted through computation; only handles touch the chain.
* **Verified inputs**: zero-knowledge proofs ensure every encrypted input is well-formed before it enters the system.
* **Verified results**: the coprocessor commits to each result onchain, and Teecryptor checks the ciphertext against that commitment before it decrypts anything.
* **Gated decryption**: nothing is decrypted without a signed ACP and a matching onchain grant. Teecryptor fails closed when either is missing.
## Next steps
Walk through encrypt to compute to decrypt with a concrete Counter example.
The TypeScript SDK for encrypting inputs and decrypting outputs.
The Solidity library for computing on encrypted data onchain.
Every CoFHE component and data flow, in detail.
# Overview
Source: https://cofhe-docs.fhenix.zone/cofhe/index
Welcome to Fhenix Documentation - Build privacy-preserving smart contracts with Fully Homomorphic Encryption
## Welcome to Fhenix
Fhenix enables developers to build privacy-preserving smart contracts using Fully Homomorphic Encryption (FHE). Create dApps where sensitive data remains encrypted throughout computation, unlocking new possibilities for confidential blockchain applications.
## Quick start
Get up and running with Fhenix in minutes.
Set up your development environment and build your first FHE-enabled smart contract.
Learn the fundamentals by building an encrypted counter contract.
## Key resources
JavaScript library for encrypting inputs, managing ACPs, and decrypting outputs.
Solidity library for performing operations on encrypted data in smart contracts.
Complete API documentation for all FHE library functions and operations.
Step-by-step guides for building FHE-enabled applications.
## Learn more
Check version compatibility and supported networks.
Security considerations and optimization techniques.
Get help and provide feedback to the team.
# ACL Usage Examples
Source: https://cofhe-docs.fhenix.zone/cofhe/tutorials/acl-usage-examples
Practical examples of using Access Control Lists (ACL) to manage permissions for encrypted data
## Overview
This tutorial provides practical examples of using Access Control Lists (ACL) to manage permissions for encrypted data in your CoFHE contracts.
See [ACL Mechanism](/cofhe/fhe-library/core-concepts/access-control) for explanation of why the ACL mechanism is needed.
## Solidity API
The following functions are available for managing access control:
1. `FHE.allowThis(CIPHERTEXT_HANDLE)` - allows the current contract access to the handle
2. `FHE.allow(CIPHERTEXT_HANDLE, ADDRESS)` - allows the specified address access to the handle
3. `FHE.allowTransient(CIPHERTEXT_HANDLE, ADDRESS)` - allows the specified address access to the handle for the duration of the transaction
## Automatic Transaction-Scoped Allowance
The contract that creates the value for the first time automatically gets ownership of the ciphertext **for the duration of the transaction**, through an internal `ACL.allowTransient(this)` call.
Every snippet on this page assumes this import:
```solidity theme={null}
import {FHE, euint32, externalEuint32} from "@fhenixprotocol/cofhe-contracts/FHE.sol";
```
```solidity theme={null}
// Contract A
function doAdd(externalEuint32 input1, externalEuint32 input2, bytes calldata inputProof) external {
// Both inputs share one batch signature, so convert them together
externalEuint32[] memory inputs = new externalEuint32[](2);
inputs[0] = input1;
inputs[1] = input2;
euint32[] memory handles = FHE.asEuint32s(inputs, inputProof); // Contract A gets temporary ownership of both
euint32 result = FHE.add(handles[0], handles[1]); // possible because Contract A has ownership of both
}
```
This automatic allowance only lasts for the duration of the current transaction. To use encrypted values in future transactions, you must explicitly grant access.
## Persistent Allowance for This Contract
To use the results in other transactions, explicit ownership must be granted with `FHE.allow(address)` or `FHE.allowThis()`.
```solidity theme={null}
contract A {
euint32 private result;
euint32 private handle1;
function doAdd(externalEuint32 input1, externalEuint32 input2, bytes calldata inputProof) external {
externalEuint32[] memory inputs = new externalEuint32[](2);
inputs[0] = input1;
inputs[1] = input2;
euint32[] memory handles = FHE.asEuint32s(inputs, inputProof); // temporary ownership of both
handle1 = handles[0];
result = FHE.add(handles[0], handles[1]); // Contract A gets temporary ownership of result
FHE.allowThis(result); // result is allowed for future transactions
}
function doSomethingWithResult() external {
FHE.allowPublic(result); // Allowed: marks result as publicly decryptable
FHE.add(handle1, result); // Reverts with ACLNotAllowed: handle1 was never allowed persistently
}
}
```
If you don't call `FHE.allowThis()` after modifying encrypted values, you won't be able to use them in future transactions. Always call `FHE.allowThis()` after operations that modify encrypted state variables.
## Allowance for Decryptions
To decrypt a ciphertext offchain through the Teecryptor, the requesting account must be allowed on the ciphertext handle with `FHE.allow(handle, userAddress)`.
```solidity theme={null}
contract A {
mapping(address => euint32) private balances;
function transfer(externalEuint32 _amount, bytes calldata inputProof, address to) external {
euint32 amount = FHE.asEuint32(_amount, inputProof);
balances[msg.sender] = FHE.sub(balances[msg.sender], amount);
balances[to] = FHE.add(balances[to], amount);
FHE.allow(balances[msg.sender], msg.sender); // now the sender can decrypt her balance
FHE.allow(balances[to], to); // now the receiver can decrypt his balance
// enable balance manipulation for future transactions
FHE.allowThis(balances[msg.sender]);
FHE.allowThis(balances[to]);
}
}
```
When allowing users to decrypt their own encrypted values, use `FHE.allow()` to grant persistent access. This enables users to decrypt values offchain using `decryptForView` without requiring additional transactions.
## Allow Other Contracts
You can also allow other contracts to use your ciphertexts, either persistently or only for the course of this transaction via `FHE.allowTransient(handle, address)`.
```solidity theme={null}
interface IContractB {
function doSomethingWithHandle1(euint32 handle1) external;
}
contract A {
address private addressB;
function doAdd(externalEuint32 input1, bytes calldata inputProof) external {
euint32 handle1 = FHE.asEuint32(input1, inputProof); // Contract A gets temporary ownership of handle1
FHE.allowTransient(handle1, addressB); // Contract B is allowed to use handle1 in this transaction alone
// or
FHE.allow(handle1, addressB); // Contract B is allowed to use handle1 forever
IContractB(addressB).doSomethingWithHandle1(handle1);
}
}
```
Use `FHE.allowTransient()` when you only need to grant access for a single transaction. Use `FHE.allow()` when you need persistent access across multiple transactions.
## Common Patterns
### Pattern 1: Allow Contract and User
When modifying encrypted values that users need to access:
```solidity theme={null}
function updateBalance(address user, externalEuint32 amount, bytes calldata inputProof) public {
euint32 encryptedAmount = FHE.asEuint32(amount, inputProof);
balances[user] = FHE.add(balances[user], encryptedAmount);
// Allow contract to use in future transactions
FHE.allowThis(balances[user]);
// Allow user to decrypt/seal their balance
FHE.allow(balances[user], user);
}
```
### Pattern 2: Allow Sender
A common pattern is to allow the message sender:
```solidity theme={null}
function submitEncryptedData(externalEuint32 data, bytes calldata inputProof) public {
euint32 encryptedData = FHE.asEuint32(data, inputProof);
storedData[msg.sender] = encryptedData;
FHE.allowThis(storedData[msg.sender]);
FHE.allowSender(storedData[msg.sender]); // Equivalent to FHE.allow(storedData[msg.sender], msg.sender)
}
```
### Pattern 3: Global Access
For values that should be accessible to everyone:
```solidity theme={null}
function setPublicValue(externalEuint32 value, bytes calldata inputProof) public onlyOwner {
publicValue = FHE.asEuint32(value, inputProof);
FHE.allowPublic(publicValue); // Everyone can now access this value
}
```
## Best Practices
After modifying any encrypted state variable, call `FHE.allowThis()` to ensure the contract can use it in future transactions.
If users need to decrypt their own values offchain, use `FHE.allow()` or `FHE.allowSender()` to grant them access.
When passing encrypted values to other contracts for a single operation, use `FHE.allowTransient()` instead of `FHE.allow()`.
## Next Steps
* Learn more about [Access Control](/cofhe/fhe-library/core-concepts/access-control) mechanisms
* Review [Your First FHE Contract](/cofhe/tutorials/your-first-fhe-contract) for a complete example
* Explore [Adding FHE to an Existing Contract](/cofhe/tutorials/adding-fhe-to-existing-contract) for migration patterns
# Adding FHE to an Existing Contract
Source: https://cofhe-docs.fhenix.zone/cofhe/tutorials/adding-fhe-to-existing-contract
Step-by-step guide for integrating FHE capabilities into contracts you've already built
## Overview
Let's start with an existing contract, and walk through the steps to migrate the contract to CoFHE.
The contract is a very simple voting contract.
## Original Contract
```solidity theme={null}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.25;
contract VotingExample {
struct Option {
string name;
uint64 votes;
}
struct Proposal {
string name;
uint256 deadline;
Option[] options;
mapping(address => bool) hasVoted;
bool exists;
uint8 winner;
}
address public owner;
uint256 public proposalCount;
mapping(uint256 => Proposal) public proposals;
event ProposalCreated(
uint256 indexed proposalId,
string name,
uint256 deadline
);
event VoteCast(
uint256 indexed proposalId,
address indexed voter,
uint256 optionIndex
);
error NotOwner();
error InvalidOptionCount();
error ProposalNotFound();
error DeadlineExpired();
error AlreadyVoted();
error InvalidOptionIndex();
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
function createProposal(
string memory _name,
string[] memory _options,
uint256 _deadline
) external onlyOwner returns (uint256) {
if (_options.length < 2 || _options.length > 4)
revert InvalidOptionCount();
uint256 proposalId = proposalCount++;
Proposal storage proposal = proposals[proposalId];
proposal.name = _name;
proposal.deadline = _deadline;
proposal.exists = true;
for (uint i = 0; i < _options.length; i++) {
proposal.options.push(Option({name: _options[i], votes: 0}));
}
emit ProposalCreated(proposalId, _name, _deadline);
return proposalId;
}
function vote(uint256 _proposalId, uint256 _optionIndex) external {
Proposal storage proposal = proposals[_proposalId];
if (!proposal.exists) revert ProposalNotFound();
if (block.timestamp >= proposal.deadline) revert DeadlineExpired();
if (proposal.hasVoted[msg.sender]) revert AlreadyVoted();
if (_optionIndex >= proposal.options.length)
revert InvalidOptionIndex();
proposal.options[_optionIndex].votes++;
proposal.hasVoted[msg.sender] = true;
emit VoteCast(_proposalId, msg.sender, _optionIndex);
}
function getProposal(
uint256 _proposalId
)
external
view
returns (
string memory name,
uint256 deadline,
Option[] memory options,
bool exists
)
{
Proposal storage proposal = proposals[_proposalId];
return (
proposal.name,
proposal.deadline,
proposal.options,
proposal.exists
);
}
function hasVoted(
uint256 _proposalId,
address _voter
) external view returns (bool) {
return proposals[_proposalId].hasVoted[_voter];
}
}
```
In this contract, the owner can create a proposal with anywhere from 2-4 possible voting options. Users can then vote on this proposal by the deadline, at which point the proposal can be closed, and the result revealed.
Unfortunately, the contract is completely public, and the vote tallies can be observed during the voting period. We will update this contract to interact with CoFHE, and use FHE encrypted variables to store the votes until the result is revealed.
We will pay special attention to the following updates that need to be made:
1. **Constant time computation**
2. **Handling the `if/else` case**
3. **Handling the `require` case**
4. **Revealing the result with the decrypt-with-proof pattern**
## Migrating the Contract
### Step 1: Import `FHE.sol`
The first thing that we need to do is import `FHE.sol` from the `cofhe-contracts` package. It gives smart contracts the FHE operations and encrypted types. We import the two encrypted types this contract stores (`euint8` and `euint64`) and the input type it receives (`externalEuint8`).
```solidity theme={null}
pragma solidity ^0.8.25;
import {FHE, euint8, euint64, externalEuint8} from "@fhenixprotocol/cofhe-contracts/FHE.sol";
contract VotingExample {
```
### Step 2: Encrypt the vote counters with `euint64`
The amount of votes cast for each option needs to be encrypted, so we can switch the variable type from a `uint64` to an `euint64` (`euint64` is included in the `FHE.sol` import). The vote counts will be represented by a `ctHash` which acts as a handle and pointer to the encrypted number of votes.
```solidity theme={null}
struct Option {
string name;
euint64 votes; // Changed from uint64
}
```
### Step 3: Initialize the `euint64` vote counts
Now that the votes type has changed, it must be initialized by performing a `trivialEncrypt` of the starting value:
```solidity theme={null}
function createProposal(
string memory _name,
string[] memory _options,
uint256 _deadline
) external onlyOwner returns (uint256) {
if (_options.length < 2 || _options.length > 4)
revert InvalidOptionCount();
uint256 proposalId = proposalCount++;
Proposal storage proposal = proposals[proposalId];
proposal.name = _name;
proposal.deadline = _deadline;
proposal.exists = true;
euint64 zero = FHE.asEuint64(0);
FHE.allowThis(zero);
for (uint i = 0; i < _options.length; i++) {
proposal.options.push(Option({name: _options[i], votes: zero}));
}
emit ProposalCreated(proposalId, _name, _deadline);
return proposalId;
}
```
The `FHE.allowThis` call matters: a trivially encrypted value is usable only inside the transaction that created it, and `vote` runs in a later transaction. Step 8 covers access in detail.
This works, but it may make sense to prepare the trivially encrypted values once, in the constructor, rather than in each transaction. That saves gas and FHE operations. Let's see how that would look:
```solidity theme={null}
contract VotingExample {
...
euint64 private EUINT64_ZERO;
euint64 private EUINT64_ONE;
constructor() {
owner = msg.sender;
EUINT64_ZERO = FHE.asEuint64(0);
EUINT64_ONE = FHE.asEuint64(1);
FHE.allowThis(EUINT64_ZERO);
FHE.allowThis(EUINT64_ONE);
}
function createProposal(
string memory _name,
string[] memory _options,
uint256 _deadline
) external onlyOwner returns (uint256) {
...
for (uint i = 0; i < _options.length; i++) {
proposal.options.push(
Option({name: _options[i], votes: EUINT64_ZERO})
);
}
...
}
}
```
### Step 4: Handle user votes with `externalEuint8`
We now need to handle the user's vote casting. The first thing that we need to do is hide which option the user is voting for. We can do this by replacing the `vote` function parameter `uint256 _optionIndex` with two parameters: `externalEuint8 _optionIndex`, the encrypted input handle, and `bytes calldata inputProof`, the proof that authenticates it. `FHE.asEuint8(_optionIndex, inputProof)` then converts the pair into an `euint8` for use in computation.
Encrypting inputs requires the use of the [**Client SDK**](/cofhe/client-sdk/introduction/overview) (`@cofhe/sdk`).
Read more about [**encrypted inputs**](/cofhe/client-sdk/guides/encrypting-inputs).
```solidity theme={null}
function vote(uint256 _proposalId, externalEuint8 _optionIndex, bytes calldata inputProof) external {
euint8 optionIndex = FHE.asEuint8(_optionIndex, inputProof);
```
### Step 5: Constant time computation
To preserve the confidentiality of the user's vote, we must make sure that we aren't leaking any information about the user's choice. If we only updated the voting option that the user has selected, then a user's vote could be deduced by simply watching which vote counter changes. Therefore, we must update *all* the vote counters to hide the user's true vote:
```solidity theme={null}
function vote(uint256 _proposalId, externalEuint8 _optionIndex, bytes calldata inputProof) external {
euint8 optionIndex = FHE.asEuint8(_optionIndex, inputProof);
Proposal storage proposal = proposals[_proposalId];
if (!proposal.exists) revert ProposalNotFound();
if (block.timestamp >= proposal.deadline) revert DeadlineExpired();
if (proposal.hasVoted[msg.sender]) revert AlreadyVoted();
for (uint8 i = 0; i < proposal.options.length; i++) {
proposal.options[i].votes = FHE.add(
proposal.options[i].votes,
FHE.select(
optionIndex.eq(FHE.asEuint8(i)),
EUINT64_ONE,
EUINT64_ZERO
)
);
}
proposal.hasVoted[msg.sender] = true;
emit VoteCast(_proposalId, msg.sender, optionIndex);
}
```
Let's break down how this works:
* We iterate through each of the proposal options
* We *always* perform an `FHE.add` on every options' vote count, this means that every options' vote count will change any time a user votes
* We only want to increment the user's selected choice, so we use `FHE.select`
* The API for select is `FHE.select(conditional, ifTrue, ifFalse)`
* Without the FHE syntax, the logic is as follows:
```solidity theme={null}
proposal.options[i].votes =
proposal.options[i].votes + (_optionIndex == i ? 1 : 0);
```
### Step 6: Handling `if/else`
Branching based on encrypted variables is not allowed, so there is one more change to make to the `vote` function, which is to remove the **`if/else`** branch that relies on `_optionIndex`.
**`require`** statements are also not allowed when working with encrypted variables for the same reason as above.
```solidity theme={null}
// Remove this check - it cannot work with encrypted variables
// if (_optionIndex >= proposal.options.length)
// revert InvalidOptionIndex();
```
It is important to *never* use an encrypted variable as part of an if/else branch, since the encrypted variable is always truthy. Instead, use `FHE.select` to replace the value with 0.
### Step 7: Update `VoteCast` event
Currently the `VoteCast` event emits a `uint256 optionIndex`. Now that the user's vote is an encrypted variable (`euint8`), we need to update the event to emit the user's encrypted input:
```solidity theme={null}
event VoteCast(
uint256 indexed proposalId,
address indexed voter,
euint8 optionIndex // Changed from uint256
);
```
And its invocation:
```solidity theme={null}
emit VoteCast(_proposalId, msg.sender, optionIndex);
```
This event will be emitted with the encrypted `euint8 optionIndex`. This is important as in the future the FHE block explorer will be able to decrypt these variables and show the true event log, but only if we make one more change.
### Step 8: Granting access with `FHE.allow`
By default, access and computation on an encrypted variable is blocked, and trying to use a variable before access has been granted will cause AccessControlList.sol (ACL) to revert with an `ACLNotAllowed` error. Access is granted using `FHE.allow` (and its variants `FHE.allowSender`, `FHE.allowThis`, and `FHE.allowPublic`). After access is granted, the encrypted variable may be used in a computation, or decrypted by any of the authorized users or contracts.
**`FHE.allow` variants:**
* `FHE.allow(ctHash, address)` - grants access to `address`. `ctHash` is any encrypted variable like `euint64` or `ebool`
* `FHE.allowSender(ctHash)` - grants access to `msg.sender`
* `FHE.allowThis(ctHash)` - grants access to the executing contract (`address(this)`)
* `FHE.allowPublic(ctHash)` - grants access to everyone, useful for things like an encrypted totalSupply variable, which everyone should have access to
```solidity theme={null}
function vote(uint256 _proposalId, externalEuint8 _optionIndex, bytes calldata inputProof) external {
euint8 optionIndex = FHE.asEuint8(_optionIndex, inputProof);
Proposal storage proposal = proposals[_proposalId];
if (!proposal.exists) revert ProposalNotFound();
if (block.timestamp >= proposal.deadline) revert DeadlineExpired();
if (proposal.hasVoted[msg.sender]) revert AlreadyVoted();
for (uint8 i = 0; i < proposal.options.length; i++) {
proposal.options[i].votes = FHE.add(
proposal.options[i].votes,
FHE.select(
optionIndex.eq(FHE.asEuint8(i)),
EUINT64_ONE,
EUINT64_ZERO
)
);
// Grant this contract access to each vote count
// Without this, FHE.add(options[i].votes, ...) would revert
FHE.allowThis(proposal.options[i].votes);
}
proposal.hasVoted[msg.sender] = true;
// Grant msg.sender access to their votingIndex
// Without this, the msg.sender would not be able to see their vote in the explorer (coming soon)
FHE.allowSender(optionIndex);
emit VoteCast(_proposalId, msg.sender, optionIndex);
}
```
It is critical to ensure that `FHE.allowThis` is used on encrypted variables that need to be used later in the contract's lifecycle. Contracts must have access to variables to perform FHE operations on them.
### Step 9: Finalize the voting with `FHE.allowPublic`
Because the votes are encrypted for the lifetime of the proposal, after the proposal has ended, we need to decrypt the results and reveal the winner. Let's start by adding a `finalizeVote` function that marks the vote counts as publicly decryptable:
```solidity theme={null}
function finalizeVote(uint256 _proposalId) external {
if (msg.sender != owner) revert NotOwner();
Proposal storage proposal = proposals[_proposalId];
if (!proposal.exists) revert ProposalNotFound();
if (block.timestamp < proposal.deadline) revert DeadlineNotReached();
for (uint8 i = 0; i < proposal.options.length; i++) {
FHE.allowPublic(proposal.options[i].votes);
}
}
```
`FHE.allowPublic` marks each vote count as eligible for public decryption. Anyone can now request the plaintext values offchain.
### Step 10: Reveal the results with `FHE.publishDecryptResult`
After `finalizeVote` is called, the vote counts need to be decrypted offchain and published onchain with proof. The client needs the encrypted vote handles to request decryption, and `getProposal` returns option names and plaintext counts only, so we add a getter for the handles:
```solidity theme={null}
function getEncryptedVotes(
uint256 _proposalId
) external view returns (euint64[] memory votes) {
Proposal storage proposal = proposals[_proposalId];
if (!proposal.exists) revert ProposalNotFound();
votes = new euint64[](proposal.options.length);
for (uint8 i = 0; i < proposal.options.length; i++) {
votes[i] = proposal.options[i].votes;
}
}
```
We then add a `revealResults` function that accepts the decrypted values and the Teecryptor signatures over them:
```solidity theme={null}
function revealResults(
uint256 _proposalId,
uint64[] calldata _decryptedVotes,
bytes[] calldata _signatures
) external {
Proposal storage proposal = proposals[_proposalId];
if (!proposal.exists) revert ProposalNotFound();
require(
_decryptedVotes.length == proposal.options.length,
"Mismatched lengths"
);
for (uint8 i = 0; i < proposal.options.length; i++) {
FHE.publishDecryptResult(
proposal.options[i].votes,
_decryptedVotes[i],
_signatures[i]
);
}
}
```
The client-side flow looks like this. It assumes a connected `client` (see [client setup](/cofhe/client-sdk/guides/client-setup)), viem `publicClient` and `walletClient` instances, the contract ABI as `votingAbi`, and its address as `votingAddress`:
```typescript theme={null}
// 1. Finalize the vote (owner only)
await walletClient.writeContract({
address: votingAddress,
abi: votingAbi,
functionName: 'finalizeVote',
args: [proposalId],
});
// 2. Read the encrypted tallies and decrypt each one offchain
const encryptedVotes = await publicClient.readContract({
address: votingAddress,
abi: votingAbi,
functionName: 'getEncryptedVotes',
args: [proposalId],
});
const decryptedVotes: bigint[] = [];
const signatures: `0x${string}`[] = [];
for (const ctHash of encryptedVotes) {
const result = await client
.decryptForTx(ctHash)
.withoutACP()
.execute();
decryptedVotes.push(result.decryptedValue);
signatures.push(result.signature);
}
// 3. Publish all results onchain
await walletClient.writeContract({
address: votingAddress,
abi: votingAbi,
functionName: 'revealResults',
args: [proposalId, decryptedVotes, signatures],
});
```
### Step 11: Checking the results with `FHE.getDecryptResultSafe`
Finally, we can update our `getProposal` function to check the final state of the proposal. Once the results have been published, `FHE.getDecryptResultSafe` will return the plaintext values:
```solidity theme={null}
function getProposal(
uint256 _proposalId
)
external
view
returns (
string memory name,
uint256 deadline,
bool exists,
string[] memory options,
uint256[] memory votes,
bool finalized,
uint8 winner
)
{
Proposal storage proposal = proposals[_proposalId];
// Plaintext values can be assigned directly
name = proposal.name;
deadline = proposal.deadline;
exists = proposal.exists;
options = new string[](proposal.options.length);
for (uint8 i = 0; i < proposal.options.length; i++) {
options[i] = proposal.options[i].name;
}
// Fetch the decrypted results with `FHE.getDecryptResultSafe`.
// If any of the results have not yet been decrypted, set `finalized` to false.
// Store the decrypted result counts in the `votes` list to be returned.
votes = new uint256[](proposal.options.length);
finalized = true;
for (uint8 i = 0; i < proposal.options.length; i++) {
(uint256 result, bool decrypted) = FHE.getDecryptResultSafe(
proposal.options[i].votes
);
votes[i] = decrypted ? result : 0;
if (!decrypted) finalized = false;
}
// If all votes have been decrypted, determine the winning option.
if (finalized) {
uint256 maxVotes = 0;
winner = 0;
for (uint8 i = 0; i < proposal.options.length; i++) {
if (votes[i] > maxVotes) {
maxVotes = votes[i];
winner = i;
}
}
}
}
```
In this block you can see a few changes. The first is that we have split `options` and `votes` from the `getProposal` return type, which allows us to better handle the decryption results.
We use `FHE.getDecryptResultSafe` to fetch the decryption result of each of the vote counts, which returns the result as well as a flag indicating whether the decryption has posted.
Once all the decryptions have posted, the `finalized` flag will update to be `true`, and the `winner` determined based on the vote counts.
## Conclusions
In this tutorial, we walked through migrating a simple voting contract to use CoFHE. The key changes were:
1. Changing the vote counts from plain `uint64` to encrypted values using `euint64`
2. Modifying the voting function to use encrypted addition instead of plain addition
3. Using `FHE.allowPublic` to mark values as decryptable after the voting deadline
4. Adding a `revealResults` function that publishes decrypted values onchain with `FHE.publishDecryptResult`
5. Updating the getter function to handle decryption of results safely
The resulting contract provides the same functionality as the original, but with the added privacy benefit that individual votes are not visible onchain until the final tally is decrypted. This demonstrates how CoFHE can be used to add privacy to existing contracts with minimal changes to the core logic.
## Final `FHEVotingExample.sol`
```solidity theme={null}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.25;
import {FHE, euint8, euint64, externalEuint8} from "@fhenixprotocol/cofhe-contracts/FHE.sol";
contract FHEVotingExample {
struct Option {
string name;
euint64 votes;
}
struct Proposal {
string name;
uint256 deadline;
Option[] options;
mapping(address => bool) hasVoted;
bool exists;
uint8 winner;
}
address public owner;
uint256 public proposalCount;
mapping(uint256 => Proposal) public proposals;
euint64 private EUINT64_ZERO;
euint64 private EUINT64_ONE;
event ProposalCreated(
uint256 indexed proposalId,
string name,
uint256 deadline
);
event VoteCast(
uint256 indexed proposalId,
address indexed voter,
euint8 optionIndex
);
error NotOwner();
error InvalidOptionCount();
error ProposalNotFound();
error DeadlineExpired();
error AlreadyVoted();
error InvalidOptionIndex();
error DeadlineNotReached();
constructor() {
owner = msg.sender;
EUINT64_ZERO = FHE.asEuint64(0);
EUINT64_ONE = FHE.asEuint64(1);
FHE.allowThis(EUINT64_ZERO);
FHE.allowThis(EUINT64_ONE);
}
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
function createProposal(
string memory _name,
string[] memory _options,
uint256 _deadline
) external onlyOwner returns (uint256) {
if (_options.length < 2 || _options.length > 4)
revert InvalidOptionCount();
uint256 proposalId = proposalCount++;
Proposal storage proposal = proposals[proposalId];
proposal.name = _name;
proposal.deadline = _deadline;
proposal.exists = true;
for (uint i = 0; i < _options.length; i++) {
proposal.options.push(
Option({name: _options[i], votes: EUINT64_ZERO})
);
}
emit ProposalCreated(proposalId, _name, _deadline);
return proposalId;
}
function vote(uint256 _proposalId, externalEuint8 _optionIndex, bytes calldata inputProof) external {
euint8 optionIndex = FHE.asEuint8(_optionIndex, inputProof);
Proposal storage proposal = proposals[_proposalId];
if (!proposal.exists) revert ProposalNotFound();
if (block.timestamp >= proposal.deadline) revert DeadlineExpired();
if (proposal.hasVoted[msg.sender]) revert AlreadyVoted();
for (uint8 i = 0; i < proposal.options.length; i++) {
proposal.options[i].votes = FHE.add(
proposal.options[i].votes,
FHE.select(
optionIndex.eq(FHE.asEuint8(i)),
EUINT64_ONE,
EUINT64_ZERO
)
);
FHE.allowThis(proposal.options[i].votes);
}
proposal.hasVoted[msg.sender] = true;
FHE.allowSender(optionIndex);
emit VoteCast(_proposalId, msg.sender, optionIndex);
}
function finalizeVote(uint256 _proposalId) external {
if (msg.sender != owner) revert NotOwner();
Proposal storage proposal = proposals[_proposalId];
if (!proposal.exists) revert ProposalNotFound();
if (block.timestamp < proposal.deadline) revert DeadlineNotReached();
for (uint8 i = 0; i < proposal.options.length; i++) {
FHE.allowPublic(proposal.options[i].votes);
}
}
function getEncryptedVotes(
uint256 _proposalId
) external view returns (euint64[] memory votes) {
Proposal storage proposal = proposals[_proposalId];
if (!proposal.exists) revert ProposalNotFound();
votes = new euint64[](proposal.options.length);
for (uint8 i = 0; i < proposal.options.length; i++) {
votes[i] = proposal.options[i].votes;
}
}
function revealResults(
uint256 _proposalId,
uint64[] calldata _decryptedVotes,
bytes[] calldata _signatures
) external {
Proposal storage proposal = proposals[_proposalId];
if (!proposal.exists) revert ProposalNotFound();
require(
_decryptedVotes.length == proposal.options.length,
"Mismatched lengths"
);
for (uint8 i = 0; i < proposal.options.length; i++) {
FHE.publishDecryptResult(
proposal.options[i].votes,
_decryptedVotes[i],
_signatures[i]
);
}
}
function getProposal(
uint256 _proposalId
)
external
view
returns (
string memory name,
uint256 deadline,
bool exists,
string[] memory options,
uint256[] memory votes,
bool finalized,
uint8 winner
)
{
Proposal storage proposal = proposals[_proposalId];
name = proposal.name;
deadline = proposal.deadline;
exists = proposal.exists;
options = new string[](proposal.options.length);
for (uint8 i = 0; i < proposal.options.length; i++) {
options[i] = proposal.options[i].name;
}
votes = new uint256[](proposal.options.length);
finalized = true;
for (uint8 i = 0; i < proposal.options.length; i++) {
(uint256 result, bool decrypted) = FHE.getDecryptResultSafe(
proposal.options[i].votes
);
votes[i] = decrypted ? result : 0;
if (!decrypted) finalized = false;
}
if (finalized) {
uint256 maxVotes = 0;
winner = 0;
for (uint8 i = 0; i < proposal.options.length; i++) {
if (votes[i] > maxVotes) {
maxVotes = votes[i];
winner = i;
}
}
}
}
function hasVoted(
uint256 _proposalId,
address _voter
) external view returns (bool) {
return proposals[_proposalId].hasVoted[_voter];
}
}
```
## Next Steps
* Learn about [Migrating from FHE.decrypt](/cofhe/tutorials/migrating-from-fhe-decrypt) to the new decryption flow
* Explore [ACL Usage Examples](/cofhe/tutorials/acl-usage-examples) for complex access control patterns
* Review [Best Practices](/cofhe/fhe-library/introduction/best-practices) for security considerations
# Migrating from FHE.decrypt to the New Decryption Flow
Source: https://cofhe-docs.fhenix.zone/cofhe/tutorials/migrating-from-fhe-decrypt
Step-by-step guide for migrating Solidity contracts from FHE.decrypt to the new allowPublic + publishDecryptResult pattern
## Overview
The old decryption pattern used `FHE.decrypt(ctHash)` to trigger an asynchronous decryption, followed by `FHE.getDecryptResultSafe(ctHash)` to read the result once available. The new pattern replaces `FHE.decrypt` with an offchain decryption step using the Client SDK, and uses `FHE.publishDecryptResult` to submit the result onchain with a cryptographic proof.
`FHE.decrypt` no longer exists in `FHE.sol`. A contract that still calls it does not compile.
This guide walks through concrete before/after Solidity examples.
## What changed
| | Old pattern | New pattern |
| - | - | - |
| **Trigger decrypt** | `FHE.decrypt(ctHash)` (onchain) | `FHE.allowPublic(ctHash)` (onchain) + `client.decryptForTx(ctHash)` (offchain) |
| **Submit result** | Automatic (async, no proof) | `FHE.publishDecryptResult(ctHash, plaintext, signature)` |
| **Verify only** | N/A | `FHE.verifyDecryptResult(ctHash, plaintext, signature)` |
| **Read result** | `FHE.getDecryptResultSafe(ctHash)` | `FHE.getDecryptResultSafe(ctHash)` (same) |
The key difference: `FHE.decrypt` triggered decryption without any proof. The new flow returns a signature from [Teecryptor](/cofhe/deep-dive/cofhe-components/teecryptor), which decrypts inside a hardware-attested TEE. Your contract verifies that signature before it uses the plaintext.
***
## The new decryption flow
Instead of calling `FHE.decrypt()`, mark the value as decryptable:
```solidity theme={null}
// Anyone can request decryption (replaces FHE.decrypt)
FHE.allowPublic(encryptedValue);
// Or restrict to specific address
FHE.allow(encryptedValue, authorizedAddress);
```
The client requests decryption from Teecryptor, which returns the plaintext and an ECDSA signature over it:
```typescript theme={null}
const result = await client
.decryptForTx(ctHash)
.withoutACP() // use .withACP() if FHE.allow was used instead of allowPublic
.execute();
// result.decryptedValue: the plaintext (bigint)
// result.signature: Teecryptor's signature over that plaintext
```
The decrypted value and signature are submitted to your contract:
```solidity theme={null}
// Publishes the result onchain (readable via getDecryptResultSafe)
FHE.publishDecryptResult(encryptedValue, plaintext, signature);
// Or verify without storing (reverts on a bad signature)
FHE.verifyDecryptResult(encryptedValue, plaintext, signature);
```
Teecryptor decrypts today. A multi-party Threshold Network is the planned replacement; see [Future Plans](/cofhe/deep-dive/research/future-plans).
***
## Example 1: Counter contract
A minimal example showing how the reveal pattern changes.
```solidity Before (FHE.decrypt) theme={null}
contract Counter {
euint64 public counter;
function increment() external {
counter = FHE.add(counter, FHE.asEuint64(1));
FHE.allowThis(counter);
}
// Old: trigger async decryption onchain
function request_reveal() external {
FHE.decrypt(counter);
}
// Old: read the result once available
function get_counter_value() external view returns (uint256) {
(uint256 value, bool decrypted) = FHE.getDecryptResultSafe(counter);
if (!decrypted) revert("Value is not ready");
return value;
}
}
```
```solidity After (new flow) theme={null}
contract Counter {
euint64 public counter;
function increment() external {
counter = FHE.add(counter, FHE.asEuint64(1));
FHE.allowThis(counter);
}
// New: mark as publicly decryptable
function allow_counter_publicly() external {
FHE.allowPublic(counter);
}
// New: accept the decrypted value with its Teecryptor signature
function reveal_counter(uint64 _decrypted, bytes memory _signature) external {
FHE.publishDecryptResult(counter, _decrypted, _signature);
}
// Same: read the published result
function get_counter_value() external view returns (uint256) {
(uint256 value, bool decrypted) = FHE.getDecryptResultSafe(counter);
if (!decrypted) revert("Value is not ready");
return value;
}
}
```
**Client-side flow (new):**
```typescript theme={null}
// 1. Allow public decryption
await counter.allow_counter_publicly();
// 2. Decrypt offchain
const ctHash = await counter.counter();
const result = await client
.decryptForTx(ctHash)
.withoutACP()
.execute();
// 3. Publish onchain with proof
await counter.reveal_counter(result.decryptedValue, result.signature);
// 4. Read the result
const value = await counter.get_counter_value();
```
***
## Example 2: Token unshield
The unshield flow is where `FHE.decrypt` was most commonly used. It already followed a two-step pattern (unshield + claim), which maps naturally to the new flow.
This example is a minimal wrapper written to show the migration. The FHERC20 wrappers in `fhenix-confidential-contracts` key claims by a claim ID rather than by the handle, because two unshields can produce the same handle. To use those contracts, follow [wrap ERC-20 tokens and ETH](/cofhe/fhe-library/confidential-contracts/fherc20/fherc20-wrapper).
```solidity Before (FHE.decrypt) theme={null}
function unshield(address to, uint64 value) public {
if (to == address(0)) to = msg.sender;
euint64 burned = _burn(msg.sender, value);
// Old: trigger async decryption onchain
FHE.decrypt(burned);
_createClaim(to, value, burned);
emit UnshieldedERC20(msg.sender, to, value);
}
function claimUnshielded(bytes32 ctHash) public {
Claim memory claim = _claims[ctHash];
if (claim.to == address(0)) revert ClaimNotFound();
if (claim.claimed) revert AlreadyClaimed();
// Old: read the async decryption result
(uint256 amount, bool decrypted) = FHE.getDecryptResultSafe(euint64.wrap(ctHash));
if (!decrypted) revert("Not yet decrypted");
claim.claimed = true;
_erc20.safeTransfer(claim.to, amount);
emit ClaimedUnshieldedERC20(msg.sender, claim.to, amount);
}
```
```solidity After (new flow) theme={null}
function unshield(address to, uint64 value) public {
if (to == address(0)) to = msg.sender;
euint64 burned = _burn(msg.sender, value);
// New: mark as publicly decryptable (replaces FHE.decrypt)
FHE.allowPublic(burned);
_createClaim(to, value, burned);
emit UnshieldedERC20(msg.sender, to, value);
}
function claimUnshielded(
bytes32 ctHash,
uint64 decryptedAmount,
bytes memory decryptionSignature
) public {
// New: verify and publish the decryption proof
FHE.publishDecryptResult(
euint64.wrap(ctHash),
decryptedAmount,
decryptionSignature
);
Claim memory claim = _claims[ctHash];
if (claim.to == address(0)) revert ClaimNotFound();
if (claim.claimed) revert AlreadyClaimed();
claim.claimed = true;
_erc20.safeTransfer(claim.to, decryptedAmount);
emit ClaimedUnshieldedERC20(msg.sender, claim.to, decryptedAmount);
}
```
**Key differences:**
* `FHE.decrypt(burned)` to `FHE.allowPublic(burned)`: no onchain decryption is triggered, only a permission grant
* `claimUnshielded(bytes32 ctHash)` to `claimUnshielded(bytes32 ctHash, uint64 decryptedAmount, bytes signature)`: the caller now provides the decrypted value + proof
* `FHE.getDecryptResultSafe` to `FHE.publishDecryptResult`: the contract verifies the Teecryptor signature instead of polling for a result
**Client-side flow (new):**
```typescript theme={null}
// 1. Request unshield
await token.unshield(recipientAddress, 100n);
// 2. Get the ctHash of the burned amount from the pending claim
// (UnshieldedERC20 does not carry it)
const claims = await token.getUserClaims(recipientAddress);
const ctHash = claims[claims.length - 1].ctHash;
// 3. Decrypt offchain
const result = await client
.decryptForTx(ctHash)
.withoutACP()
.execute();
// 4. Claim with proof
await token.claimUnshielded(
ctHash,
result.decryptedValue,
result.signature
);
```
***
## Example 3: Revealing a vote result
Reveal a single encrypted vote count after a deadline.
```solidity Before (FHE.decrypt) theme={null}
euint64 public totalVotes;
function closeVoting() external onlyOwner {
require(block.timestamp >= deadline, "Not ended");
// Old: trigger async decryption onchain
FHE.decrypt(totalVotes);
}
function getResult() external view returns (uint256) {
// Old: poll for the async result
(uint256 result, bool decrypted) = FHE.getDecryptResultSafe(totalVotes);
if (!decrypted) revert("Not yet decrypted");
return result;
}
```
```solidity After (new flow) theme={null}
euint64 public totalVotes;
function closeVoting() external onlyOwner {
require(block.timestamp >= deadline, "Not ended");
// New: mark as publicly decryptable
FHE.allowPublic(totalVotes);
}
function revealResult(uint64 plaintext, bytes calldata signature) external {
// New: publish the plaintext with its Teecryptor signature
FHE.publishDecryptResult(totalVotes, plaintext, signature);
}
function getResult() external view returns (uint256) {
// Same: read the published result
(uint256 result, bool decrypted) = FHE.getDecryptResultSafe(totalVotes);
if (!decrypted) revert("Not yet decrypted");
return result;
}
```
***
## `publishDecryptResult` vs `verifyDecryptResult`
| Method | Stores result onchain | Others can read it | Use case |
| - | - | - | - |
| `publishDecryptResult` | Yes | Yes, via `getDecryptResultSafe` | Revealing results publicly (auctions, votes, counters) |
| `verifyDecryptResult` | No | No | One-time verification (transfers, burns) |
`verifyDecryptResult` reverts when the signature is invalid. `verifyDecryptResultSafe` returns `false` instead, so you can choose your own revert message.
Use either one when you only need to confirm the plaintext is authentic and don't need other contracts or future calls to read it:
```solidity theme={null}
function transferToPublic(
address recipient,
bytes32 ctHash,
uint32 plaintext,
bytes calldata signature
) external {
// Verify authenticity without publishing
require(
FHE.verifyDecryptResultSafe(euint32.wrap(ctHash), plaintext, signature),
"Invalid decrypt proof"
);
_transfer(msg.sender, recipient, plaintext);
}
```
***
## Migration checklist
Search your contracts for `FHE.decrypt(`. Each call needs to be replaced.
In the function that previously called `FHE.decrypt(ctHash)`, replace it with `FHE.allowPublic(ctHash)`.
Create a new function that accepts `(plaintext, signature)` parameters and calls `FHE.publishDecryptResult` or `FHE.verifyDecryptResult`.
Add the offchain decryption step using `client.decryptForTx()` between the two onchain calls.
## Next steps
* Read about [Decryption Operations](/cofhe/fhe-library/core-concepts/decryption-operations) for the full reference
* See [Adding FHE to an Existing Contract](/cofhe/tutorials/adding-fhe-to-existing-contract) for a complete contract migration
* Learn about [Access Control](/cofhe/fhe-library/core-concepts/access-control) for managing decrypt permissions
* Check the [Client SDK decrypt guide](/cofhe/client-sdk/guides/decrypt-to-tx) for the full client-side API
# CoFHE Tutorials Overview
Source: https://cofhe-docs.fhenix.zone/cofhe/tutorials/overview
An index of the CoFHE tutorials: build, extend, and migrate FHE smart contracts step by step
## Welcome
Welcome to the CoFHE tutorials hub! This page provides a comprehensive overview of all available tutorials to help you learn and master Fully Homomorphic Encryption (FHE) development on the blockchain. Our tutorials are organized into progressive learning paths, from beginner-friendly introductions to advanced implementation techniques.
## Quick Start Tutorials
Start your CoFHE journey with these foundational tutorials designed to get you up and running quickly:
| Tutorial | Description |
| - | - |
| [Quick Start](/cofhe/fhe-library/introduction/quick-start) | Set up your environment for FHE enabled development and testing |
| [Best Practices](/cofhe/fhe-library/introduction/best-practices) | Discover security and performance optimization techniques for FHE development |
## Advanced Tutorials
Once you've mastered the basics, dive deeper with these specialized tutorials:
| Tutorial | Description |
| - | - |
| [Your First FHE Contract](/cofhe/tutorials/your-first-fhe-contract) | Build a simple encrypted counter contract to understand fundamental FHE concepts |
| [Adding FHE to an Existing Contract](/cofhe/tutorials/adding-fhe-to-existing-contract) | Learn how to transform traditional smart contracts into privacy-preserving ones |
| [Migrating from FHE.decrypt](/cofhe/tutorials/migrating-from-fhe-decrypt) | Migrate from the old `FHE.decrypt` pattern to the new `allowPublic` + `publishDecryptResult` flow |
| [ACL Usage Examples](/cofhe/tutorials/acl-usage-examples) | Implement common access control mechanisms for encrypted data |
Ready to start your privacy-preserving blockchain development journey? Choose a tutorial from the table above and dive in!
# Your First FHE Contract
Source: https://cofhe-docs.fhenix.zone/cofhe/tutorials/your-first-fhe-contract
Step-by-step tutorial for building your first FHE-enabled smart contract from scratch
## Overview
Let's take a look at a simple contract that uses FHE to encrypt a counter, and break it down into its components.
## Complete contract example
```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import {FHE, euint64, externalEuint64} from "@fhenixprotocol/cofhe-contracts/FHE.sol";
contract SimpleCounter {
address owner;
euint64 counter;
euint64 delta;
modifier onlyOwner() {
require(msg.sender == owner, "Only the owner can access that function");
_;
}
constructor(uint64 initial_value) {
owner = msg.sender;
counter = FHE.asEuint64(initial_value);
FHE.allowThis(counter);
// Encrypt the value 1 only once instead of every value change
delta = FHE.asEuint64(1);
FHE.allowThis(delta);
}
function increment_counter() external onlyOwner {
counter = FHE.add(counter, delta);
FHE.allowThis(counter);
}
function decrement_counter() external onlyOwner {
counter = FHE.sub(counter, delta);
FHE.allowThis(counter);
}
function reset_counter(externalEuint64 value, bytes calldata inputProof) external onlyOwner {
counter = FHE.asEuint64(value, inputProof);
FHE.allowThis(counter);
}
function allow_counter_publicly() external onlyOwner {
FHE.allowPublic(counter);
}
function reveal_counter(uint64 _decrypted, bytes memory _signature) external {
FHE.publishDecryptResult(counter, _decrypted, _signature);
}
function get_counter_value() external view returns(uint256) {
(uint256 value, bool decrypted) = FHE.getDecryptResultSafe(counter);
if (!decrypted)
revert("Value is not ready");
return value;
}
function get_encrypted_counter_value() external view returns(euint64) {
return counter;
}
}
```
## Breaking down the contract
### Importing the FHE library
To start using FHE, we need to import the FHE library.
In this example, we're importing the types `euint64` and `externalEuint64` from the [FHE library](/cofhe/fhe-library/reference/fhe-sol/overview).
```solidity theme={null}
import {FHE, euint64, externalEuint64} from "@fhenixprotocol/cofhe-contracts/FHE.sol";
```
We want to keep the counter encrypted at all times, so we'll use the `euint64` type.
### State variables
Next, we define some state variables for the contract:
```solidity theme={null}
euint64 counter;
euint64 delta;
```
### Constructor initialization
In the constructor, we initialize the `counter` and `delta` variables.
We encrypt the `delta` here to avoid calculating the same encrypted value every time we increment or decrement the counter.
```solidity theme={null}
counter = FHE.asEuint64(initial_value);
delta = FHE.asEuint64(1);
```
We wanted the example contract to be as simple as possible, so readers can plug-and-play it into their preferred environment.
There are some privacy improvements that could be made to this contract.
When we initialize the `delta` and `counter` variables, we use **trivial encryption**.
**Trivial encryption** produces a ciphertext from a public value, but this variable, even though represented as a ciphertext handle, is not really confidential because everyone can see what is the plaintext value that went into it.
To make it completely private, initialize these variables from an encrypted input sent by the client (an `externalEuint64` handle plus its proof), the same way `reset_counter` does.
Read more on the [trivial encryption](/cofhe/fhe-library/core-concepts/trivial-encryption) page.
### Access control
For every encrypted variable, we need to call `FHE.allowThis()` to allow the contract to access it.
**Allowing access to encrypted variables** is an important concept in FHE-enabled contracts.
Without it, the contract could not continue to use this encrypted variable in future transactions.
You can read more about this in the [ACL Mechanism](/cofhe/fhe-library/core-concepts/access-control) page.
```solidity theme={null}
FHE.allowThis(counter);
FHE.allowThis(delta);
```
### Increment and decrement functions
In the `increment_counter` and `decrement_counter` functions, we use the `FHE.add` and `FHE.sub` functions to increment and decrement the counter, respectively.
And we also call `FHE.allowThis()` to allow the contract to access the new counter value.
```solidity theme={null}
counter = FHE.add(counter, delta);
FHE.allowThis(counter);
```
### Reset function
In the `reset_counter` function, we receive an `externalEuint64` handle and the `bytes` proof that authenticates it. Together they represent an encrypted value that can be used to reset the counter, and `FHE.asEuint64(value, inputProof)` converts the pair into a `euint64` the contract can compute on.
Both come from the client, created with the SDK. See [encrypting inputs](/cofhe/client-sdk/guides/encrypting-inputs).
### Decryption: allow public and reveal
Decryption follows a two-step onchain pattern, with an offchain step in between.
**Step 1: Allow public decryption (onchain)**
The owner calls `allow_counter_publicly` to mark the counter as eligible for public decryption:
```solidity theme={null}
function allow_counter_publicly() external onlyOwner {
FHE.allowPublic(counter);
}
```
**Step 2: Decrypt offchain**
Anyone can now request decryption offchain with `decryptForTx`. The Teecryptor (the TEE decryption service) returns the plaintext value and a signature over it. The client snippets on this page assume a connected `client` (see [client setup](/cofhe/client-sdk/guides/client-setup)), viem `publicClient` and `walletClient` instances, and the contract ABI as `simpleCounterAbi`.
The contract keeps `counter` private, so read the handle through `get_encrypted_counter_value`:
```typescript theme={null}
const countCtHash = await publicClient.readContract({
address: counterAddress,
abi: simpleCounterAbi,
functionName: 'get_encrypted_counter_value',
});
const decryptResult = await client
.decryptForTx(countCtHash)
.withoutACP()
.execute();
decryptResult.decryptedValue; // bigint
decryptResult.signature; // Teecryptor signature over the value
```
**Step 3: Publish onchain with proof**
The decrypted value and signature are submitted onchain. `FHE.publishDecryptResult` verifies the signature and stores the plaintext. If the signature is invalid, the transaction reverts:
```solidity theme={null}
function reveal_counter(uint64 _decrypted, bytes memory _signature) external {
FHE.publishDecryptResult(counter, _decrypted, _signature);
}
```
```typescript theme={null}
await walletClient.writeContract({
address: counterAddress,
abi: simpleCounterAbi,
functionName: 'reveal_counter',
args: [decryptResult.decryptedValue, decryptResult.signature],
});
```
### Reading the decrypted value
Once the result has been published, anyone can read the counter's value using `get_counter_value`. This function uses `FHE.getDecryptResultSafe` to check if a published result is available:
```solidity theme={null}
function get_counter_value() external view returns(uint256) {
(uint256 value, bool decrypted) = FHE.getDecryptResultSafe(counter);
if (!decrypted)
revert("Value is not ready");
return value;
}
```
If the result has not been published yet, the function reverts. Otherwise, it returns the plaintext value.
## Privacy considerations
In this contract, only the owner can allow public decryption. Once `reveal_counter` is called, the plaintext value is published onchain and visible to everyone.
What if we want to allow the owner to privately read the value without revealing it publicly?
For that, we need to add a call for `FHE.allow(counter, owner)` or `FHE.allowSender(counter)` every time that we change the counter's value.
This lets the owner read the encrypted counter's value using the `get_encrypted_counter_value` function and decrypt it privately offchain using `decryptForView`. The call needs the value's type, and it resolves to the plaintext directly:
```solidity theme={null}
function increment_counter() external onlyOwner {
counter = FHE.add(counter, delta);
FHE.allowThis(counter);
FHE.allowSender(counter);
}
function get_encrypted_counter_value() external view returns(euint64) {
return counter;
}
```
```typescript theme={null}
import { FheTypes } from '@cofhe/sdk';
// Decrypt privately offchain: no transaction, no public reveal
const countCtHash = await publicClient.readContract({
address: counterAddress,
abi: simpleCounterAbi,
functionName: 'get_encrypted_counter_value',
});
await client.acp.getOrCreateSelfACP();
const plaintext = await client
.decryptForView(countCtHash, FheTypes.Uint64)
.withACP()
.execute();
console.log(`Counter value (private): ${plaintext}`);
```
## Next steps
* Explore [Adding FHE to an Existing Contract](/cofhe/tutorials/adding-fhe-to-existing-contract)
* Review [ACL Usage Examples](/cofhe/tutorials/acl-usage-examples) for more access control patterns
* Understand [Decryption Operations](/cofhe/fhe-library/core-concepts/decryption-operations) in detail
# Fhenix Developer Docs
Source: https://cofhe-docs.fhenix.zone/index
Guides, references, and tutorials for every Fhenix product.
Fhenix Developer Docs
Build with Fhenix
Guides, references, and tutorials for every Fhenix product. Pick a product to start.
# Trading
Source: https://cofhe-docs.fhenix.zone/trading/index
Trade confidential tokens at a public price, with hidden size.
## Coming soon
The Trading documentation is in progress.
For encrypted computation on Fhenix today, see the [CoFHE documentation](/cofhe).
# Choose a token
Source: https://cofhe-docs.fhenix.zone/wallet/balances-and-tokens/choose-a-token
Select the asset used by the action you are performing.
Select the asset used by the action you are performing. To open the selector, open the token dropdown in Shield, Unshield, or Send.
## Screen components
1. **Token rows.** Identify each asset by its icon and ticker.
2. **Type badges.** DUAL marks a dual-mode token. FHERC marks a confidential token counterpart.
3. **Balance and value.** Use the displayed balance to choose an asset. Available funds depend on whether the current action uses the public or private balance.
4. **Highlighted row.** Marks the selected asset.
5. **Collapse control.** Close the selector and return to the action form.
## Steps
Open the token dropdown.
Select the asset you want to use.
Recheck the amount, available balance, and receive summary after you change tokens.
The example list is not a supported-assets announcement. Use the assets available on the network in your current session.
# Dual token details
Source: https://cofhe-docs.fhenix.zone/wallet/balances-and-tokens/dual-token-details
Understand the public and private portions of one dual-mode token.
Understand the public and private portions of one dual-mode token. To open it, select a token with the DUAL badge in your portfolio.
## Screen components
1. **Back and token heading.** Return to the portfolio or identify the selected asset. The DUAL badge marks a token with public and private balances in the same token contract.
2. **Total value.** Review the displayed value for this token. It is a valuation, not a separate spendable balance.
3. **Private and public amounts.** Read the two amounts above the bar to see how your holdings are divided.
4. **Balance bar.** The shielded side is private and the crossed-shield side is public.
5. **Shield and Send.** Open the balance-mode flow or send this token.
6. **Information panel.** Read the supported actions for this token type.
7. **Resources.** Copy the displayed contract address or open the token website and whitepaper.
## Steps
Confirm the token name and how its balance divides between private and public.
Choose **Shield** to change the balance mode, or **Send** to transfer.
Check the selected mode and amount.
Moving tokens into the public balance makes that amount public. It does not make the remaining private balance public.
# Balances and tokens
Source: https://cofhe-docs.fhenix.zone/wallet/balances-and-tokens/index
Read your portfolio, token details, and the token selector in the Fhenix wallet module.
These screens show how much of each asset is private, public, or pending. Example balances and percentage changes in the screenshots are illustrative. Check the actual values in your session.
See your supported assets and how much of each is private, public, or pending.
Understand the public and private portions of one dual-mode token.
See a confidential FHERC20 token and its underlying public asset together.
Select the asset used by the action you are performing.
# Wrapped token details
Source: https://cofhe-docs.fhenix.zone/wallet/balances-and-tokens/wrapped-token-details
See a confidential FHERC20 token and its underlying public asset together.
See the confidential token and its underlying public asset together. To open it, select a token with the FHERC badge in your portfolio.
## Screen components
1. **Token heading and FHERC badge.** Identify the confidential wrapped token.
2. **Total value and balance bar.** Review the combined display and its private/public split.
3. **Shielded row.** Shows the confidential token balance.
4. **Public row.** Shows the corresponding public asset balance. Check the ticker, because the public and confidential tokens can use different names.
5. **Shield and Send.** Start a conversion or transfer for this asset.
6. **Token information.** Explains which confidential actions are supported.
7. **Resources.** The FHERC20 and ERC20 addresses identify different contracts. Use the copy control for the one you need.
## Steps
Confirm both token names and their balances.
Use **Shield** to move public assets into confidential form.
Use **Unshield** and complete **Claim** when you need the public asset again.
A wrapped token uses a confidential counterpart to a public asset. A DUAL token holds both modes within one token contract.
# Your portfolio
Source: https://cofhe-docs.fhenix.zone/wallet/balances-and-tokens/your-portfolio
See your supported assets and how much of each is private, public, or pending.
See your supported assets and understand how much is private, public, or still pending. To open it, connect your wallet and open the wallet panel. On your first connection, the wallet prompts you to create an ACP.
## Screen components
1. **Wallet header.** Check the connected address and network. Use the copy icon to copy the address. The header also contains privacy controls, Settings, and Close.
2. **Total value.** The summary shows a portfolio value and a change indicator. A masked value or N/A means some information is unavailable. It does not mean your balance is zero.
3. **Pinned asset.** The card above the main actions gives one asset a prominent place in the panel. It shows the token, its type badge, its balance and value, and its balance bar.
4. **Shield and Send.** Start shielding an asset or open the transfer screen.
5. **Tokens and Activity.** Switch between your asset list and recorded wallet activity.
6. **Token rows.** Each row shows a token, its type badge, balance, value, a balance bar, and a right arrow for token details.
7. **Balance bar.** The legend under each bar splits the holding into shielded (private), claim (an unshielded amount waiting to be claimed), and public. These percentages describe holdings, not yield.
## Steps
Confirm the address in the wallet header.
Open **Tokens** and select an asset for its details.
Use **Shield** or **Send**.
# Common questions
Source: https://cofhe-docs.fhenix.zone/wallet/faq
Short answers about masked balances, permits, shielding, unshielding, and uncertain transactions.
The wallet cannot currently show part of the balance. Check the Privacy Center for a missing or expired permit or a failed query. A masked balance is not a zero balance.
No. A viewing-permit signature authorizes access to encrypted information. Sending tokens is a separate transaction through your connected wallet.
No. Private amounts are encrypted, while addresses and public blockchain metadata remain visible.
Approval and Shield are separate steps when approval is required. Continue the shielding flow and check for its completion.
Unshield starts the request. Wait through In progress and complete Claim once the amount is ready.
No. The delegated view is read-only. It provides permitted visibility, not the holder's transaction-signing key.
No. Information a recipient has already viewed or saved cannot be erased by deleting a permit. Check the application's confirmation for the exact effect on future access.
Check Activity, your connected wallet, and the transaction's network explorer link. Verify its state before you submit the same action again.
# Get started
Source: https://cofhe-docs.fhenix.zone/wallet/get-started
Connect your wallet, authorize private viewing, and complete your first shield and unshield on Ethereum or Arbitrum.
This page takes you from a new connection to a funded private balance. The wallet works on Ethereum mainnet and Arbitrum. You need a wallet that holds ETH for gas on the network you use and at least one supported token.
## Connect your wallet
Open the app and select **Connect Wallet**, or choose one of the available wallet connectors. Choose a connection method and connect the account you want to use. Confirm the account and the network (Ethereum or Arbitrum) before you continue.
## Authorize private viewing
Create an Access Control Permit (ACP) and review the message in your connected wallet. Sign it to authorize your private-balance view. If you cancel, you can return to permit creation later. See [Create a viewing permit](/wallet/settings-and-permits/create-viewing-permit).
## Find your assets
Open **Tokens** and select an asset to review its public and private balances. Use a public balance for shielding or a public transfer. Use an available private balance for a private transfer or unshielding.
## Complete the whole flow
Shielding can include an approval before the shield transaction. Unshielding has three stages: Unshield, In progress, and Claim. Check the final state before you treat either operation as complete.
## Check the context before you sign
Before you sign, check the account, network, token, amount, and recipient or contract. Fees use the network's gas asset, so a token balance alone may not be enough to submit a transaction.
## Find the right guide
| If you need to | Read |
| - | - |
| Understand balances and tokens | [Balances and tokens](/wallet/balances-and-tokens) |
| Move assets | [Moving assets](/wallet/moving-assets) |
| Create or use viewing permits | [Create a viewing permit](/wallet/settings-and-permits/create-viewing-permit) |
| Fix a masked balance or failed read | [Troubleshooting](/wallet/troubleshooting) |
# Wallet Module
Source: https://cofhe-docs.fhenix.zone/wallet/index
What the Fhenix wallet module does, how it shows confidential balances, and what stays public.
The Fhenix wallet module is an interface for viewing and managing confidential tokens inside an application. Read this page to understand what the wallet shows, what it keeps private, and which guide to open next.
The wallet module works on Ethereum mainnet and Arbitrum. Screenshots in this guide contain example values and placeholders. Use the addresses, assets, fees, and status shown in your own session.
## Why this wallet exists
Confidential tokens keep balance information encrypted onchain. Your connected wallet can sign transactions, but a standard balance display may not show those encrypted holdings. The wallet module gives you a dedicated interface to view and manage supported confidential assets inside the application.
## How it works
Connect your wallet, then sign a viewing permit when the wallet module asks. The module uses that authorization to retrieve and display your permitted private information. Your connected wallet remains the signer for token transactions. Viewing a private balance does not publish that balance onchain.
## What you can do
* Review your portfolio.
* Inspect public, private, and pending balances.
* Shield tokens, and unshield and claim them.
* Send tokens.
* Inspect recorded activity.
* Manage permits, shared views, and token allowances.
## What confidentiality means
Private balances and private transfer amounts are encrypted. Wallet addresses and public blockchain metadata remain visible. Public balances and public transfer amounts are visible onchain. Shielding or unshielding crosses the public/private boundary and can expose the converted amount.
## Two token types
The wallet supports two kinds of confidential token. Read both token names and contract addresses before you convert or send.
* **DUAL tokens** hold a public balance and a private balance in one token contract. See [dual-mode tokens](/cofhe/fhe-library/confidential-contracts/dual-mode/overview).
* **FHERC20 tokens** provide a confidential counterpart to a public asset. See [FHERC20 tokens](/cofhe/fhe-library/confidential-contracts/fherc20/overview).
## Three kinds of authorization
The wallet asks you to authorize three different things. They serve different purposes, and renewing one does not repair the others.
| Authorization | What it allows |
| - | - |
| Access permit | Viewing your encrypted data |
| Allowance | Token use by an address or contract |
| Transaction signature | The specific transaction shown in your connected wallet |
## Where to go next
Connect your wallet, sign a permit, and find your assets.
Portfolio, token details, and the token selector.
Shield, unshield, claim, send, and view activity.
Settings, access permits, and viewing permits.
Privacy Center, missing permits, and decryption failures.
Short answers about balances, permits, and transactions.
# Claim unshielded tokens
Source: https://cofhe-docs.fhenix.zone/wallet/moving-assets/claim-unshielded-tokens
Complete an unshielding request once its tokens are ready to claim.
Complete an unshielding request once its tokens are ready to claim. To open it, open the token details and look for **Claim now** and the claimable amount.
## Screen components
1. **Balance split.** Review the private, public, and pending portions of the token.
2. **Shielded and public rows.** Distinguish the confidential balance from the public balance.
3. **Claimable amount.** Shows the amount awaiting the final claim action.
4. **Claim now.** Starts the final step for the available amount.
5. **Other actions.** Shield and Send remain separate actions. Starting another action does not replace Claim.
## Steps
**Claim now** appears when the amount is available.
Confirm the claimable asset and amount.
Select **Claim now** and complete any wallet confirmation.
Wait for completion, then check the public balance and activity.
The dual-token detail screen also has a **Claim now** action. A pending amount is not a completed claim.
# Moving assets
Source: https://cofhe-docs.fhenix.zone/wallet/moving-assets/index
Shield, unshield, claim, and send confidential tokens, and check the result in Activity.
These screens move value between public and private form, and between wallets. Fees in the screenshots are examples, not fixed charges.
Move an amount from a public balance into confidential form.
Move a private amount back into public form through Unshield, In progress, and Claim.
Complete an unshielding request once its tokens are ready to claim.
Transfer a supported token using either its private or public balance.
Review actions recorded by the wallet and inspect their transaction references.
# Send tokens
Source: https://cofhe-docs.fhenix.zone/wallet/moving-assets/send-tokens
Transfer a supported token using either its private or public balance.
Transfer a supported token using either its private or public balance. To open it, choose **Send** from the portfolio or token details.
## Screen components
1. **Send and Receive tabs.** Keep Send selected to make a transfer.
2. **Privacy mode.** The shield controls and the Private transaction label show the selected transfer mode. Check it before you continue.
3. **Recipient address.** Paste the destination wallet address and verify it.
4. **Amount and token.** Choose the asset and amount. The balance shown corresponds to the selected mode.
5. **Amount shortcuts.** Use a percentage or max to fill the amount, then recheck it.
6. **Recipient receives.** Review the destination asset and amount.
7. **Send and fee.** Continue to your connected wallet and review its transaction request.
## Steps
Choose private or public transfer mode.
Enter the recipient, token, and amount.
Check **Recipient receives** and the fee.
Select **Send**, confirm in your wallet, and monitor completion in Activity.
Private transfers protect the amount. They do not make wallet addresses anonymous. Public transfers expose their amounts onchain.
# Shield tokens
Source: https://cofhe-docs.fhenix.zone/wallet/moving-assets/shield-tokens
Move an amount from a public balance into confidential form.
Move an amount from a public balance into confidential form. To open it, choose **Shield** from the portfolio or token details.
## Screen components
1. **Shield and Unshield tabs.** Select Shield for public-to-private conversion.
2. **You deposit.** Enter the amount and choose the token. The balance label helps you check available funds.
3. **Amount shortcuts.** 25%, 50%, 75%, and max fill the amount using the available balance.
4. **You Receive.** Review the confidential asset and amount you receive.
5. **Privacy notice.** Explains that the resulting balance is encrypted and viewable with the appropriate permission.
6. **Progress tracker.** Follow the required steps. The wrapped-token flow shown here has Approve, Shield, and Done.
7. **Shield button and fee.** Start or continue the flow. Review the wallet request and the current fee estimate.
## Steps
Select **Shield**, a token, and an amount.
Check **You Receive** and the fee.
If the wallet requests an approval, confirm the token approval in your connected wallet.
Continue with **Shield** and wait for completion.
Approval authorizes token use. It does not shield tokens. Follow every required step.
# Unshield tokens
Source: https://cofhe-docs.fhenix.zone/wallet/moving-assets/unshield-tokens
Move a private amount back into public form through Unshield, In progress, and Claim.
Move a private amount back into public form. Unshielding has three stages: Unshield, In progress, and Claim. To open it, open **Shield / Unshield** and select **Unshield**.
## Screen components
1. **Unshield tab.** Check that Unshield is selected before you enter an amount.
2. **You unshield.** Choose the confidential asset and enter an amount within its available private balance.
3. **Amount shortcuts.** Use 25%, 50%, 75%, or max to fill the amount.
4. **You Receive.** Check the public asset and amount shown by the live application.
5. **Privacy notice.** Unshielding exposes the selected amount as a public balance. This action is visible onchain.
6. **Three-stage tracker.** Unshield starts the request. In progress means processing is underway. Claim is the final action.
7. **Unshield button and fee.** Submit the request through your connected wallet after you review its details.
## Steps
Select the token and amount, and review the public output.
Confirm **Unshield** in your connected wallet.
Processing continues until the amount is ready.
When Claim is available, complete it to receive the public balance.
A confirmed Unshield request is not the end of this flow. Complete Claim before you treat the amount as an available public balance.
# View activity
Source: https://cofhe-docs.fhenix.zone/wallet/moving-assets/view-activity
Review actions recorded by the wallet and inspect their transaction references.
Review actions recorded by the wallet and inspect their transaction references. To open it, select **Activity** next to **Tokens** in the portfolio.
## Screen components
1. **Tokens and Activity tabs.** Switch between balances and recorded actions.
2. **Action label.** Identifies the kind of action on each row.
3. **Status.** Shows the reported state of that action.
4. **Time.** Shows when the action occurred or how long ago it was recorded.
5. **Token amount and value.** Shows the amount and any available value for that entry.
6. **Transaction reference.** Use the copy control or external link to inspect the transaction in the network explorer.
## Steps
Open Activity and locate the relevant action.
Check its status and time.
Use its transaction reference for further inspection.
For an unshielding request, return to token details and complete Claim when it is ready.
A missing entry does not prove that a transaction did not occur. Check the connected wallet and the transaction reference.
# Create a viewing permit
Source: https://cofhe-docs.fhenix.zone/wallet/settings-and-permits/create-viewing-permit
Authorize access to your encrypted data using a signed permit.
Authorize access to your encrypted data using a signed permit. To open it, select **New** in the Generated permits section.
## Screen components
1. **Permit explanation.** Describes the signature that proves control of the connected wallet.
2. **Permit Name.** Add an optional name so you can recognize this permit later.
3. **Set Expiration.** Choose a preset or enter a custom duration.
4. **Generate New.** Open the permit-signing request in your connected wallet.
## Steps
Confirm that the correct wallet is connected.
Name the permit and choose how long it lasts.
Select **Generate New** and review the signature request.
Sign to create the permit, then return to your balances.
Creating a viewing permit is a message-signing step, not a token transfer. Review the requested access and duration before you sign.
# Settings and permits management
Source: https://cofhe-docs.fhenix.zone/wallet/settings-and-permits/index
Open Settings, review the permits that authorize viewing encrypted data, and create a new viewing permit.
Use Settings to open the Privacy Center, manage access permits, or find documentation. A permit is a signed authorization that lets the wallet display your encrypted data. These pages explain how to review and create permits.
Open the wallet privacy tools, permit management, and help resources from one menu.
Review the permits used to view encrypted data.
Authorize access to your encrypted data using a signed permit.
# Manage access permits
Source: https://cofhe-docs.fhenix.zone/wallet/settings-and-permits/manage-access-permits
Review the permits used to view encrypted data.
Review the permits used to view encrypted data. To open the Permits screen, use the wallet controls or the Privacy Center.
## Screen components
1. **Generated.** Contains your own permits. **New** opens permit creation.
2. **Status and default pin.** Active and Expired identify permit states. The pin marks the default permit.
3. **Renew and remove controls.** Renew an expired permit or review the removal action for a listed permit.
## Steps
Choose **Generated**, **Delegated**, or **Received**.
Open the relevant permit or use its available action.
Check the issuer, recipient, status, and expiration before you use or share access.
# Settings
Source: https://cofhe-docs.fhenix.zone/wallet/settings-and-permits/settings
Open the wallet privacy tools, permit management, and help resources from one menu.
Open the wallet's privacy tools, permit management, and help resources from one menu. To open it, select the **Settings** gear in the wallet header.
## Screen components
1. **Back and Settings heading.** Identify the settings menu and use the back arrow to return to the previous screen.
2. **Privacy Center.** Open the access-permit status, errors affecting balances or transactions, and token allowances. Use this screen when a private balance is masked or a request fails.
3. **ACP Management.** Review permits, or start creating a new viewing permit. See [Manage access permits](/wallet/settings-and-permits/manage-access-permits).
4. **Documentation.** Open the wallet's linked help documentation.
5. **Wallet header.** Check the connected address before you manage access. The copy control copies the address. Close dismisses the wallet panel.
## Steps
Select the Settings gear in the wallet header.
Choose **Privacy Center** to investigate a balance or transaction error, **ACP Management** to manage viewing permits, or **Documentation** for help.
On the destination screen, check the affected account, permit, token, or action.
Use **Back** to return to the wallet and check the result.
Settings is a navigation menu, not a transaction or permit-signing step. Opening ACP Management does not create or renew a permit. Viewing permits and token allowances authorize different things.
# Troubleshooting
Source: https://cofhe-docs.fhenix.zone/wallet/troubleshooting/index
Use the Privacy Center to fix missing permits, expired access, timed-out balance reads, and decryption failures.
Most wallet problems come from a missing or expired viewing permit, a timed-out balance request, or too little gas. Start in the Privacy Center, which lists each error and its fix.
Check viewing permissions, token allowances, and errors in one place.
Restore your private-balance view when a permit is missing or unusable.
Find and address a failed private-balance read.
# Resolve decryption failures
Source: https://cofhe-docs.fhenix.zone/wallet/troubleshooting/resolve-decryption-failures
Find and address a failed private-balance read.
Find and address a failed private-balance read. To open it, select **Privacy Center** in the Decryption Failed banner.
## Screen components
1. **Masked private value.** Shows that part of the balance could not be displayed.
2. **Decryption Failed banner.** Distinguishes a failed read from a successful balance display.
3. **Privacy Center action.** Opens details that help identify the affected request and error.
4. **Inline token error.** Each affected row shows a Decryption failed message in place of its balance bar, and its percentages appear as asterisks.
5. **Close control.** Dismisses the notice without resolving the underlying failure.
## Steps
Open the Privacy Center and read the specific error.
Check the active permit and the connected account.
Renew the permit if required, or use the retry control for a timed-out request.
Recheck the balance. If the error persists, record the token, network, error, and time for support.
A failed read does not establish that funds were lost or a transfer failed. Check transaction status separately before you repeat a send.
# Restore missing private balances
Source: https://cofhe-docs.fhenix.zone/wallet/troubleshooting/restore-missing-private-balances
Restore your private-balance view when a permit is missing or unusable.
Restore your private-balance view when a permit is missing or unusable. To open it, follow the **Permit missing** banner or an inline **Renew permit to decrypt** message.
## Screen components
1. **Masked total.** The private portion appears as a placeholder while any available public value stays visible.
2. **Permit missing banner.** Explains that a permit is needed to display private balances.
3. **Privacy Center button.** Opens the place to create or renew the permit.
4. **Token rows.** Masked amounts and a permit placeholder bar identify balances that cannot currently be displayed. The shielded, claim, and public labels stay in place without values.
5. **Dismiss control.** Closes the banner. It does not create or renew a permit.
## Steps
Confirm your connected account and network.
Select **Privacy Center**.
Create a permit if none is available, or renew the expired permit.
Complete the message signature and return to the portfolio.
A masked amount is not zero. It means the wallet cannot currently show that private amount. Do not resend a transfer only because its balance is hidden.
# Use the Privacy Center
Source: https://cofhe-docs.fhenix.zone/wallet/troubleshooting/use-the-privacy-center
Check viewing permissions, token allowances, and errors in one place.
Check viewing permissions, token allowances, and errors in one place. To open it, use the wallet privacy controls or an error banner.
## Screen components
1. **ACP status.** Shows the reported access-permit status. Check the individual error rows as well as the summary.
2. **Errors and Bugs.** Each row names the affected balance or action and its error, such as ACP Expired, Time out, or Not enough gas.
3. **Retry controls.** A retry icon appears for retryable failures such as a timed-out balance request.
4. **Permits.** Review active and expired permits. Use **Renew** when your viewing authorization needs renewal.
5. **Allowance.** Review token-use permissions separately from viewing permits. Rows show an asset, address, public/private label, limit, and any expiration.
6. **Allowance actions.** Use the available action to review removal or renewal. Read its confirmation and any connected-wallet request.
## Steps
Find the affected token or action in the error list.
Renew a missing or expired viewing permit, retry a timed-out query, or add gas for a gas error.
Return to the wallet and check the affected balance or action again.
Permits control access to encrypted information. Allowances authorize token use by an address or contract. Renewing one does not necessarily repair the other.