Only this pageAll pages
Powered by GitBook
1 of 35

0.4

Loading...

Getting Started

Loading...

Loading...

Loading...

Loading...

Fundamentals

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Guides

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Tutorials

Loading...

References

Loading...

Loading...

Developer

Loading...

Loading...

Welcome to fhEVM

fhEVM is a technology that enables confidential smart contracts on the EVM using Fully Homomorphic Encryption (FHE).

Get started

Learn the basics of fhEVM, set it up, and make it run with ease.

Develop a fhEVM smart contract

Start developing fhEVM smart contracts in Solidity by exploring its core features, discovering essential guides, and learning more with user-friendly tutorials.

Explore more

Access to additional resources and join the Zama community.

References

Refer to the API and access additional resources for in-depth explanations while working with fhEVM.

  • API function specifications

  • Repositories

Supports

Ask technical questions and discuss with the community. Our team of experts usually answers within 24 hours in working days.

  • Community forum

  • Discord channel

  • Telegram

Developers

Collaborate with us to advance the FHE spaces and drive innovation together.

  • Contribute to fhEVM

  • Follow the development roadmap

  • See the latest test release note

  • Request a feature

  • Report a bug


We value your feedback! Take a 5-question developer survey to improve the fhEVM library and the documentation and help other developers use FHE.

What is fhEVM

Bring confidential smart contracts to your blockchain with Zama's fhEVM

There used to be a dilemma in blockchain: keep your application and user data on-chain, allowing everyone to see it, or keep it privately off-chain and lose contract composability. Thanks to a breakthrough in homomorphic encryption, Zama’s fhEVM makes it possible to run confidential smart contracts on encrypted data, guaranteeing both confidentiality and composability.

Build confidential dapps just as you would regular ones

fhEVM contracts are simple solidity contracts that are built using traditional solidity toolchains. ‍Developers can use the euint data types to mark which part of their contracts should be private. ‍All the logic for access control of encrypted states is defined by developers in their smart contracts.

Use cases

  • Tokenization: Swap tokens and RWAs on-chain without others seeing the amounts.

  • Blind auctions: Bid on items without revealing the amount or the winner.

  • On-chain games: Keep moves, selections, cards, or items hidden until ready to reveal.

  • Confidential voting: Prevents bribery and blackmailing by keeping votes private.

  • Encrypted DIDs: Store identities on-chain and generate attestations without ZK.

  • Private transfers: Keep balances and amounts private, without using mixers.

Tutorials and examples

  • 🎥 Workshop during ETHcc [by Morten Dahl — Zama]

  • 🎥 How to Write Confidential Smart Contracts Using Zama's fhEVM [by Clément Danjou (Zama)]

  • 📃 Programmable Privacy and Onchain Compliance using Homomorphic Encryption [by Rand Hindi and Clément Danjou — Zama]

  • 📃 Confidential ERC-20 Tokens Using Homomorphic Encryption [by [Clément Danjou — Zama]

  • 📃 On-chain Blind Auctions Using Homomorphic Encryption [by Clément Danjou — Zama]

  • 🖥️ ERC-20

  • 🖥️ Blind Auction

  • 🖥️ Governor DAO

  • 🖥️ Battleship [by Owen Murovec]

  • 🖥️ Darkpool [by Owen Murovec]

Connecting to Zama Devnet

Here are the main steps from the provided by Metamask:

Add these information to access to blockchain

Fields
Value
Fields
Value

Network Name

Zama Network

New RPC URL

https://devnet.zama.ai

Chain ID

8009

Currency symbol

ZAMA

Block explorer URL (Optional)

https://main.explorer.zama.ai

Network Name

Zama Local

New RPC URL

http://localhost:8545/

Chain ID

9000

Currency symbol

ZAMA

Block explorer URL (Optional)

official guide
How to add network from popup
From the homepage of your wallet, click on the network selector in the top left, and then on 'Add network'
How to add network
MetaMask will open in a new tab in fullscreen mode. From here, find and the 'Add network manually' button at the bottom of the network list.
How to select correct network on Metamask
Choose the Zama Devnet

What is fhEVM

Understand the basic concepts of fhEVM library.

Using Zama Faucet

Follow the step-by-step guide to use Zama Faucet

Connecting to Zama devnet

Get 10 Zama token to start working with fhEVM

Local dev node

Use our docker image to spin up a fhEVM node for local development

Fundamentals

Explore core features.

  • Write contract

  • Use encrypted types

Guides

Deploy your project.

  • Gas estimation

  • Common pitfalls and best practices

Tutorials

Learn more with tutorials.

  • Start here

  • Go further

Cover
Cover
Cover
Cover
Cover
Cover
Cover

Other development environment

Foundry

The fhEVM does not work with Foundry as Foundry employs its own EVM, preventing us from incorporating a mock for our precompiled contract. An is exploring the possibility of incorporating a plugin system for precompiles, which could potentially pave the way for the utilization of Foundry at a later stage.

However, you could still use Foundry with the mocked version of the fhEVM, but please be aware that this approach is NOT recommended, since the mocked version is not fully equivalent to the real fhEVM node's implementation (see warning in ). In order to do this, you will need to rename your TFHE.sol imports from fhevm/lib/TFHE.sol to fhevm/mocks/TFHE.sol in your solidity source files.

Using Zama Faucet

You can get 10 Zama token on .

ongoing discussion
hardhat
https://faucet.zama.ai/

Write contract

Usage

Our library compiles seamlessly with the traditional Solidity compiler and is generally compatible with traditional Solidity tools. However, it's important to note that the execution is designed to function exclusively on a fhEVM. As a result, this library is not intended for deployment on a classic EVM, such as Goerli or Ganache.

Installation

To get started with fhEVM Solidity Library, you need to install it as a dependency in your JavaScript project. You can do this using npm (Node Package Manager) or Yarn. Open your terminal and navigate to your project's directory, then run one of the following commands:

# Using npm
npm install fhevm

# Using Yarn
yarn add fhevm

# Using pnpm
pnpm add fhevm

This will download and install the fhEVM Solidity Library and its dependencies into your project.

Typical workflow for writing confidential smart contracts

1/ For quick prototyping of a specific feature, use the Zama version of the Remix IDE. This will let you quickly deploy a contract on the devnet via Metamask, and interact easily with it through the Remix UI. Otherwise, for a bigger project, you should use our custom fhevm-hardhat-template repository. Hardhat is a popular development environment for Solidity developers and will let you test and deploy your contracts to the fhEVM using TypeScript.

2/ A good first step is to start with an unencrypted version of the contract you want to implement, as you would usually do on a regular EVM chain. It is easier to reason first on cleartext variables, before thinking on how to add confidentiality.

3/ When you're ready, you can start to add confidentiality by using the TFHE solidity library. Typically, this would involve converting some uintX types to euintX, as well as following all the detailed advices that we gave in the pitfalls to avoid and best practises section of the documentation. For inspiration, you can take a look at the examples inside the fhevm repository. If you're using the Hardhat template, read the advices that we gave in the Hardhat section.

How can I break a loop ?

❌ In FHE, it is not possible to break a loop based on an encrypted condition. For example, this would not work:

euint8 maxValue = TFHE.asEuint(6); // Could be a value between 0 and 10
euint8 x = TFHE.asEuint(0);
// some code
while(TFHE.lt(x, maxValue)){
    x = TFHE.add(x, 2);
}

If your code logic requires looping on an encrypted boolean condition, we highly suggest to try to replace it by a finite loop with an appropriate constant maximum number of steps and use TFHE.select inside the loop.

✅ For example, the previous code could maybe be replaced by the following snippet:

euint8 maxValue = TFHE.asEuint(6); // Could be a value between 0 and 10
euint8 x;
// some code
for (uint32 i = 0; i < 10; i++) {
    euint8 toAdd = TFHE.select(TFHE.lt(x, maxValue), 2, 0);
    x = TFHE.add(x, toAdd);
}

In this snippet, we perform 10 iterations, adding 4 to x in each iteration as long as the iteration count is less than maxValue. If the iteration count exceeds maxValue, we add 0 instead for the remaining iterations because we can't break the loop.

Generate random number

Random encrypted integers can be generated fully on-chain.

That can only be done during transactions and not on an eth_call RPC method, because PRNG state needs to be mutated on-chain during generation.

WARNING: Not for use in production! Currently, integers are generated in the plain via a PRNG whose seed and state are public, with the state being on-chain. An FHE-based PRNG is coming soon, where the seed and state will be encrypted.

Example

euint8 r8 = TFHE.randEuint8();
euint16 r16 = TFHE.randEuint16();
euint32 r32 = TFHE.randEuint32();
euint64 r64 = TFHE.randEuint64();

Branching in FHE

The result of is of type ebool. Typical boolean operations are not supported for this type, because it is an encrypted boolean.

Condition with encrypted boolean

fhEVM provides a method which acts as a ternary operator on encrypted integers. This method is called .

It is important to keep in mind that each time we assign a value using TFHE.select, the value changes, even if the plaintext value remains the same.

Error handling

If a condition is not satisfied, the transaction will not be reverted, potentially posing a challenge when attempting to communicate issues to users. A recommended approach to address this is by implementing an error handler in which the contract stores the latest error information for all wallets.

Encrypt an input

The library provides a set of functions to encrypt integers of various sizes (Bool, 4, 8, 16, 32 and 64 bits) using the blockchain's public key. These encrypted integers can then be securely used as parameters for smart contract methods within the blockchain ecosystem.

Use the CLI

fhevmjs include a Command-Line Interface (CLI) tool. With this handy utility, you can encrypt 8/16/32bits integer with the blockchain's FHE public key. To get started with fhevmjs CLI, first, ensure you have Node.js installed on your system. Next, install the fhevmjs package globally using the '-g' flag, which allows you to access the CLI tool from any directory:

Once installed, fhevm command should be available. You can get all commands and options available with fhevm help or fhevm encrypt help.

Examples

Encrypt 71721075 as 32bits integer:

Contributing

There are two ways to contribute to the Zama fhEVM:

  • to report bugs and typos, or to suggest new ideas

  • Request to become an official contributor by emailing .

Becoming an approved contributor involves signing our Contributor License Agreement (CLA)). Only approved contributors can send pull requests, so please make sure to get in touch before you do!

Repositories

Implementation

Libraries

const instance = getInstance();

const encryptedParamBool = instance.encryptBool(true);
const encryptedParam4 = instance.encrypt4(3);
const encryptedParam8 = instance.encrypt8(14);
const encryptedParam16 = instance.encrypt16(650);
const encryptedParam32 = instance.encrypt32(71721057);
const encryptedParam64 = instance.encrypt64(71721075);
npm install -g fhevmjs
fhevm encrypt --node devnet.zama.ai 32 71721075
function bid(bytes calldata encryptedBid) internal {
  euint32 bid = TFHE.asEuint32(encryptedBid);
  ebool isAbove = TFHE.le(bid, highestBid);

  // Replace highest bid
  highestBid = TFHE.select(isAbove, bid, highestBid);
}
struct LastError {
  euint8 error;
  uint timestamp;
}

euint8 internal NO_ERROR;
euint8 internal NOT_ENOUGH_FUND;

constructor() {
  NO_ERROR = TFHE.asEuint8(0);
  NOT_ENOUGH_FUND = TFHE.asEuint8(1);
}

function setLastError(euint8 error, address addr) private {
  _lastErrors[addr] = LastError(error, block.timestamp);
  emit ErrorChanged(addr);
}

function _transfer(address from, address to, euint32 amount) internal {
  // Make sure the sender has enough tokens.
  ebool canTransfer = TFHE.le(amount, balances[from]);
  setLastError(TFHE.select(canTransfer, NO_ERROR, NOT_ENOUGH_FUND), msg.sender);

  // Add to the balance of `to` and subract from the balance of `from`.
  balances[to] = balances[to] + TFHE.select(canTransfer, amount, TFHE.asEuint32(0));
  balances[from] = balances[from] - TFHE.select(canTransfer, amount, TFHE.asEuint32(0));
}
comparison operations
select
Open issues
[email protected]
fhEVM on evmos
Solidity library
fhevmjs

Build with Node

First, you need to install the library.

# Using npm
npm install fhevmjs

# Using Yarn
yarn add fhevmjs

# Using pnpm
pnpm add fhevmjs

fhevmjs uses ESM format for web version and commonjs for node version. You need to set the type to "commonjs" in your package.json to load the correct version of fhevmjs. If your node project use "type": "module", you can force the loading of the Node version by using import { createInstance } from 'fhevmjs/node';

Create an instance

const { createInstance, getPublicKeyCallParams } = require("fhevmjs");
const { ethers, JsonRpcProvider } = require("ethers");

const provider = new JsonRpcProvider(`https://devnet.zama.ai/`);

const createFhevmInstance = async () => {
  // 1. Get the chain id
  const network = await provider.getNetwork();
  const chainId = +network.chainId.toString();
  // 2. Fetch the FHE public key from the blockchain
  const ret = await provider.call(getPublicKeyCallParams());
  const decoded = ethers.AbiCoder.defaultAbiCoder().decode(["bytes"], ret);
  const publicKey = decoded[0];

  // 3. Create the instance
  return createInstance({ chainId, publicKey });
};
createFhevmInstance().then((instance) => {
  console.log(instance);
});

You can now use your instance to encrypt parameters or do a reencryption.

Common webpack errors

"Module not found: Error: Can't resolve 'tfhe_bg.wasm'"

In the codebase, there is a new URL('tfhe_bg.wasm') which triggers a resolve by Webpack. If you encounter an issue, you can add a fallback for this file by adding a resolve configuration in your webpack.config.js:

resolve: {
  fallback: {
    'tfhe_bg.wasm': require.resolve('tfhe/tfhe_bg.wasm'),
  },
},

ReferenceError: Buffer is not defined

If you encounter this issue with the Node Buffer object, you should offer an alternative solution. Similar issues might arise with different Node objects. In such cases, install the corresponding browserified npm package and include the fallback as follows.

resolve: {
  fallback: {
    buffer: require.resolve('buffer/'),
    crypto: require.resolve('crypto-browserify'),
    stream: require.resolve('stream-browserify'),
    path: require.resolve('path-browserify'),
  },
},

Issue with importing ESM version

With a bundler such as Webpack or Rollup, imports will be replaced with the version mentioned in the "browser" field of the package.json. If you encounter issue with typing, you can use this tsconfig.json using TypeScript 5.

If you encounter any other issue, you can force import of the browser package.

import { initFhevm, createInstance } from "fhevmjs/web";

Use bundled version

If you have an issue with bundling the library (for example with some SSR framework), you can use the prebundled version available in fhevmjs/bundle. Just embed the library with a <script> tag and you're good to go.

const start = async () => {
  await window.fhevm.initFhevm(); // load wasm needed
  const instance = window.fhevm.createInstance({ chainId, publicKey }).then((instance) => {
    console.log(instance);
  });
};

Reencryption

How it's working

The reencryption process involves converting a ciphertext that was encrypted with the FHE blockchain key into one that is encrypted with the NaCl public key generated by the client. First, the user needs to provide two elements:

  • The NaCl box private/public key pair is generated by the client library (fhevmjs).

  • A signature of an EIP-712 object that contains the public key and the contract authorized to initiate a reencryption request.

Then, the contract verifies the signature by generating the EIP-712 with the provided public key and contract address. The signature must match the msg.sender. If the signature match, the contract can return the reencryption of the value with the provided public key

Note that this mechanism grants the application (whether web-based or node-based) access to all the ciphertext made available by the mentioned contract for the user.

Generate your typed data structure and call the method

You can generate the data to sign by using the generatePublicKey method. To sign, we'll use the eth_signTypedData_v4 method available on wallets.

const instance = getInstance();
const reencryption = instance.generatePublicKey("0x1c786b8ca49D932AFaDCEc00827352B503edf16c"); // The allowed contract address
const params = [userAddress, JSON.stringify(reencryption.eip712)];
const sign = await window.ethereum.request({
  method: "eth_signTypedData_v4",
  params,
});

// We call the balanceOf method to get the reencrypt balance
const encryptedBalance = await contract.balanceOf(reencryption.publicKey, sign);

Write the Solidity code

Getting a reencryption needs to be implemented into the contract and on the client side. We'll take the balanceOf method of an ERC-20 contract.

This view function needs to validate the user to prevent anyone to reencrypt any user's balance. To prevent this, the user provides a signature of the given public key and the contract address. Since this is something very useful, fhEVM library provide an abstract to use in your contract:

import "fhevm/abstracts/Reencrypt.sol";

contract EncryptedERC20 is Reencrypt {
  function balanceOf(
    bytes32 publicKey,
    bytes calldata signature
  ) public view onlySignedPublicKey(publicKey, signature) returns (bytes memory) {
    return TFHE.reencrypt(balances[msg.sender], publicKey, 0);
  }
}

The modifier onlySignedPublicKey(publicKey, signature) will verify the signature of the user.

Decrypt the reencryption

When generating the public key using generatePublicKey, the corresponding private key is kept by the fhEVM instance and linked to the specified contract. To decrypt a value using the user's private key, you only need to provide the contract address and the encrypted value.

const encryptedBalance = await contract.balanceOf(reencryption.publicKey, sign);
const balance = instance.decrypt("0x1c786b8ca49D932AFaDCEc00827352B503edf16c", encryptedBalance);

Contracts standard library

Getting Started

Installation

# Using npm
npm install fhevm-contracts

# Using Yarn
yarn add fhevm-contracts

# Using pnpm
pnpm add fhevm-contracts

A Simple Example

// SPDX-License-Identifier: BSD-3-Clause-Clear

pragma solidity ^0.8.20;

import "fhevm/lib/TFHE.sol";
import "fhevm-contracts/contracts/token/ERC20/EncryptedERC20.sol";

contract MyERC20 is EncryptedERC20 {
  constructor() EncryptedERC20("MyToken", "MYTOKEN") {
    _mint(1000000, msg.sender);
  }
}

Available contracts

  • EncryptedERC20

  • DAO

  • EncryptedErrors

Using Hardhat

The best way to start writing smart contracts with fhEVM is to use our Hardhat template.

It allows you to start a fhEVM docker image and run your smart contract on it. Read the README for more information.

When developing confidential contracts, we recommend to use first the mocked version of fhEVM for faster testing with pnpm test:mock and coverage computation via pnpm coverage:mock, this will lead to a better developer experience. However, keep in mind that the mocked fhEVM has some limitations and discrepancies compared to the real fhEVM node, as explained in the warning section at the end of this section.

It's essential to run tests of the final contract version using the real fhEVM. You can do this by running pnpm test before deployment.

Mocked mode

For faster testing iterations, instead of launching all the tests on the local fhEVM node via pnpm testor npx hardhat test which could last several minutes, you could use instead a mocked version of the TFHE.sol library. The same tests should (almost always) pass, as is, without any modification: neither the javascript files neither the solidity files need to be changed between the mocked and the real version. The mocked mode does not actually use encryption for encrypted types and runs the tests on a local hardhat node which is implementing the original EVM (i.e non-fhEVM).

To run the mocked tests use either:

pnpm test:mock

Or equivalently:

HARDHAT_NETWORK=hardhat npx hardhat test --network hardhat

In mocked mode, all tests should pass in few seconds instead of few minutes, allowing a better developer experience.

Furthermore, getting the coverage of tests is only possible in mocked mode. Just use the following command:

pnpm coverage:mock

Or equivalently:

HARDHAT_NETWORK=hardhat npx hardhat coverage-mock --network hardhat

Then open the file coverage/index.html. This will allow increased security by pointing out missing branches not covered yet by the current test suite.

⚠️ Warning : Notice that, due to intrinsic limitations of the original EVM, the mocked version differ in few corner cases from the real fhEVM, the most important change is the TFHE.isInitialized method which will always return true in the mocked version. Another big difference in mocked mode, compared to the real fhEVM implementation, is that there is no ciphertext verification neither checking that a ciphertext has been honestly obtained in the mocked version (see section 4 of the whitepaper). This means that before deploying to production, developers still need to run the tests with the original fhEVM node, as a final check in non-mocked mode, with pnpm test or npx hardhat test.

Using Remix

You can use Remix to interact with a blockchain using fhEVM. If you want to send an encrypted input, you need to encrypt it with fhevmjs CLI tool for example. It becomes more complex if you want to reencrypt a value directly in Remix.

To avoid this problem, we developed a version of Remix IDE with these two missing features:

  • Encryption of input

  • Generation of public key and signature for reencryption and decryption.

You can use it on https://remix.zama.ai.

Usage

To import TFHE library, simply import it at the top of your contract.

import "fhevm/lib/TFHE.sol";

Be sure to be on the correct network before deploying your contract

Choose the Zama Devnet
Choose "Injected Provider - Metamask"

````

See all tutorials

Start here

  • - October 30, 2023

Go further

Code examples in GitHub

  • : A variation of the standard ERC20 smart contract that incorporates encrypted balances, providing additional privacy for token holders. -

  • : A smart contract that enables anonymous trading of cryptocurrencies or assets, typically used to execute large orders without affecting the market price.[by ]

  • : A Hardhat-based template for developing Solidity smart contracts, with sensible defaults. - by Clément Danjou [by Clément Danjou]

  • : A smart contract that replicates the classic Battleship game on a blockchain in a transparent manner. [by ]

  • : A DAO smart contract that facilitates governance decisions through encrypted voting

  • : A smart contract for conducting blind auctions where bids are encrypted and the winning bid remains private.

  • : A blockchain-based identity management system using smart contracts to store and manage encrypted personal data.

Blog tutorials:

  • - February 29, 2024

  • -November 23, 2023

  • - October 12, 2023

  • - July 10, 2023

  • - June 28, 2023

Video tutorials:

  • - January 30, 2024

  • - October 27, 2023

  • - July 18, 2023

[Video tutorial] How to Write Confidential Smart Contracts Using Zama's fhEVM
ERC-20
Darkpool
Owen Murovec
Cipherbomb
Battleship
Owen Murovec
Governor DAO
Blind Auction
Decentralized ID
Build an Encrypted Wordle Game Onchain using FHE and Zama's fhEVM
Programmable Privacy and Onchain Compliance using Homomorphic Encryption
Confidential DAO Voting Using Homomorphic Encryption
On-chain Blind Auctions Using Homomorphic Encryption and the fhEVM
Confidential ERC-20 Tokens Using Homomorphic Encryption and the fhEVM
Accelerate your code testing and get code coverage using fhEVM mocks
Use the CMUX operator on Zama’s fhEVM
Workshop during ETHcc: Homomorphic Encryption in the EVM

Build a web application

Using a template

fhevmjs is working out of the box and we recommend you to use it. We also provide three GitHub templates to start your project with everything set.

React + TypeScript

You can use this template to start an application with fhevmjs, using Vite + React + TypeScript.

VueJS + TypeScript

You can also use this template to start an application with fhevmjs, using Vite + Vue + TypeScript.

NextJS + Typescript

You can also use this template to start an application with fhevmjs, using Next + TypeScript.

Using directly the library

First, you need to install the library.

# Using npm
npm install fhevmjs

# Using Yarn
yarn add fhevmjs

# Using pnpm
pnpm add fhevmjs

fhevmjs uses ESM format. You need to set the type to "module" in your package.json. If your node project use "type": "commonjs" or no type, you can force the loading of the web version by using import { createInstance } from 'fhevmjs/web';

To use the library in your project, you need to load the WASM of TFHE first with initFhevm.

import { initFhevm } from "fhevmjs";

const init = async () => {
  await initFhevm(); // Load TFHE
};

init().then((instance) => {
  console.log(instance);
});

Once the WASM is loaded, you can now create an instance. An instance needs two element:

  • The blockchain public key. This key is needed to encrypt inputs

  • The blockchain' chain ID. This value is needed for reencryption process.

import { ethers, BrowserProvider } from "ethers";
import { initFhevm, createInstance, getPublicKeyCallParams } from "fhevmjs";

const createFhevmInstance = async () => {
  const provider = new BrowserProvider(window.ethereum);
  // 1. Get the chain id
  const network = await provider.getNetwork();
  const chainId = +network.chainId.toString();
  // 2. Fetch the FHE public key from the blockchain
  const ret = await provider.call(getPublicKeyCallParams());
  const decoded = ethers.AbiCoder.defaultAbiCoder().decode(["bytes"], ret);
  const publicKey = decoded[0];

  // 3. Create the instance
  return createInstance({ chainId, publicKey });
};

const init = async () => {
  await initFhevm(); // Load TFHE
  return createFhevmInstance();
};

init().then((instance) => {
  console.log(instance);
});

You can now use your instance to encrypt parameters or do a reencryption.

Local dev node

Setup

We provide a docker image to spin up a fhEVM node for local development.

docker run -i -p 8545:8545 -p 8546:8546 --rm --name fhevm ghcr.io/zama-ai/ethermint-dev-node:v0.4.2
Type
URL

JSON-RPC

http://127.0.0.1:8545

Websocket

http://127.0.0.1:8546

However, we advise developers to use directly pnpm fhevm:start or npm run fhevm:start commands available within the hardhat template, instead of the previous command, as this will launch a bash script which will also deploy automatically the oracle contract and launch the oracle relayer service, which are needed for asynchronous decryption requests.

WARNING: > OracleCaller.sol must be imported at least once in one of your smart contracts if you wish to use the recommended fhevm:start command, or else the bash script will emit an error and decryptions would fail. This is needed because hardhat needs to compile the oracle predeploy contract before your initial deployment. This can be done simply by adding the following import at the top of any of the smart contracts used in your project: import "fhevm/oracle/OracleCaller.sol";

Faucet

If you need to get coins for a specific wallet, you can use the faucet as follow:

docker exec -i fhevm faucet 0xa5e1defb98EFe38EBb2D958CEe052410247F4c80

Use encrypted types

The TFHE library provides encrypted integer types and a type system that is checked both at compile time and at run time.

Encrypted integers behave as much as possible as Solidity's integer types. Currently, however, behaviour such as "revert on overflow" is not supported as this would leak some information about the encrypted value. Therefore, arithmetic on e(u)int types is , i.e. there is wrap-around on overflow.

Encrypted integers with overflow checking are coming soon to the TFHE library. They will allow reversal in case of an overflow, but will leak some information about the operands.

In terms of implementation in the fhEVM, encrypted integers take the form of FHE ciphertexts. The TFHE library abstracts away that and, instead, exposes ciphertext handles to smart contract developers. The e(u)int types are wrappers over these handles.

The following encrypted data types are defined:

type
supported

Higher-precision integers are supported in the TFHE-rs library and can be added as needed to fhEVM.

Verification

When users send serialized ciphertexts as bytes to the blockchain, they first need to be converted to the respective encrypted integer type. Conversion verifies if the ciphertext is well-formed and includes proof verification. These steps prevent usage of arbitrary inputs. For example, following functions are provided for ebool, euint8, euint16 and euint32:

  • TFHE.asEbool(bytes ciphertext) verifies the provided ciphertext and returns an ebool

  • TFHE.asEuint4(bytes ciphertext) verifies the provided ciphertext and returns an euint4

  • TFHE.asEuint8(bytes ciphertext) verifies the provided ciphertext and returns an euint8

  • TFHE.asEuint16(bytes ciphertext) verifies the provided ciphertext and returns an euint16

  • TFHE.asEuint32(bytes ciphertext) verifies the provided ciphertext and returns an euint32

  • TFHE.asEuint64(bytes ciphertext) verifies the provided ciphertext and returns an euint64

  • TFHE.asEaddress(bytes ciphertext) verifies the provided ciphertext and returns an eaddress

  • ... more functions for the respective encrypted integer types

Example

Contract state variables with encrypted types

If you require a state variable that utilizes these encrypted types, you cannot assign the value with immutable or constant keyword. If you're using these types, the compiler attempts to ascertain the value of TFHE.asEuintXX(yy) during compilation, which is not feasible because asEuintXX() invokes a precompiled contract. To address this challenge, you must not declare your encrypted state variables as immutable or constant. Still, you can use the following methods to set your variables:

Development roadmap

Features

name
description
ETA

Operations

name
function name
type
ETA

NOTE 1: Methods prefixed with safe will do an overflow check by decrypting an overflow bit and revert if that bit is true.

NOTE 2: Random encrypted integers that are generated fully on-chain. Currently, implemented as a mockup by using a PRNG in the plain. Not for use in production!

128bits (scalar)

Add euint128 for scalar operations

Q3 '24

Proof for inputs

Generate a proof for every encrypted input

Q3 '24

Threshold decryption

Use threshold decryption

Q3 '24

Random unsigned int

TFHE.randEuintX()

Random

Q3 '24

Add w/ overflow check

TFHE.safeAdd

Binary, Decryption

Coming soon (1)

Sub w/ overflow check

TFHE.safeSub

Binary, Decryption

Coming soon (1)

Mul w/ overflow check

TFHE.safeMul

Binary, Decryption

Coming soon (1)

Random signed int

TFHE.randEintX()

Random

-

Div

TFHE.div

Binary

-

Rem

TFHE.rem

Binary

-

Set inclusion

TFHE.isIn()

Binary

-

ebool

yes

euint4

yes

euint8

yes

euint16

yes

euint32

yes

euint64

yes

eaddress

yes

eint8

no, coming soon

eint16

no, coming soon

eint32

no, coming soon

eint64

no, coming soon

function mint(bytes calldata encryptedAmount) public onlyContractOwner {
  euint64 amount = TFHE.asEuint64(encryptedAmount);
  balances[contractOwner] = balances[contractOwner] + amount;
  totalSupply = totalSupply + amount;
}
euint32 private totalSupply = TFHE.asEuint(0);
euint32 private totalSupply;
constructor() {
  totalSupply = TFHE.asEuint32(0);
}
unchecked

Operations on encrypted types

The TFHE library defines the following operations with FHE ciphertexts:

name
function name
symbol
type

Add

TFHE.add

+

Binary

Sub

TFHE.sub

-

Binary

Mul

TFHE.mul

*

Binary

Div (plaintext divisor)

TFHE.div

Binary

Rem (plaintext divisor)

TFHE.rem

Binary

BitAnd

TFHE.and

&

Binary

BitOr

TFHE.or

|

Binary

BitXor

TFHE.xor

^

Binary

Shift Right

TFHE.shr

Binary

Shift Left

TFHE.shl

Binary

Rotate Right

TFHE.rotr

Binary

Rotate Left

TFHE.rotl

Binary

Equal

TFHE.eq

Binary

Not equal

TFHE.ne

Binary

Greater than or equal

TFHE.ge

Binary

Greater than

TFHE.gt

Binary

Less than or equal

TFHE.le

Binary

Less than

TFHE.lt

Binary

Min

TFHE.min

Binary

Max

TFHE.max

Binary

Neg

TFHE.neg

-

Unary

Not

TFHE.not

~

Unary

Select

TFHE.select

Ternary

Decrypt

TFHE.decrypt()

Decryption

Reencrypt

TFHE.reencrypt()

Reencryption

Random unsigned int (mockup)

TFHE.randEuintX()

Random

NOTE 1: Random encrypted integers that are generated fully on-chain. Currently, implemented as a mockup by using a PRNG in the plain. Not for use in production!

NOTE 2: The shift operators TFHE.shr and TFHE.shl can take any encrypted type euintX as a first operand and either a uint8or a euint8 as a second operand, however the second operand will always be computed modulo the number of bits of the first operand. For example, TFHE.shr(euint64 x, 70) will actually be equal to TFHE.shr(euint64 x, 6) because 70 % 64 = 6. This is in contrast to the classical shift operators in Solidity where there is no intermediate modulo operation, so for instance any uint64 shifted right via >> would give a null result.

Overloaded operators +, -, *, &, ... on encrypted integers are supported (using for). As of now, overloaded operators will call the versions without an overflow check.

More information about the supported operations can be found in the function specifications page or in the TFHE-rs docs.

If you find yourself in search of a missing feature, we encourage you to consult our roadmap for upcoming developments. Alternatively, don't hesitate to reach out to us on Discord or visit our community forum.

Common pitfalls and best practises

Common pitfalls to avoid

No constant nor immutable encrypted state variables

Never use encrypted types for constant or immutable state variables, even if they should actually stay constants, or else any transaction involving those will fail. This is because ciphertexts should always be stored in the privileged storage of the contract (see paragraph 4.4 of whitepaper) while constant and immutable variables are just appended to the bytecode of the deployed contract at construction time.

❌ So, even if a and b should never change after construction, this code :

contract C {
  euint32 internal constant a = TFHE.asEuint32(42);
  euint32 internal immutable b;

  constructor(uint32 _b) {
    b = TFHE.asEuint32(_b);
  }
}

✅ Should be replaced by this snippet:

contract C {
  euint32 internal a = TFHE.asEuint32(42);
  euint32 internal b;

  constructor(uint32 _b) {
    b = TFHE.asEuint32(_b);
  }
}

Never use public encrypted state variables

Declaring an encrypted state variable as public exposes the variable to any external untrusted smart contract to access and potentially decrypt them, compromising their confidentiality.

❌ In summary, never write in production:

contract C {
  euint32 public a;

  constructor(uint32 _a) {
    a = TFHE.asEuint32(_a);
  }
}

✅ Instead, you should declare the state variable as follow:

contract C {
  euint32 internal a;

  constructor(uint32 _a) {
    a = TFHE.asEuint32(_a);
  }
}

In this last snippet, the internal keyword could have been omitted (state variables are internal by default) or alternatively have been replaced by private.

Protect access of view functions using reencryptions

If a view function is using TFHE.reencrypt it is mandatory to protect its access to not leak confidentiality, for instance this is doable easily via the onlySignedPublicKey modifier imported from "fhevm/abstracts/Reencrypt.sol". See the example from the Decrypt page. Failing to address this allows anyone to reencrypt another person's ciphertext. This vulnerability comes from the ability to impersonate any msg.sender address during a static call to a view function, as it does not require a signature, unlike transactions.

Best practises

Obfuscate branching

The previous paragraph emphasized that branch logic should rely as much as possible on TFHE.select instead of decryptions. It hides effectively which branch has been executed.

However, this is sometimes not enough. Enhancing the privacy of smart contracts often requires revisiting your application's logic.

For example, if implementing a simple AMM for two encrypted ERC20 tokens based on a linear constant function, it is recommended to not only hide the amounts being swapped, but also the token which is swapped in a pair.

✅ Here is a very simplified example implementations, we suppose here that the the rate between tokenA and tokenB is constant and equals to 1:

    // typically either encryptedAmountAIn or encryptedAmountBIn is an encrypted null value
    // ideally, the user already owns some amounts of both tokens and has pre-approved the AMM on both tokens
    function swapTokensForTokens(
        bytes calldata encryptedAmountAIn,
        bytes calldata encryptedAmountBIn,
    ) external {
        euint32 encryptedAmountA = TFHE.asEuint32(encryptedAmountAIn); // even if amount is null, do a transfer to obfuscate trade direction
        euint32 encryptedAmountB = TFHE.asEuint32(encryptedAmountBIn); // even if amount is null, do a transfer to obfuscate trade direction

        // send tokens from user to AMM contract
        IEncryptedERC20(tokenA).transferFrom(
            msg.sender, address(this), encryptedAmountA
        );
        IEncryptedERC20(tokenB).transferFrom(
            msg.sender, address(this), encryptedAmountB
        );

        // send tokens from AMM contract to user
        // Price of tokenA in tokenB is constant and equal to 1, so we just swap the encrypted amounts here
        IEncryptedERC20(tokenA).transfer(
            msg.sender, encryptedAmountB
        );
        IEncryptedERC20(tokenB).transferFrom(
            msg.sender, address(this), encryptedAmountA
        );
    }

Notice that to preserve confidentiality, we had to make two inputs transfers on both tokens from the user to the AMM contract, and similarly two output transfers from the AMM to the user, even if technically most of the times it will make sense that one of the user inputs encryptedAmountAIn or encryptedAmountBIn is actually an encrypted zero.

This is different from a classical non-confidential AMM with regular ERC20 tokens: in this case, the user would need to just do one input transfer to the AMM on the token being sold, and receive only one output transfer from the AMM on the token being bought.

Avoid using encrypted indexes

Using encrypted indexes to pick an element from an array without revealing it is not very efficient, because you would still need to loop on all the indexes to preserve confidentiality.

However, there are plans to make this kind of operation much more efficient in the future, by adding specialized operators for arrays.

For instance, imagine you have an encrypted array called encArray and you want to update an encrypted value x to match an item from this list, encArray[i], without disclosing which item you're choosing.

❌ You must loop over all the indexes and check equality homomorphically, however this pattern is very expensive in gas and should be avoided whenever possible.

euint32 x;
euint32[] encArray;

function setXwithEncryptedIndex(bytes calldata encryptedIndex) public {
    euint32 index = TFHE.asEuint32(encryptedIndex);
    for (uint32 i = 0; i < encArray.length; i++) {
        ebool isEqual = TFHE.eq(index, i);
        x = TFHE.select(isEqual, encArray[i], x);
    }
}

Use scalar operands when possible to save gas

Some TFHE operators exist in two versions : one where all operands are ciphertexts handles, and another where one of the operands is an unencrypted scalar. Whenever possible, use the scalar operand version, as this will save a lot of gas. See the page on Gas to discover which operators support scalar operands and compare the gas saved between both versions: all-encrypted operands vs scalar.

❌ For example, this snippet cost way more in gas:

euint32 x;
...
x = TFHE.add(x,TFHE.asEuint(42));

✅ Than this one:

euint32 x;
...
x = TFHE.add(x,42);

Despite both leading to the same encrypted result!

Beware of overflows of TFHE arithmetic operators

TFHE arithmetic operators can overflow. Do not forget to take into account such a possibility when implementing fhEVM smart contracts.

❌ For example, if you wanted to create a mint function for an encrypted ERC20 tokens with an encrypted totalSupply state variable, this code is vulnerable to overflows:

function mint(bytes calldata encryptedAmount) public {
  euint32 mintedAmount = TFHE.asEuint32(encryptedAmount);
  totalSupply = TFHE.add(totalSupply, mintedAmount);
  balances[msg.sender] = TFHE.add(balances[msg.sender], mintedAmount);
}

✅ But you can fix this issue by using TFHE.select to cancel the mint in case of an overflow:

function mint(bytes calldata encryptedAmount) public {
  euint32 mintedAmount = TFHE.asEuint32(encryptedAmount);
  euint32 tempTotalSupply = TFHE.add(totalSupply, mintedAmount);
  ebool isOverflow = TFHE.lt(tempTotalSupply, totalSupply);
  totalSupply = TFHE.select(isOverflow, totalSupply, tempTotalSupply);
  euint32 tempBalanceOf = TFHE.add(balances[msg.sender], mintedAmount);
  balances[msg.sender] = TFHE.select(isOverflow, balances[msg.sender], tempBalanceOf);
}

Notice that we did not check separately the overflow on balances[msg.sender] but only on totalSupply variable, because totalSupply is the sum of the balances of all the users, so balances[msg.sender] could never overflow if totalSupply did not.

API Function specifications

The functions exposed by the TFHE Solidity library come in various shapes and sizes in order to facilitate developer experience. For example, most binary operators (e.g., add) can take as input any combination of the supported data types.

In the fhEVM, FHE operations are only defined on same-type operands. Implicit upcasting will be done automatically, if necessary.

Most binary operators are also defined with a mix of ciphertext and plaintext operands, under the condition that the size of the plaintext operand is at most the size of the encrypted operand. For example, add(uint8 a, euint8 b) is defined but add(uint32 a, euint16 b) is not. Note that these ciphertext-plaintext operations may take less time to compute than ciphertext-ciphertext operations.

asEuint

The asEuint functions serve three purposes:

  1. verify ciphertext bytes and return a valid handle to the calling smart contract;

  2. cast a euintX typed ciphertext to a euintY typed ciphertext, where X != Y;

  3. trivially encrypt a plaintext value.

The first case is used to process encrypted inputs, e.g. user-provided ciphertexts. Those are generally included in a transaction payload.

The second case is self-explanatory. When X > Y, the most significant bits are dropped. When X < Y, the ciphertext is padded to the left with trivial encryptions of 0.

The third case is used to "encrypt" a public value so that it can be used as a ciphertext. Note that what we call a trivial encryption is not secure in any sense. When trivially encrypting a plaintext value, this value is still visible in the ciphertext bytes. More information about trivial encryption can be found .

Examples

asEbool

The asEbool functions behave similarly to the asEuint functions, but for encrypted boolean values.

Reencrypt

The reencrypt functions takes as inputs a ciphertext and a public encryption key (namely, a ).

During reencryption, the ciphertext is decrypted using the network private key (the threshold decryption protocol is in the works). Then, the decrypted result is encrypted under the user-provided public encryption key. The result of this encryption is sent back to the caller as bytes memory.

It is also possible to provide a default value to the reencrypt function. In this case, if the provided ciphertext is not initialized (i.e., if the ciphertext handle is 0), the function will return an encryption of the provided default value.

Examples

NOTE: If one of the following operations is called with an uninitialized ciphertext handle as an operand, this handle will be made to point to a trivial encryption of 0 before the operation is executed.

Arithmetic operations (add, sub, mul, div, rem)

Performs the operation homomorphically.

Note that division/remainder only support plaintext divisors.

Examples

Bitwise operations (AND, OR, XOR)

Unlike other binary operations, bitwise operations do not natively accept a mix of ciphertext and plaintext inputs. To ease developer experience, the TFHE library adds function overloads for these operations. Such overloads implicitely do a trivial encryption before actually calling the operation function, as shown in the examples below.

Examples

Bit shift operations (<<, >>)

Shifts the bits of the base two representation of a by b positions.

Examples

Comparison operation (eq, ne, ge, gt, le, lt)

Note that in the case of ciphertext-plaintext operations, since our backend only accepts plaintext right operands, calling the operation with a plaintext left operand will actually invert the operand order and call the opposite comparison.

The result of comparison operations is an encrypted boolean (ebool). In the backend, the boolean is represented by an encrypted unsinged integer of bit width 8, but this is abstracted away by the Solidity library.

Examples

Multiplexer operator (select)

This operator takes three inputs. The first input b is of type ebool and the two others of type euintX. If b is an encryption of true, the first integer parameter is returned. Otherwise, the second integer parameter is returned.

Example

min, max

Returns the minimum (resp. maximum) of the two given values.

Examples

Unary operators (neg, not)

There are two unary operators: neg (-) and not (!). Note that since we work with unsigned integers, the result of negation is interpreted as the modular opposite. The not operator returns the value obtained after flipping all the bits of the operand.

NOTE: More information about the behavior of these operators can be found at the .

Generating random encrypted integers

Random encrypted integers can be generated fully on-chain.

That can only be done during transactions and not on an eth_call RPC method, because PRNG state needs to be mutated on-chain during generation.

WARNING: Not for use in production! Currently, integers are generated in the plain via a PRNG whose seed and state are public, with the state being on-chain. An FHE-based PRNG is coming soon, where the seed and state will be encrypted.

Example

// first case
function asEuint8(bytes memory ciphertext) internal view returns (euint8)
// second case
function asEuint16(euint8 ciphertext) internal view returns (euint16)
// third case
function asEuint16(uint16 value) internal view returns (euint16)
// returns the decryption of `ciphertext`, encrypted under `publicKey`.
function reencrypt(euint32 ciphertext, bytes32 publicKey) internal view returns (bytes memory reencrypted)

// if the handle of `ciphertext` is equal to `0`, returns `defaultValue` encrypted under `publicKey`.
// otherwise, returns as above
function reencrypt(euint32 ciphertext, bytes32 publicKey, uint32 defaultValue) internal view returns (bytes memory reencrypted)
// a + b
function add(euint8 a, euint8 b) internal view returns (euint8)
function add(euint8 a, euint16 b) internal view returns (euint16)
function add(uint32 a, euint32 b) internal view returns (euint32)

// a / b
function div(euint8 a, uint8 b) internal pure returns (euint8)
function div(euint16 a, uint16 b) internal pure returns (euint16)
function div(euint32 a, uint32 b) internal pure returns (euint32)
// a & b
function and(euint8 a, euint8 b) internal view returns (euint8)

// implicit trivial encryption of `b` before calling the operator
function and(euint8 a, uint16 b) internal view returns (euint16)
// a << b
function shl(euint16 a, euint8 b) internal view returns (euint16)
// a >> b
function shr(euint32 a, euint16 b) internal view returns (euint32)
// a == b
function eq(euint32 a, euint16 b) internal view returns (ebool)

// actually returns `lt(b, a)`
function gt(uint32 a, euint16 b) internal view returns (ebool)

// actually returns `gt(a, b)`
function gt(euint16 a, uint32 b) internal view returns (ebool)
// if (b == true) return val1 else return val2
function select(ebool b, euint8 val1, euint8 val2) internal view returns (euint8) {
  return TFHE.select(b, val1, val2);
}
// min(a, b)
function min(euint32 a, euint16 b) internal view returns (euint32)

// max(a, b)
function max(uint32 a, euint8 b) internal view returns (euint32)
// Generate a random encrypted unsigned integer `r`.
euint32 r = TFHE.randEuint32();
here
NaCl box
TFHE-rs docs

Decrypt and reencrypt

Deprecation warning TFHE.decrypt is deprecated and will be removed in the next release. We strongly encourage users to migrate their code to use the new asynchronous decryption method.

Asynchronous decryption

We allow explicit decryption requests for any encrypted type. The values are decrypted with the network private key (the threshold decryption protocol is in the works).

Example

The decryption operation is asynchronous. To use it, your contract must extend the OracleCaller contract. This will import automatically the Oracle solidity library as well. See the following example:

pragma solidity ^0.8.20;

import "fhevm/lib/TFHE.sol";
import "fhevm/oracle/OracleCaller.sol";

contract TestAsyncDecrypt is OracleCaller {
  ebool xBool;
  bool public yBool;

  constructor() {
      xBool = TFHE.asEbool(true);
  }

  function requestBool() public {
    ebool[] memory cts = new ebool[](1);
    cts[0] = xBool;
    Oracle.requestDecryption(cts, this.myCustomCallback.selector, 0, block.timestamp + 100);
  }

  function myCustomCallback(uint256 /*requestID*/, bool decryptedInput) public onlyOracle returns (bool) {
    yBool = decryptedInput;
    return yBool;
  }

Note that an OraclePredeploy contract is already predeployed on the fhEVM testnet, and a default relayer account is added through the specification of the environment variable PRIVATE_KEY_ORACLE_RELAYER in the .env file. Relayers are the only accounts authorized to fulfil the decryption requests. However OraclePredeploy would still check the KMS signature during the fulfilment, so we trust the relayer only to forward the request on time, a rogue relayer could not cheat by sending fake decryption results (the KMS signature is in the works).

The interface of the Oracle.requestDecryption function from previous snippet is the following:

function requestDecryption(
    eXXX[] memory ct,
    bytes4 callbackSelector,
    uint256 msgValue,
    uint256 maxTimestamp
) returns(uint256 requestID)

The first argument, ct, should be an array of ciphertexts of a single same type i.e eXXX stands for either ebool, euint4, euint8, euint16, euint32, euint64 or eaddress. ct is the list of ciphertexts that are requested to be decrypted. Calling requestDecryption will emit an EventDecryptionEXXX on the OraclePredeploy contract which will be detected by a relayer. Then, the relayer will send the corresponding ciphertexts to the KMS for decryption before fulfilling the request.

callbackSelector is the function selector of the callback function which will be called by the OraclePredeploy contract once the relayer fulfils the decryption request. Notice that the callback function should always follow this convention:

function [callbackName](uint256 requestID, XXX x_0, XXX x_1, ..., XXX x_N-1) external onlyOracle

Here callbackName is a custom name given by the developer to the callback function, requestID will be the request id of the decryption (could be commented if not needed in the logic, but must be present) and x_0, x_1, ... x_N-1 are the results of the decryption of the ct array values, i.e their number should be the size of the ct array.

msgValue is the value in native tokens to be sent to the calling contract during fulfilment, i.e when the callback will be called with the results of decryption.

maxTimestamp is the maximum timestamp after which the callback will not be able to receive the results of decryption, i.e the fulfilment transaction will fail in this case. This can be used for time-sensitive applications, where we prefer to reject decryption results on too old, out-of-date, values.

WARNING: Notice that the callback should be protected by the onlyOracle modifier to ensure security, as only the OraclePredeploy contract should be able to call it.

Finally, if you need to pass additional arguments to be used inside the callback, you could use any of the following utility functions during the request, which would store additional values in the storage of your smart contract:

function addParamsEBool(uint256 requestID, ebool _ebool) internal;

function addParamsEUint4(uint256 requestID, euint4 _euint4) internal;

function addParamsEUint8(uint256 requestID, euint8 _euint8) internal;

function addParamsEUint16(uint256 requestID, euint16 _euint16) internal;

function addParamsEUint32(uint256 requestID, euint32 _euint32) internal;

function addParamsEUint64(uint256 requestID, euint64 _euint64) internal;

function addParamsEAddress(uint256 requestID, address _eaddress) internal;

function addParamsAddress(uint256 requestID, address _address) internal;

function addParamsUint(uint256 requestID, uint256 _uint) internal;

With their corresponding getter functions to be used inside the callback:

function getParamsEBool(uint256 requestID) internal;

function getParamsEUint4(uint256 requestID) internal;

function getParamsEUint8(uint256 requestID) internal;

function getParamsEUint16(uint256 requestID) internal;

function getParamsEUint32(uint256 requestID) internal;

function getParamsEUint64(uint256 requestID) internal;

function getParamsEAddress(uint256 requestID) internal;

function getParamsAddress(uint256 requestID) internal;

function getParamsUint(uint256 requestID) internal;

For example, see this snippet where we add two uints during the request call, to make them available later during the callback:

pragma solidity ^0.8.20;

import "../lib/TFHE.sol";
import "../oracle/OracleCaller.sol";

contract TestAsyncDecrypt is OracleCaller {
  euint32 xUint32;
  uint32 public yUint32;

  constructor() {
      xUint32 = TFHE.asEuint32(32);
  }

  function requestUint32(uint32 input1, uint32 input2) public {
      euint32[] memory cts = new euint32[](1);
      cts[0] = xUint32;
      uint256 requestID = Oracle.requestDecryption(cts, this.callbackUint32.selector, 0, block.timestamp + 100);
      addParamsUint(requestID, input1);
      addParamsUint(requestID, input2);
  }

  function callbackUint32(uint256 requestID, uint32 decryptedInput) public onlyOracle returns (uint32) {
    uint256[] memory params = getParamsUint(requestID);
    unchecked {
        uint32 result = uint32(params[0]) + uint32(params[1]) + decryptedInput;
        yUint32 = result;
        return result;
    }
}

When the decryption request is fufilled by the relayer, the OraclePredeploy contract, when calling the callback function, will also emit one of the following events, depending on the type of requested ciphertext:

event ResultCallbackBool(uint256 indexed requestID, bool success, bytes result);
event ResultCallbackUint4(uint256 indexed requestID, bool success, bytes result);
event ResultCallbackUint8(uint256 indexed requestID, bool success, bytes result);
event ResultCallbackUint16(uint256 indexed requestID, bool success, bytes result);
event ResultCallbackUint32(uint256 indexed requestID, bool success, bytes result);
event ResultCallbackUint64(uint256 indexed requestID, bool success, bytes result);
event ResultCallbackAddress(uint256 indexed requestID, bool success, bytes result);

The first argument is the requestID of the corresponding decryption request, success is a boolean assessing if the call to the callback succeeded, and result is the bytes array corresponding the to return data from the callback.

In your hardhat tests, if you sent some transactions which are requesting one or several decryptions and you wish to await the fulfilment of those decryptions, you should import the two helper methods asyncDecrypt and awaitAllDecryptionResults from the asyncDecrypt.ts utility file. This would work both when testing on an fhEVM node or in mocked mode. Here is a simple hardhat test for the previous TestAsyncDecrypt contract (more examples can be seen here):

import { asyncDecrypt, awaitAllDecryptionResults } from "../asyncDecrypt";
import { getSigners, initSigners } from "../signers";
import { expect } from "chai";
import { ethers } from "hardhat";

describe("TestAsyncDecrypt", function () {
  before(async function () {
    await asyncDecrypt();
    await initSigners(3);
    this.signers = await getSigners();
  });

  beforeEach(async function () {
    const contractFactory = await ethers.getContractFactory("TestAsyncDecrypt");
    this.contract = await contractFactory.connect(this.signers.alice).deploy();
  });

  it("test async decrypt uint32", async function () {
    const tx2 = await this.contract.connect(this.signers.carol).requestUint32(5, 15, { gasLimit: 500_000 }); // custom gasLimit to avoid gas estimation error in fhEVM mode
    await tx2.wait();
    await awaitAllDecryptionResults();
    const y = await this.contract.yUint32();
    expect(y).to.equal(52); // 5+15+32
  });
});

You should setup the oracle handler by calling asyncDecrypt at the top of the before block. Notice that when testing on the fhEVM, a decryption is fulfilled usually 2 blocks after the request, while in mocked mode the fulfilment will always happen as soon as you call the awaitAllDecryptionResults helper function. A good way to standardize hardhat tests is hence to always callawaitAllDecryptionResults which will ensure that all pending decryptions are fulfilled in both modes.

Reencrypt

The reencrypt functions takes as inputs a ciphertext and a public encryption key (namely, a NaCl box).

During reencryption, the ciphertext is decrypted using the network private key (the threshold decryption protocol is in the works). Then, the decrypted result is encrypted under the user-provided public encryption key. The result of this encryption is sent back to the caller as bytes memory.

It is also possible to provide a default value to the reencrypt function. In this case, if the provided ciphertext is not initialized (i.e., if the ciphertext handle is 0), the function will return an encryption of the provided default value.

Example

TFHE.reencrypt(balances[msg.sender], publicKey, 0);

NOTE: If one of the following operations is called with an uninitialized ciphertext handle as an operand, this handle will be made to point to a trivial encryption of 0 before the operation is executed.

Handle private reencryption

In the example above (balanceOf), this view function need to validate the user to prevent anyone to reencrypt any user's balance. To prevent this, the user provides a signature of the given public key. The best way to do it is to use EIP-712 standard. Since this is something very useful, fhEVM library provide an abstract to use in your contract:

import "fhevm/abstracts/Reencrypt.sol";

contract EncryptedERC20 is Reencrypt {
  ...
}

When a contract uses Reencrypt abstract, a modifier is available to check user signature.

function balanceOf(
  bytes32 publicKey,
  bytes calldata signature
) public view onlySignedPublicKey(publicKey, signature) returns (bytes memory) {
  return TFHE.reencrypt(balances[msg.sender], publicKey, 0);
}

This signature can be generated on client side using fhevmjs library.

Gas estimation

FHE operations are typically more computationally expensive than classical operations due to their inherent complexity. As a reference, here is an approximation of the gas cost associated with each operation.

ebool

Function name
Gas

and/or/xor

26,000

not

30,000

decrypt

500,000

euint4

function name
Gas

add/sub

65,000

add/sub (scalar)

65,000

mul

150,000

mul (scalar)

88,000

div (scalar)

139,000

rem (scalar)

286,000

and/or/xor

32,000

shr/shl

116,000

shr/shl (scalar)

35,000

rotr/rotl

116,000

rotr/rotl (scalar)

35,000

eq/ne

51,000

ge/gt/le/lt

70,000

min/max

121,000

min/max (scalar)

121,000

neg

60,000

not

33,000

select

45,000

decrypt

500,000

euint8

Function name
Gas

add/sub

94,000

add/sub (scalar)

94,000

mul

197,000

mul (scalar)

159,000

div (scalar)

238,000

rem (scalar)

460,000

and/or/xor

34,000

shr/shl

133,000

shr/shl (scalar)

35,000

rotr/rotl

133,000

rotr/rotl (scalar)

35,000

eq/ne

53,000

ge/gt/le/lt

82,000

min/max

128,000

min/max (scalar)

128,000

neg

95,000

not

34,000

select

47,000

decrypt

500,000

randEuint8()

100,000

euint16

function name
euint16

add/sub

133,000

add/sub (scalar)

133,000

mul

262,000

mul (scalar)

208,000

div (scalar)

314,000

rem (scalar)

622,000

and/or/xor

34,000

shr/shl

153,000

shr/shl (scalar)

35,000

rotr/rotl

153,000

rotr/rotl (scalar)

35,000

eq/ne

54,000

ge/gt/le/lt

105,000

min/max

153,000

min/max (scalar)

150,000

neg

131,000

not

35,000

select

47,000

decrypt

500,000

randEuint16()

100,000

euint32

Function name
Gas fee

add/sub

162,000

add/sub (scalar)

162,000

mul

359,000

mul (scalar)

264,000

div (scalar)

398,000

rem (scalar)

805,000

and/or/xor

35,000

shr/shl

183,000

shr/shl (scalar)

35,000

rotr/rotl

183,000

rotr/rotl (scalar)

35,000

eq/ne

82,000

ge/gt/le/lt

128,000

min/max

183,000

min/max (scalar)

164,000

neg

160,000

not

36,000

select

50,000

decrypt

500,000

randEuint32()

100,000

euint64

Function name
Gas fee

add/sub

188,000

add/sub (scalar)

188,000

mul

641,000

mul (scalar)

356,000

div (scalar)

584,000

rem (scalar)

1,095,000

and/or/xor

38,000

shr/shl

227,000

shr/shl (scalar)

38,000

rotr/rotl

227,000

rotr/rotl (scalar)

38,000

eq/ne

86,000

ge/gt/le/lt

156,000

min/max

210,000

min/max (scalar)

192,000

neg

199,000

not

37,000

select

53,000

decrypt

500,000

randEuint64()

100,000

eaddress

Function name
Gas fee

eq/ne

90,000

Estimate gas

When you call estimate gas method, we can’t determine accurately the gas usage if your function uses TFHE.decrypt. During gas estimation, all TFHE.decrypt() will return 1.

What does it mean?

  • require(TFHE.decrypt(ebool)); will be ok but require(!TFHE.decrypt(ebool)); will fail during estimation (revert transaction)

  • A loop, where you expect a decrypt to be false to break, will never end in gas estimate method (and fails), since the decrypt will always return 1 (true)

  • On the other hand, if your loop should last 2 or 3 cycles, until the value is 1, the estimation will be below.

  • If you have branches (if/else) based on a decryption, the estimation will use the branch running when the decryption is 1

While it’s challenging to accurately estimate gas consumption when using TFHE.decrypt, we strongly encourage you to take this into consideration.

What can I do?

A possible solution is to overestimate your gas estimation. You can take this function (with ethers.js) as an example where we multiply the gas limit by 1.2.

export const createTransaction = async <A extends [...{ [I in keyof A]-?: A[I] | Typed }]>(
  method: TypedContractMethod<A>,
  ...params: A
) => {
  const gasLimit = await method.estimateGas(...params);
  const updatedParams: ContractMethodArgs<A> = [
    ...params,
    { gasLimit: Math.min(Math.round(+gasLimit.toString() * 1.2), 10000000) },
  ];
  return method(...updatedParams);
};

Gas limit

The current devnet has a gas limit of 10,000,000. If you send a transaction exceeding this limit, it won't be executed. Consequently, your wallet won't be able to emit a new transaction. To address this, emit a new transaction with the same nonce but the correct gas limit. In Metamask, you can enforce the use of a specific nonce by enabling the feature in 'Advanced Settings'.