# Zama FHEVM Combined Documentation ## Table of Contents - [examples/fhe-counter.md](#examples/fhe-counter.md) - [examples/fhe-encrypt-multiple-value.md](#examples/fhe-encrypt-multiple-value.md) - [examples/fhe-encrypt-multiple-values.md](#examples/fhe-encrypt-multiple-values.md) - [examples/fhe-encrypt-single-value.md](#examples/fhe-encrypt-single-value.md) - [examples/fhe-public-decrypt-multiple-values.md](#examples/fhe-public-decrypt-multiple-values.md) - [examples/fhe-public-decrypt-single-value.md](#examples/fhe-public-decrypt-single-value.md) - [examples/fhe-user-decrypt-multiple-values.md](#examples/fhe-user-decrypt-multiple-values.md) - [examples/fhe-user-decrypt-single-value.md](#examples/fhe-user-decrypt-single-value.md) - [examples/fheadd.md](#examples/fheadd.md) - [examples/fheifthenelse.md](#examples/fheifthenelse.md) - [examples/legacy/see-all-tutorials.md](#examples/legacy/see-all-tutorials.md) - [examples/openzeppelin/erc7984-tutorial.md](#examples/openzeppelin/erc7984-tutorial.md) - [examples/openzeppelin/erc7984.md](#examples/openzeppelin/erc7984.md) - [examples/openzeppelin/ERC7984ERC20WrapperMock.md](#examples/openzeppelin/erc7984erc20wrappermock.md) - [examples/openzeppelin/README.md](#examples/openzeppelin/readme.md) - [examples/openzeppelin/swapERC7984ToERC20.md](#examples/openzeppelin/swaperc7984toerc20.md) - [examples/openzeppelin/swapERC7984ToERC7984.md](#examples/openzeppelin/swaperc7984toerc7984.md) - [examples/openzeppelin/vesting-wallet.md](#examples/openzeppelin/vesting-wallet.md) - [examples/sealed-bid-auction-tutorial.md](#examples/sealed-bid-auction-tutorial.md) - [examples/sealed-bid-auction.md](#examples/sealed-bid-auction.md) - [examples/SUMMARY.md](#examples/summary.md) - [operators/operators-overview.md](#operators/operators-overview.md) - [protocol/architecture/coprocessor.md](#protocol/architecture/coprocessor.md) - [protocol/architecture/gateway.md](#protocol/architecture/gateway.md) - [protocol/architecture/hostchain.md](#protocol/architecture/hostchain.md) - [protocol/architecture/kms.md](#protocol/architecture/kms.md) - [protocol/architecture/library.md](#protocol/architecture/library.md) - [protocol/architecture/overview.md](#protocol/architecture/overview.md) - [protocol/architecture/relayer_oracle.md](#protocol/architecture/relayer_oracle.md) - [protocol/contribute.md](#protocol/contribute.md) - [protocol/d_re_ecrypt_compute.md](#protocol/d_re_ecrypt_compute.md) - [protocol/README.md](#protocol/readme.md) - [protocol/roadmap.md](#protocol/roadmap.md) - [protocol/SUMMARY.md](#protocol/summary.md) - [sdk-guides/cli.md](#sdk-guides/cli.md) - [sdk-guides/initialization.md](#sdk-guides/initialization.md) - [sdk-guides/input.md](#sdk-guides/input.md) - [sdk-guides/public-decryption.md](#sdk-guides/public-decryption.md) - [sdk-guides/sdk-overview.md](#sdk-guides/sdk-overview.md) - [sdk-guides/SUMMARY.md](#sdk-guides/summary.md) - [sdk-guides/user-decryption.md](#sdk-guides/user-decryption.md) - [sdk-guides/webapp.md](#sdk-guides/webapp.md) - [sdk-guides/webpack.md](#sdk-guides/webpack.md) - [solidity-guides/acl/acl_examples.md](#solidity-guides/acl/acl_examples.md) - [solidity-guides/acl/README.md](#solidity-guides/acl/readme.md) - [solidity-guides/acl/reorgs_handling.md](#solidity-guides/acl/reorgs_handling.md) - [solidity-guides/configure.md](#solidity-guides/configure.md) - [solidity-guides/contract_addresses.md](#solidity-guides/contract_addresses.md) - [solidity-guides/debug_decrypt.md](#solidity-guides/debug_decrypt.md) - [solidity-guides/decryption/debugging.md](#solidity-guides/decryption/debugging.md) - [solidity-guides/decryption/oracle.md](#solidity-guides/decryption/oracle.md) - [solidity-guides/foundry.md](#solidity-guides/foundry.md) - [solidity-guides/functions.md](#solidity-guides/functions.md) - [solidity-guides/getting-started/overview.md](#solidity-guides/getting-started/overview.md) - [solidity-guides/getting-started/quick-start-tutorial/README.md](#solidity-guides/getting-started/quick-start-tutorial/readme.md) - [solidity-guides/getting-started/quick-start-tutorial/setup.md](#solidity-guides/getting-started/quick-start-tutorial/setup.md) - [solidity-guides/getting-started/quick-start-tutorial/test_the_fhevm_contract.md](#solidity-guides/getting-started/quick-start-tutorial/test_the_fhevm_contract.md) - [solidity-guides/getting-started/quick-start-tutorial/turn_it_into_fhevm.md](#solidity-guides/getting-started/quick-start-tutorial/turn_it_into_fhevm.md) - [solidity-guides/getting-started/quick-start-tutorial/write_a_simple_contract.md](#solidity-guides/getting-started/quick-start-tutorial/write_a_simple_contract.md) - [solidity-guides/hardhat/README.md](#solidity-guides/hardhat/readme.md) - [solidity-guides/hardhat/run_test.md](#solidity-guides/hardhat/run_test.md) - [solidity-guides/hardhat/write_task.md](#solidity-guides/hardhat/write_task.md) - [solidity-guides/hardhat/write_test.md](#solidity-guides/hardhat/write_test.md) - [solidity-guides/hcu.md](#solidity-guides/hcu.md) - [solidity-guides/inputs.md](#solidity-guides/inputs.md) - [solidity-guides/key_concepts.md](#solidity-guides/key_concepts.md) - [solidity-guides/logics/conditions.md](#solidity-guides/logics/conditions.md) - [solidity-guides/logics/error_handling.md](#solidity-guides/logics/error_handling.md) - [solidity-guides/logics/loop.md](#solidity-guides/logics/loop.md) - [solidity-guides/logics/README.md](#solidity-guides/logics/readme.md) - [solidity-guides/migration.md](#solidity-guides/migration.md) - [solidity-guides/mocked.md](#solidity-guides/mocked.md) - [solidity-guides/operations/casting.md](#solidity-guides/operations/casting.md) - [solidity-guides/operations/random.md](#solidity-guides/operations/random.md) - [solidity-guides/operations/README.md](#solidity-guides/operations/readme.md) - [solidity-guides/README.md](#solidity-guides/readme.md) - [solidity-guides/SUMMARY.md](#solidity-guides/summary.md) - [solidity-guides/transform_smart_contract_with_fhevm.md](#solidity-guides/transform_smart_contract_with_fhevm.md) - [solidity-guides/types.md](#solidity-guides/types.md) --- ## examples/fhe-counter.md > _From `examples/fhe-counter.md`_ This example demonstrates how to build an confidential counter using FHEVM, in comparison to a simple counter. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} ## A simple counter {% tabs %} {% tab title="counter.sol" %} ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title A simple counter contract contract Counter { uint32 private _count; /// @notice Returns the current count function getCount() external view returns (uint32) { return _count; } /// @notice Increments the counter by a specific value function increment(uint32 value) external { _count += value; } /// @notice Decrements the counter by a specific value function decrement(uint32 value) external { require(_count >= value, "Counter: cannot decrement below zero"); _count -= value; } } ``` {% endtab %} {% tab title="counter.ts" %} ```ts import { Counter, Counter__factory } from "../types"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; type Signers = { deployer: HardhatEthersSigner; alice: HardhatEthersSigner; bob: HardhatEthersSigner; }; async function deployFixture() { const factory = (await ethers.getContractFactory("Counter")) as Counter__factory; const counterContract = (await factory.deploy()) as Counter; const counterContractAddress = await counterContract.getAddress(); return { counterContract, counterContractAddress }; } describe("Counter", function () { let signers: Signers; let counterContract: Counter; before(async function () { const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { deployer: ethSigners[0], alice: ethSigners[1], bob: ethSigners[2] }; }); beforeEach(async () => { ({ counterContract } = await deployFixture()); }); it("count should be zero after deployment", async function () { const count = await counterContract.getCount(); console.log(`Counter.getCount() === ${count}`); // Expect initial count to be 0 after deployment expect(count).to.eq(0); }); it("increment the counter by 1", async function () { const countBeforeInc = await counterContract.getCount(); const tx = await counterContract.connect(signers.alice).increment(1); await tx.wait(); const countAfterInc = await counterContract.getCount(); expect(countAfterInc).to.eq(countBeforeInc + 1n); }); it("decrement the counter by 1", async function () { // First increment, count becomes 1 let tx = await counterContract.connect(signers.alice).increment(1); await tx.wait(); // Then decrement, count goes back to 0 tx = await counterContract.connect(signers.alice).decrement(1); await tx.wait(); const count = await counterContract.getCount(); expect(count).to.eq(0); }); }); ``` {% endtab %} {% endtabs %} ## An FHE counter {% tabs %} {% tab title="FHECounter.sol" %} ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { FHE, euint32, externalEuint32 } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; /// @title A simple FHE counter contract contract FHECounter is SepoliaConfig { euint32 private _count; /// @notice Returns the current count function getCount() external view returns (euint32) { return _count; } /// @notice Increments the counter by a specified encrypted value. /// @dev This example omits overflow/underflow checks for simplicity and readability. /// In a production contract, proper range checks should be implemented. function increment(externalEuint32 inputEuint32, bytes calldata inputProof) external { euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof); _count = FHE.add(_count, encryptedEuint32); FHE.allowThis(_count); FHE.allow(_count, msg.sender); } /// @notice Decrements the counter by a specified encrypted value. /// @dev This example omits overflow/underflow checks for simplicity and readability. /// In a production contract, proper range checks should be implemented. function decrement(externalEuint32 inputEuint32, bytes calldata inputProof) external { euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof); _count = FHE.sub(_count, encryptedEuint32); FHE.allowThis(_count); FHE.allow(_count, msg.sender); } } ``` {% endtab %} {% tab title="FHECounter.ts" %} ```ts import { FHECounter, FHECounter__factory } from "../types"; import { FhevmType } from "@fhevm/hardhat-plugin"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers, fhevm } from "hardhat"; type Signers = { deployer: HardhatEthersSigner; alice: HardhatEthersSigner; bob: HardhatEthersSigner; }; async function deployFixture() { const factory = (await ethers.getContractFactory("FHECounter")) as FHECounter__factory; const fheCounterContract = (await factory.deploy()) as FHECounter; const fheCounterContractAddress = await fheCounterContract.getAddress(); return { fheCounterContract, fheCounterContractAddress }; } describe("FHECounter", function () { let signers: Signers; let fheCounterContract: FHECounter; let fheCounterContractAddress: string; before(async function () { const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { deployer: ethSigners[0], alice: ethSigners[1], bob: ethSigners[2] }; }); beforeEach(async () => { ({ fheCounterContract, fheCounterContractAddress } = await deployFixture()); }); it("encrypted count should be uninitialized after deployment", async function () { const encryptedCount = await fheCounterContract.getCount(); // Expect initial count to be bytes32(0) after deployment, // (meaning the encrypted count value is uninitialized) expect(encryptedCount).to.eq(ethers.ZeroHash); }); it("increment the counter by 1", async function () { const encryptedCountBeforeInc = await fheCounterContract.getCount(); expect(encryptedCountBeforeInc).to.eq(ethers.ZeroHash); const clearCountBeforeInc = 0; // Encrypt constant 1 as a euint32 const clearOne = 1; const encryptedOne = await fhevm .createEncryptedInput(fheCounterContractAddress, signers.alice.address) .add32(clearOne) .encrypt(); const tx = await fheCounterContract .connect(signers.alice) .increment(encryptedOne.handles[0], encryptedOne.inputProof); await tx.wait(); const encryptedCountAfterInc = await fheCounterContract.getCount(); const clearCountAfterInc = await fhevm.userDecryptEuint( FhevmType.euint32, encryptedCountAfterInc, fheCounterContractAddress, signers.alice, ); expect(clearCountAfterInc).to.eq(clearCountBeforeInc + clearOne); }); it("decrement the counter by 1", async function () { // Encrypt constant 1 as a euint32 const clearOne = 1; const encryptedOne = await fhevm .createEncryptedInput(fheCounterContractAddress, signers.alice.address) .add32(clearOne) .encrypt(); // First increment by 1, count becomes 1 let tx = await fheCounterContract .connect(signers.alice) .increment(encryptedOne.handles[0], encryptedOne.inputProof); await tx.wait(); // Then decrement by 1, count goes back to 0 tx = await fheCounterContract.connect(signers.alice).decrement(encryptedOne.handles[0], encryptedOne.inputProof); await tx.wait(); const encryptedCountAfterDec = await fheCounterContract.getCount(); const clearCountAfterDec = await fhevm.userDecryptEuint( FhevmType.euint32, encryptedCountAfterDec, fheCounterContractAddress, signers.alice, ); expect(clearCountAfterDec).to.eq(0); }); }); ``` {% endtab %} {% endtabs %} --- ## examples/fhe-encrypt-multiple-value.md > _From `examples/fhe-encrypt-multiple-value.md`_ This example demonstrates how to build an confidential counter using FHEVM, in comparison to a simple counter. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} ## A simple counter {% tabs %} {% tab title="counter.sol" %} ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title A simple counter contract contract Counter { uint32 private _count; /// @notice Returns the current count function getCount() external view returns (uint32) { return _count; } /// @notice Increments the counter by 1 function increment(uint32 value) external { _count += value; } /// @notice Decrements the counter by 1 function decrement(uint32 value) external { require(_count > value, "Counter: cannot decrement below zero"); _count -= value; } } ``` {% endtab %} {% tab title="counter.ts" %} ```ts import { Counter, Counter__factory } from "../types"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; type Signers = { deployer: HardhatEthersSigner; alice: HardhatEthersSigner; bob: HardhatEthersSigner; }; async function deployFixture() { const factory = (await ethers.getContractFactory("Counter")) as Counter__factory; const counterContract = (await factory.deploy()) as Counter; const counterContractAddress = await counterContract.getAddress(); return { counterContract, counterContractAddress }; } describe("Counter", function () { let signers: Signers; let counterContract: Counter; before(async function () { const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { deployer: ethSigners[0], alice: ethSigners[1], bob: ethSigners[2] }; }); beforeEach(async () => { ({ counterContract } = await deployFixture()); }); it("count should be zero after deployment", async function () { const count = await counterContract.getCount(); console.log(`Counter.getCount() === ${count}`); // Expect initial count to be 0 after deployment expect(count).to.eq(0); }); it("increment the counter by 1", async function () { const countBeforeInc = await counterContract.getCount(); const tx = await counterContract.connect(signers.alice).increment(1); await tx.wait(); const countAfterInc = await counterContract.getCount(); expect(countAfterInc).to.eq(countBeforeInc + 1n); }); it("decrement the counter by 1", async function () { // First increment, count becomes 1 let tx = await counterContract.connect(signers.alice).increment(); await tx.wait(); // Then decrement, count goes back to 0 tx = await counterContract.connect(signers.alice).decrement(1); await tx.wait(); const count = await counterContract.getCount(); expect(count).to.eq(0); }); }); ``` {% endtab %} {% endtabs %} ## An FHE counter {% tabs %} {% tab title="FHECounter.sol" %} ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { FHE, euint32, externalEuint32 } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; /// @title A simple FHE counter contract contract FHECounter is SepoliaConfig { euint32 private _count; /// @notice Returns the current count function getCount() external view returns (euint32) { return _count; } /// @notice Increments the counter by a specified encrypted value. /// @dev This example omits overflow/underflow checks for simplicity and readability. /// In a production contract, proper range checks should be implemented. function increment(externalEuint32 inputEuint32, bytes calldata inputProof) external { euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof); _count = FHE.add(_count, encryptedEuint32); FHE.allowThis(_count); FHE.allow(_count, msg.sender); } /// @notice Decrements the counter by a specified encrypted value. /// @dev This example omits overflow/underflow checks for simplicity and readability. /// In a production contract, proper range checks should be implemented. function decrement(externalEuint32 inputEuint32, bytes calldata inputProof) external { euint32 encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof); _count = FHE.sub(_count, encryptedEuint32); FHE.allowThis(_count); FHE.allow(_count, msg.sender); } } ``` {% endtab %} {% tab title="FHECounter.ts" %} ```ts import { FHECounter, FHECounter__factory } from "../types"; import { FhevmType } from "@fhevm/hardhat-plugin"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers, fhevm } from "hardhat"; type Signers = { deployer: HardhatEthersSigner; alice: HardhatEthersSigner; bob: HardhatEthersSigner; }; async function deployFixture() { const factory = (await ethers.getContractFactory("FHECounter")) as FHECounter__factory; const fheCounterContract = (await factory.deploy()) as FHECounter; const fheCounterContractAddress = await fheCounterContract.getAddress(); return { fheCounterContract, fheCounterContractAddress }; } describe("FHECounter", function () { let signers: Signers; let fheCounterContract: FHECounter; let fheCounterContractAddress: string; before(async function () { const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { deployer: ethSigners[0], alice: ethSigners[1], bob: ethSigners[2] }; }); beforeEach(async () => { ({ fheCounterContract, fheCounterContractAddress } = await deployFixture()); }); it("encrypted count should be uninitialized after deployment", async function () { const encryptedCount = await fheCounterContract.getCount(); // Expect initial count to be bytes32(0) after deployment, // (meaning the encrypted count value is uninitialized) expect(encryptedCount).to.eq(ethers.ZeroHash); }); it("increment the counter by 1", async function () { const encryptedCountBeforeInc = await fheCounterContract.getCount(); expect(encryptedCountBeforeInc).to.eq(ethers.ZeroHash); const clearCountBeforeInc = 0; // Encrypt constant 1 as a euint32 const clearOne = 1; const encryptedOne = await fhevm .createEncryptedInput(fheCounterContractAddress, signers.alice.address) .add32(clearOne) .encrypt(); const tx = await fheCounterContract .connect(signers.alice) .increment(encryptedOne.handles[0], encryptedOne.inputProof); await tx.wait(); const encryptedCountAfterInc = await fheCounterContract.getCount(); const clearCountAfterInc = await fhevm.userDecryptEuint( FhevmType.euint32, encryptedCountAfterInc, fheCounterContractAddress, signers.alice, ); expect(clearCountAfterInc).to.eq(clearCountBeforeInc + clearOne); }); it("decrement the counter by 1", async function () { // Encrypt constant 1 as a euint32 const clearOne = 1; const encryptedOne = await fhevm .createEncryptedInput(fheCounterContractAddress, signers.alice.address) .add32(clearOne) .encrypt(); // First increment by 1, count becomes 1 let tx = await fheCounterContract .connect(signers.alice) .increment(encryptedOne.handles[0], encryptedOne.inputProof); await tx.wait(); // Then decrement by 1, count goes back to 0 tx = await fheCounterContract.connect(signers.alice).decrement(encryptedOne.handles[0], encryptedOne.inputProof); await tx.wait(); const encryptedCountAfterDec = await fheCounterContract.getCount(); const clearCountAfterInc = await fhevm.userDecryptEuint( FhevmType.euint32, encryptedCountAfterDec, fheCounterContractAddress, signers.alice, ); expect(clearCountAfterInc).to.eq(0); }); }); ``` {% endtab %} {% endtabs %} --- ## examples/fhe-encrypt-multiple-values.md > _From `examples/fhe-encrypt-multiple-values.md`_ This example demonstrates the FHE encryption mechanism with multiple values. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="EncryptMultipleValues.sol" %} ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, externalEbool, externalEuint32, externalEaddress, ebool, euint32, eaddress } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; /** * This trivial example demonstrates the FHE encryption mechanism. */ contract EncryptMultipleValues is SepoliaConfig { ebool private _encryptedEbool; euint32 private _encryptedEuint32; eaddress private _encryptedEaddress; // solhint-disable-next-line no-empty-blocks constructor() {} function initialize( externalEbool inputEbool, externalEuint32 inputEuint32, externalEaddress inputEaddress, bytes calldata inputProof ) external { _encryptedEbool = FHE.fromExternal(inputEbool, inputProof); _encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof); _encryptedEaddress = FHE.fromExternal(inputEaddress, inputProof); // For each of the 3 values: // Grant FHE permission to both the contract itself (`address(this)`) and the caller (`msg.sender`), // to allow future decryption by the caller (`msg.sender`). FHE.allowThis(_encryptedEbool); FHE.allow(_encryptedEbool, msg.sender); FHE.allowThis(_encryptedEuint32); FHE.allow(_encryptedEuint32, msg.sender); FHE.allowThis(_encryptedEaddress); FHE.allow(_encryptedEaddress, msg.sender); } function encryptedBool() public view returns (ebool) { return _encryptedEbool; } function encryptedUint32() public view returns (euint32) { return _encryptedEuint32; } function encryptedAddress() public view returns (eaddress) { return _encryptedEaddress; } } ``` {% endtab %} {% tab title="EncryptMultipleValues.ts" %} ```ts //TODO; import { EncryptMultipleValues, EncryptMultipleValues__factory } from "../../../types"; import type { Signers } from "../../types"; import { FhevmType, HardhatFhevmRuntimeEnvironment } from "@fhevm/hardhat-plugin"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; import * as hre from "hardhat"; async function deployFixture() { // Contracts are deployed using the first signer/account by default const factory = (await ethers.getContractFactory("EncryptMultipleValues")) as EncryptMultipleValues__factory; const encryptMultipleValues = (await factory.deploy()) as EncryptMultipleValues; const encryptMultipleValues_address = await encryptMultipleValues.getAddress(); return { encryptMultipleValues, encryptMultipleValues_address }; } /** * This trivial example demonstrates the FHE encryption mechanism * and highlights a common pitfall developers may encounter. */ describe("EncryptMultipleValues", function () { let contract: EncryptMultipleValues; let contractAddress: string; let signers: Signers; before(async function () { // Check whether the tests are running against an FHEVM mock environment if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); } const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { owner: ethSigners[0], alice: ethSigners[1] }; }); beforeEach(async function () { // Deploy a new contract each time we run a new test const deployment = await deployFixture(); contractAddress = deployment.encryptMultipleValues_address; contract = deployment.encryptMultipleValues; }); // ✅ Test should succeed it("encryption should succeed", async function () { // Use the FHEVM Hardhat plugin runtime environment // to perform FHEVM input encryptions. const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; const input = fhevm.createEncryptedInput(contractAddress, signers.alice.address); input.addBool(true); input.add32(123456); input.addAddress(signers.owner.address); const enc = await input.encrypt(); const inputEbool = enc.handles[0]; const inputEuint32 = enc.handles[1]; const inputEaddress = enc.handles[2]; const inputProof = enc.inputProof; // Don't forget to call `connect(signers.alice)` to make sure // the Solidity `msg.sender` is `signers.alice.address`. const tx = await contract.connect(signers.alice).initialize(inputEbool, inputEuint32, inputEaddress, inputProof); await tx.wait(); const encryptedBool = await contract.encryptedBool(); const encryptedUint32 = await contract.encryptedUint32(); const encryptedAddress = await contract.encryptedAddress(); const clearBool = await fhevm.userDecryptEbool( encryptedBool, contractAddress, // The contract address signers.alice, // The user wallet ); const clearUint32 = await fhevm.userDecryptEuint( FhevmType.euint32, // Specify the encrypted type encryptedUint32, contractAddress, // The contract address signers.alice, // The user wallet ); const clearAddress = await fhevm.userDecryptEaddress( encryptedAddress, contractAddress, // The contract address signers.alice, // The user wallet ); expect(clearBool).to.equal(true); expect(clearUint32).to.equal(123456); expect(clearAddress).to.equal(signers.owner.address); }); }); ``` {% endtab %} {% endtabs %} --- ## examples/fhe-encrypt-single-value.md > _From `examples/fhe-encrypt-single-value.md`_ This example demonstrates the FHE encryption mechanism and highlights a common pitfall developers may encounter. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="EncryptSingleValue.sol" %} ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, externalEuint32, euint32 } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; /** * This trivial example demonstrates the FHE encryption mechanism. */ contract EncryptSingleValue is SepoliaConfig { euint32 private _encryptedEuint32; // solhint-disable-next-line no-empty-blocks constructor() {} function initialize(externalEuint32 inputEuint32, bytes calldata inputProof) external { _encryptedEuint32 = FHE.fromExternal(inputEuint32, inputProof); // Grant FHE permission to both the contract itself (`address(this)`) and the caller (`msg.sender`), // to allow future decryption by the caller (`msg.sender`). FHE.allowThis(_encryptedEuint32); FHE.allow(_encryptedEuint32, msg.sender); } function encryptedUint32() public view returns (euint32) { return _encryptedEuint32; } } ``` {% endtab %} {% tab title="EncryptSingleValue.ts" %} ```ts import { EncryptSingleValue, EncryptSingleValue__factory } from "../../../types"; import type { Signers } from "../../types"; import { FhevmType, HardhatFhevmRuntimeEnvironment } from "@fhevm/hardhat-plugin"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; import * as hre from "hardhat"; async function deployFixture() { // Contracts are deployed using the first signer/account by default const factory = (await ethers.getContractFactory("EncryptSingleValue")) as EncryptSingleValue__factory; const encryptSingleValue = (await factory.deploy()) as EncryptSingleValue; const encryptSingleValue_address = await encryptSingleValue.getAddress(); return { encryptSingleValue, encryptSingleValue_address }; } /** * This trivial example demonstrates the FHE encryption mechanism * and highlights a common pitfall developers may encounter. */ describe("EncryptSingleValue", function () { let contract: EncryptSingleValue; let contractAddress: string; let signers: Signers; before(async function () { // Check whether the tests are running against an FHEVM mock environment if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); } const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { owner: ethSigners[0], alice: ethSigners[1] }; }); beforeEach(async function () { // Deploy a new contract each time we run a new test const deployment = await deployFixture(); contractAddress = deployment.encryptSingleValue_address; contract = deployment.encryptSingleValue; }); // ✅ Test should succeed it("encryption should succeed", async function () { // Use the FHEVM Hardhat plugin runtime environment // to perform FHEVM input encryptions. const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; // 🔐 Encryption Process: // Values are encrypted locally and bound to a specific contract/user pair. // This grants the bound contract FHE permissions to receive and process the encrypted value, // but only when it is sent by the bound user. const input = fhevm.createEncryptedInput(contractAddress, signers.alice.address); // Add a uint32 value to the list of values to encrypt locally. input.add32(123456); // Perform the local encryption. This operation produces two components: // 1. `handles`: an array of FHEVM handles. In this case, a single handle associated with the // locally encrypted uint32 value `123456`. // 2. `inputProof`: a zero-knowledge proof that attests the `handles` are cryptographically // bound to the pair `[contractAddress, signers.alice.address]`. const enc = await input.encrypt(); // a 32-bytes FHEVM handle that represents a future Solidity `euint32` value. const inputEuint32 = enc.handles[0]; const inputProof = enc.inputProof; // Now `signers.alice.address` can send the encrypted value and its associated zero-knowledge proof // to the smart contract deployed at `contractAddress`. const tx = await contract.connect(signers.alice).initialize(inputEuint32, inputProof); await tx.wait(); // Let's try to decrypt it to check that everything is ok! const encryptedUint32 = await contract.encryptedUint32(); const clearUint32 = await fhevm.userDecryptEuint( FhevmType.euint32, // Specify the encrypted type encryptedUint32, contractAddress, // The contract address signers.alice, // The user wallet ); expect(clearUint32).to.equal(123456); }); // ❌ This test illustrates a very common pitfall it("encryption should fail", async function () { const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; const enc = await fhevm.createEncryptedInput(contractAddress, signers.alice.address).add32(123456).encrypt(); const inputEuint32 = enc.handles[0]; const inputProof = enc.inputProof; try { // Here is a very common error ! // `contract.initialize` will sign the Ethereum transaction using user `signers.owner` // instead of `signers.alice`. // // In the Solidity contract the following is checked: // - Is the contract allowed to manipulate `inputEuint32`? Answer is: ✅ yes! // - Is the sender allowed to manipulate `inputEuint32`? Answer is: ❌ no! Only `signers.alice` is! const tx = await contract.initialize(inputEuint32, inputProof); await tx.wait(); } catch { //console.log(e); } }); }); ``` {% endtab %} {% endtabs %} --- ## examples/fhe-public-decrypt-multiple-values.md > _From `examples/fhe-public-decrypt-multiple-values.md`_ This example demonstrates the FHE public decryption mechanism with multiple value. Public decryption is a mechanism that makes encrypted values visible to everyone once decrypted. Unlike user decryption where values remain private to authorized users, public decryption makes the data permanently visible to all participants. The public decryption call occurs onchain through smart contracts, making the decrypted value part of the blockchain's public state. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="PublicDecryptMultipleValues.sol" %} ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, ebool, euint32, euint64 } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; contract PublicDecryptMultipleValues is SepoliaConfig { ebool private _encryptedBool; // = 0 (uninitialized) euint32 private _encryptedUint32; // = 0 (uninitialized) euint64 private _encryptedUint64; // = 0 (uninitialized) bool private _clearBool; // = 0 (uninitialized) uint32 private _clearUint32; // = 0 (uninitialized) uint64 private _clearUint64; // = 0 (uninitialized) // solhint-disable-next-line no-empty-blocks constructor() {} function initialize(bool a, uint32 b, uint64 c) external { // Compute 3 trivial FHE formulas // _encryptedBool = a ^ false _encryptedBool = FHE.xor(FHE.asEbool(a), FHE.asEbool(false)); // _encryptedUint32 = b + 1 _encryptedUint32 = FHE.add(FHE.asEuint32(b), FHE.asEuint32(1)); // _encryptedUint64 = c + 1 _encryptedUint64 = FHE.add(FHE.asEuint64(c), FHE.asEuint64(1)); // see `DecryptSingleValueInSolidity.sol` for more detailed explanations // about FHE permissions and asynchronous public decryption requests. FHE.allowThis(_encryptedBool); FHE.allowThis(_encryptedUint32); FHE.allowThis(_encryptedUint64); } function requestDecryptMultipleValues() external { // To public decrypt multiple values, we must construct an array of the encrypted values // we want to public decrypt. // // ⚠️ Warning: The order of values in the array is critical! // The FHEVM backend will pass the public decrypted values to the callback function // in the exact same order they appear in this array. // Therefore, the order must match the parameter declaration in the callback. bytes32[] memory cypherTexts = new bytes32[](3); cypherTexts[0] = FHE.toBytes32(_encryptedBool); cypherTexts[1] = FHE.toBytes32(_encryptedUint32); cypherTexts[2] = FHE.toBytes32(_encryptedUint64); FHE.requestDecryption( // the list of encrypte values we want to public decrypt cypherTexts, // Selector of the Solidity callback function that the FHEVM backend will call with // the decrypted (clear) values as arguments this.callbackDecryptMultipleValues.selector ); } // ⚠️ WARNING: The `cleartexts` argument is an ABI encoding of the decrypted values associated // to the handles (using `abi.encode`). // // These values' types must match exactly! Mismatched types—such as using `uint32 decryptedUint64` // instead of the correct `uint64 decryptedUint64` can cause subtle and hard-to-detect bugs, // especially for developers new to the FHEVM stack. // Always ensure that the parameter types align with the expected decrypted value types. // // !DOUBLE-CHECK! function callbackDecryptMultipleValues( uint256 requestID, bytes memory cleartexts, bytes memory decryptionProof ) external { // ⚠️ Don't forget the signature checks! (see `DecryptSingleValueInSolidity.sol` for detailed explanations) // The signatures are included in the `decryptionProof` parameter. FHE.checkSignatures(requestID, cleartexts, decryptionProof); (bool decryptedBool, uint32 decryptedUint32, uint64 decryptedUint64) = abi.decode(cleartexts, (bool, uint32, uint64)); _clearBool = decryptedBool; _clearUint32 = decryptedUint32; _clearUint64 = decryptedUint64; } function clearBool() public view returns (bool) { return _clearBool; } function clearUint32() public view returns (uint32) { return _clearUint32; } function clearUint64() public view returns (uint64) { return _clearUint64; } } ``` {% endtab %} {% tab title="PublicDecryptMultipleValues.ts" %} ```ts import { PublicDecryptMultipleValues, PublicDecryptMultipleValues__factory } from "../../../types"; import type { Signers } from "../../types"; import { HardhatFhevmRuntimeEnvironment } from "@fhevm/hardhat-plugin"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; import * as hre from "hardhat"; async function deployFixture() { // Contracts are deployed using the first signer/account by default const factory = (await ethers.getContractFactory( "PublicDecryptMultipleValues", )) as PublicDecryptMultipleValues__factory; const publicDecryptMultipleValues = (await factory.deploy()) as PublicDecryptMultipleValues; const publicDecryptMultipleValues_address = await publicDecryptMultipleValues.getAddress(); return { publicDecryptMultipleValues, publicDecryptMultipleValues_address }; } /** * This trivial example demonstrates the FHE public decryption mechanism * and highlights a common pitfall developers may encounter. */ describe("PublicDecryptMultipleValues", function () { let contract: PublicDecryptMultipleValues; let signers: Signers; before(async function () { // Check whether the tests are running against an FHEVM mock environment if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); } const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { owner: ethSigners[0], alice: ethSigners[1] }; }); beforeEach(async function () { // Deploy a new contract each time we run a new test const deployment = await deployFixture(); contract = deployment.publicDecryptMultipleValues; }); // ✅ Test should succeed it("public decryption should succeed", async function () { // For simplicity, we create 3 trivialy encrypted values onchain. let tx = await contract.connect(signers.alice).initialize(true, 123456, 78901234567); await tx.wait(); tx = await contract.requestDecryptMultipleValues(); await tx.wait(); // We use the FHEVM Hardhat plugin to simulate the asynchronous onchain // public decryption const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; // Use the built-in `awaitDecryptionOracle` helper to wait for the FHEVM public decryption oracle // to complete all pending Solidity public decryption requests. await fhevm.awaitDecryptionOracle(); // At this point, the Solidity callback should have been invoked by the FHEVM backend. // We can now retrieve the 3 publicly decrypted (clear) values. const clearBool = await contract.clearBool(); const clearUint32 = await contract.clearUint32(); const clearUint64 = await contract.clearUint64(); expect(clearBool).to.equal(true); expect(clearUint32).to.equal(123456 + 1); expect(clearUint64).to.equal(78901234567 + 1); }); }); ``` {% endtab %} {% endtabs %} --- ## examples/fhe-public-decrypt-single-value.md > _From `examples/fhe-public-decrypt-single-value.md`_ This example demonstrates the FHE public decryption mechanism with a single value. Public decryption is a mechanism that makes encrypted values visible to everyone once decrypted. Unlike user decryption where values remain private to authorized users, public decryption makes the data permanently visible to all participants. The public decryption call occurs onchain through smart contracts, making the decrypted value part of the blockchain's public state. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="PublicDecryptSingleValue.sol" %} ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, euint32 } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; contract PublicDecryptSingleValue is SepoliaConfig { euint32 private _encryptedUint32; // = 0 (uninitizalized) uint32 private _clearUint32; // = 0 (uninitizalized) // solhint-disable-next-line no-empty-blocks constructor() {} function initializeUint32(uint32 value) external { // Compute a trivial FHE formula _trivialEuint32 = value + 1 _encryptedUint32 = FHE.add(FHE.asEuint32(value), FHE.asEuint32(1)); // Grant FHE permissions to: // ✅ The contract itself (`address(this)`): allows it to request async public decryption to the FHEVM backend // // Note: If you forget to call `FHE.allowThis(_trivialEuint32)`, // any async public decryption request of `_trivialEuint32` // by the contract itself (`address(this)`) will fail! FHE.allowThis(_encryptedUint32); } function initializeUint32Wrong(uint32 value) external { // Compute a trivial FHE formula _trivialEuint32 = value + 1 _encryptedUint32 = FHE.add(FHE.asEuint32(value), FHE.asEuint32(1)); } function requestDecryptSingleUint32() external { bytes32[] memory cypherTexts = new bytes32[](1); cypherTexts[0] = FHE.toBytes32(_encryptedUint32); // Two possible outcomes: // ✅ If `initializeUint32` was called, the public decryption request will succeed. // ❌ If `initializeUint32Wrong` was called, the public decryption request will fail 💥 // // Explanation: // The request succeeds only if the contract itself (`address(this)`) was granted // the necessary FHE permissions. Missing `FHE.allowThis(...)` will cause failure. FHE.requestDecryption( // the list of encrypte values we want to publc decrypt cypherTexts, // the function selector the FHEVM backend will callback with the clear values as arguments this.callbackDecryptSingleUint32.selector ); } function callbackDecryptSingleUint32(uint256 requestID, bytes memory cleartexts, bytes memory decryptionProof) external { // The `cleartexts` argument is an ABI encoding of the decrypted values associated to the // handles (using `abi.encode`). // // =============================== // ☠️🔒 SECURITY WARNING! 🔒☠️ // =============================== // // Must call `FHE.checkSignatures(...)` here! // ------------------------ // // This callback must only be called by the authorized FHEVM backend. // To enforce this, the contract author MUST verify the authenticity of the caller // by using the `FHE.checkSignatures` helper. This ensures that the provided signatures // match the expected FHEVM backend and prevents unauthorized or malicious calls. // // Failing to perform this verification allows anyone to invoke this function with // forged values, potentially compromising contract integrity. // // The responsibility for signature validation lies entirely with the contract author. // // The signatures are included in the `decryptionProof` parameter. // FHE.checkSignatures(requestID, cleartexts, decryptionProof); (uint32 decryptedInput) = abi.decode(cleartexts, (uint32)); _clearUint32 = decryptedInput; } function clearUint32() public view returns (uint32) { return _clearUint32; } } ``` {% endtab %} {% tab title="PublicDecryptSingleValue.ts" %} ```ts import { PublicDecryptSingleValue, PublicDecryptSingleValue__factory } from "../../../types"; import type { Signers } from "../../types"; import { HardhatFhevmRuntimeEnvironment } from "@fhevm/hardhat-plugin"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; import * as hre from "hardhat"; async function deployFixture() { // Contracts are deployed using the first signer/account by default const factory = (await ethers.getContractFactory( "PublicDecryptSingleValue", )) as PublicDecryptSingleValue__factory; const publicDecryptSingleValue = (await factory.deploy()) as PublicDecryptSingleValue; const publicDecryptSingleValue_address = await publicDecryptSingleValue.getAddress(); return { publicDecryptSingleValue, publicDecryptSingleValue_address }; } /** * This trivial example demonstrates the FHE public decryption mechanism * and highlights a common pitfall developers may encounter. */ describe("PublicDecryptSingleValue", function () { let contract: PublicDecryptSingleValue; let signers: Signers; before(async function () { // Check whether the tests are running against an FHEVM mock environment if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); } const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { owner: ethSigners[0], alice: ethSigners[1] }; }); beforeEach(async function () { // Deploy a new contract each time we run a new test const deployment = await deployFixture(); contract = deployment.publicDecryptSingleValue; }); // ✅ Test should succeed it("public decryption should succeed", async function () { let tx = await contract.connect(signers.alice).initializeUint32(123456); await tx.wait(); tx = await contract.requestDecryptSingleUint32(); await tx.wait(); // We use the FHEVM Hardhat plugin to simulate the asynchronous onchain // public decryption const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; // Use the built-in `awaitDecryptionOracle` helper to wait for the FHEVM public decryption oracle // to complete all pending Solidity public decryption requests. await fhevm.awaitDecryptionOracle(); // At this point, the Solidity callback should have been invoked by the FHEVM backend. // We can now retrieve the decrypted (clear) value. const clearUint32 = await contract.clearUint32(); expect(clearUint32).to.equal(123456 + 1); }); // ❌ Test should fail it("decryption should fail", async function () { const tx = await contract.connect(signers.alice).initializeUint32Wrong(123456); await tx.wait(); const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; const senderNotAllowedError = fhevm.revertedWithCustomErrorArgs("ACL", "SenderNotAllowed"); await expect(contract.connect(signers.alice).requestDecryptSingleUint32()).to.be.revertedWithCustomError( ...senderNotAllowedError, ); }); }); ``` {% endtab %} {% endtabs %} --- ## examples/fhe-user-decrypt-multiple-values.md > _From `examples/fhe-user-decrypt-multiple-values.md`_ This example demonstrates the FHE user decryption mechanism with multiple values. User decryption is a mechanism that allows specific users to decrypt encrypted values while keeping them hidden from others. Unlike public decryption where decrypted values become visible to everyone, user decryption maintains privacy by only allowing authorized users with the proper permissions to view the data. While permissions are granted onchain through smart contracts, the actual **decryption call occurs off-chain in the frontend application**. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="UserDecryptMultipleValues.sol" %} ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, ebool, euint32, euint64 } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; contract UserDecryptMultipleValues is SepoliaConfig { ebool private _encryptedBool; // = 0 (uninitizalized) euint32 private _encryptedUint32; // = 0 (uninitizalized) euint64 private _encryptedUint64; // = 0 (uninitizalized) // solhint-disable-next-line no-empty-blocks constructor() {} function initialize(bool a, uint32 b, uint64 c) external { // Compute 3 trivial FHE formulas // _encryptedBool = a ^ false _encryptedBool = FHE.xor(FHE.asEbool(a), FHE.asEbool(false)); // _encryptedUint32 = b + 1 _encryptedUint32 = FHE.add(FHE.asEuint32(b), FHE.asEuint32(1)); // _encryptedUint64 = c + 1 _encryptedUint64 = FHE.add(FHE.asEuint64(c), FHE.asEuint64(1)); // see `DecryptSingleValue.sol` for more detailed explanations // about FHE permissions and asynchronous user decryption requests. FHE.allowThis(_encryptedBool); FHE.allowThis(_encryptedUint32); FHE.allowThis(_encryptedUint64); FHE.allow(_encryptedBool, msg.sender); FHE.allow(_encryptedUint32, msg.sender); FHE.allow(_encryptedUint64, msg.sender); } function encryptedBool() public view returns (ebool) { return _encryptedBool; } function encryptedUint32() public view returns (euint32) { return _encryptedUint32; } function encryptedUint64() public view returns (euint64) { return _encryptedUint64; } } ``` {% endtab %} {% tab title="UserDecryptMultipleValues.ts" %} ```ts import { UserDecryptMultipleValues, UserDecryptMultipleValues__factory } from "../../../types"; import type { Signers } from "../../types"; import { HardhatFhevmRuntimeEnvironment } from "@fhevm/hardhat-plugin"; import { utils as fhevm_utils } from "@fhevm/mock-utils"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { DecryptedResults } from "@zama-fhe/relayer-sdk"; import { expect } from "chai"; import { ethers } from "hardhat"; import * as hre from "hardhat"; async function deployFixture() { // Contracts are deployed using the first signer/account by default const factory = (await ethers.getContractFactory("UserDecryptMultipleValues")) as UserDecryptMultipleValues__factory; const userDecryptMultipleValues = (await factory.deploy()) as UserDecryptMultipleValues; const userDecryptMultipleValues_address = await userDecryptMultipleValues.getAddress(); return { userDecryptMultipleValues, userDecryptMultipleValues_address }; } /** * This trivial example demonstrates the FHE user decryption mechanism * and highlights a common pitfall developers may encounter. */ describe("UserDecryptMultipleValues", function () { let contract: UserDecryptMultipleValues; let contractAddress: string; let signers: Signers; before(async function () { // Check whether the tests are running against an FHEVM mock environment if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); } const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { owner: ethSigners[0], alice: ethSigners[1] }; }); beforeEach(async function () { // Deploy a new contract each time we run a new test const deployment = await deployFixture(); contractAddress = deployment.userDecryptMultipleValues_address; contract = deployment.userDecryptMultipleValues; }); // ✅ Test should succeed it("user decryption should succeed", async function () { const tx = await contract.connect(signers.alice).initialize(true, 123456, 78901234567); await tx.wait(); const encryptedBool = await contract.encryptedBool(); const encryptedUint32 = await contract.encryptedUint32(); const encryptedUint64 = await contract.encryptedUint64(); // The FHEVM Hardhat plugin provides a set of convenient helper functions // that make it easy to perform FHEVM operations within your Hardhat environment. const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; const aliceKeypair = fhevm.generateKeypair(); const startTimestamp = fhevm_utils.timestampNow(); const durationDays = 365; const aliceEip712 = fhevm.createEIP712(aliceKeypair.publicKey, [contractAddress], startTimestamp, durationDays); const aliceSignature = await signers.alice.signTypedData( aliceEip712.domain, { UserDecryptRequestVerification: aliceEip712.types.UserDecryptRequestVerification }, aliceEip712.message, ); const decrytepResults: DecryptedResults = await fhevm.userDecrypt( [ { handle: encryptedBool, contractAddress: contractAddress }, { handle: encryptedUint32, contractAddress: contractAddress }, { handle: encryptedUint64, contractAddress: contractAddress }, ], aliceKeypair.privateKey, aliceKeypair.publicKey, aliceSignature, [contractAddress], signers.alice.address, startTimestamp, durationDays, ); expect(decrytepResults[encryptedBool]).to.equal(true); expect(decrytepResults[encryptedUint32]).to.equal(123456 + 1); expect(decrytepResults[encryptedUint64]).to.equal(78901234567 + 1); }); }); ``` {% endtab %} {% endtabs %} --- ## examples/fhe-user-decrypt-single-value.md > _From `examples/fhe-user-decrypt-single-value.md`_ This example demonstrates the FHE user decryption mechanism with a single value. User decryption is a mechanism that allows specific users to decrypt encrypted values while keeping them hidden from others. Unlike public decryption where decrypted values become visible to everyone, user decryption maintains privacy by only allowing authorized users with the proper permissions to view the data. While permissions are granted onchain through smart contracts, the actual **decryption call occurs off-chain in the frontend application**. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="UserDecryptSingleValue.sol" %} ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, euint32 } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; /** * This trivial example demonstrates the FHE decryption mechanism * and highlights common pitfalls developers may encounter. */ contract UserDecryptSingleValue is SepoliaConfig { euint32 private _trivialEuint32; // solhint-disable-next-line no-empty-blocks constructor() {} function initializeUint32(uint32 value) external { // Compute a trivial FHE formula _trivialEuint32 = value + 1 _trivialEuint32 = FHE.add(FHE.asEuint32(value), FHE.asEuint32(1)); // Grant FHE permissions to: // ✅ The contract caller (`msg.sender`): allows them to decrypt `_trivialEuint32`. // ✅ The contract itself (`address(this)`): allows it to operate on `_trivialEuint32` and // also enables the caller to perform user decryption. // // Note: If you forget to call `FHE.allowThis(_trivialEuint32)`, the user will NOT be able // to user decrypt the value! Both the contract and the caller must have FHE permissions // for user decryption to succeed. FHE.allowThis(_trivialEuint32); FHE.allow(_trivialEuint32, msg.sender); } function initializeUint32Wrong(uint32 value) external { // Compute a trivial FHE formula _trivialEuint32 = value + 1 _trivialEuint32 = FHE.add(FHE.asEuint32(value), FHE.asEuint32(1)); // ❌ Common FHE permission mistake: // ================================================================ // We grant FHE permissions to the contract caller (`msg.sender`), // expecting they will be able to user decrypt the encrypted value later. // // However, this will fail! 💥 // The contract itself (`address(this)`) also needs FHE permissions to allow user decryption. // Without granting the contract access using `FHE.allowThis(...)`, // the user decryption attempt by the user will not succeed. FHE.allow(_trivialEuint32, msg.sender); } function encryptedUint32() public view returns (euint32) { return _trivialEuint32; } } ``` {% endtab %} {% tab title="UserDecryptSingleValue.ts" %} ```ts import { UserDecryptSingleValue, UserDecryptSingleValue__factory } from "../../../types"; import type { Signers } from "../../types"; import { FhevmType, HardhatFhevmRuntimeEnvironment } from "@fhevm/hardhat-plugin"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; import * as hre from "hardhat"; async function deployFixture() { // Contracts are deployed using the first signer/account by default const factory = (await ethers.getContractFactory("UserDecryptSingleValue")) as UserDecryptSingleValue__factory; const userUserDecryptSingleValue = (await factory.deploy()) as UserDecryptSingleValue; const userUserDecryptSingleValue_address = await userUserDecryptSingleValue.getAddress(); return { userUserDecryptSingleValue, userUserDecryptSingleValue_address }; } /** * This trivial example demonstrates the FHE user decryption mechanism * and highlights a common pitfall developers may encounter. */ describe("UserDecryptSingleValue", function () { let contract: UserDecryptSingleValue; let contractAddress: string; let signers: Signers; before(async function () { // Check whether the tests are running against an FHEVM mock environment if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); } const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { owner: ethSigners[0], alice: ethSigners[1] }; }); beforeEach(async function () { // Deploy a new contract each time we run a new test const deployment = await deployFixture(); contractAddress = deployment.userUserDecryptSingleValue_address; contract = deployment.userUserDecryptSingleValue; }); // ✅ Test should succeed it("user decryption should succeed", async function () { const tx = await contract.connect(signers.alice).initializeUint32(123456); await tx.wait(); const encryptedUint32 = await contract.encryptedUint32(); // The FHEVM Hardhat plugin provides a set of convenient helper functions // that make it easy to perform FHEVM operations within your Hardhat environment. const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; const clearUint32 = await fhevm.userDecryptEuint( FhevmType.euint32, // Specify the encrypted type encryptedUint32, contractAddress, // The contract address signers.alice, // The user wallet ); expect(clearUint32).to.equal(123456 + 1); }); // ❌ Test should fail it("user decryption should fail", async function () { const tx = await contract.connect(signers.alice).initializeUint32Wrong(123456); await tx.wait(); const encryptedUint32 = await contract.encryptedUint32(); await expect( hre.fhevm.userDecryptEuint(FhevmType.euint32, encryptedUint32, contractAddress, signers.alice), ).to.be.rejectedWith(new RegExp("^dapp contract (.+) is not authorized to user decrypt handle (.+).")); }); }); ``` {% endtab %} {% endtabs %} --- ## examples/fheadd.md > _From `examples/fheadd.md`_ This example demonstrates how to write a simple "a + b" contract using FHEVM. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="FHEAdd.sol" %} ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, euint8, externalEuint8 } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; contract FHEAdd is SepoliaConfig { euint8 private _a; euint8 private _b; // solhint-disable-next-line var-name-mixedcase euint8 private _a_plus_b; // solhint-disable-next-line no-empty-blocks constructor() {} function setA(externalEuint8 inputA, bytes calldata inputProof) external { _a = FHE.fromExternal(inputA, inputProof); FHE.allowThis(_a); } function setB(externalEuint8 inputB, bytes calldata inputProof) external { _b = FHE.fromExternal(inputB, inputProof); FHE.allowThis(_b); } function computeAPlusB() external { // The sum `a + b` is computed by the contract itself (`address(this)`). // Since the contract has FHE permissions over both `a` and `b`, // it is authorized to perform the `FHE.add` operation on these values. // It does not matter if the contract caller (`msg.sender`) has FHE permission or not. _a_plus_b = FHE.add(_a, _b); // At this point the contract ifself (`address(this)`) has been granted ephemeral FHE permission // over `_a_plus_b`. This FHE permission will be revoked when the function exits. // // Now, to make sure `_a_plus_b` can be decrypted by the contract caller (`msg.sender`), // we need to grant permanent FHE permissions to both the contract ifself (`address(this)`) // and the contract caller (`msg.sender`) FHE.allowThis(_a_plus_b); FHE.allow(_a_plus_b, msg.sender); } function result() public view returns (euint8) { return _a_plus_b; } } ``` {% endtab %} {% tab title="FHEAdd.ts" %} ```ts import { FHEAdd, FHEAdd__factory } from "../../../types"; import type { Signers } from "../../types"; import { FhevmType, HardhatFhevmRuntimeEnvironment } from "@fhevm/hardhat-plugin"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; import * as hre from "hardhat"; async function deployFixture() { // Contracts are deployed using the first signer/account by default const factory = (await ethers.getContractFactory("FHEAdd")) as FHEAdd__factory; const fheAdd = (await factory.deploy()) as FHEAdd; const fheAdd_address = await fheAdd.getAddress(); return { fheAdd, fheAdd_address }; } /** * This trivial example demonstrates the FHE encryption mechanism * and highlights a common pitfall developers may encounter. */ describe("FHEAdd", function () { let contract: FHEAdd; let contractAddress: string; let signers: Signers; let bob: HardhatEthersSigner; before(async function () { // Check whether the tests are running against an FHEVM mock environment if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); } const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { owner: ethSigners[0], alice: ethSigners[1] }; bob = ethSigners[2]; }); beforeEach(async function () { // Deploy a new contract each time we run a new test const deployment = await deployFixture(); contractAddress = deployment.fheAdd_address; contract = deployment.fheAdd; }); it("a + b should succeed", async function () { const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; let tx; // Let's compute 80 + 123 = 203 const a = 80; const b = 123; // Alice encrypts and sets `a` as 80 const inputA = await fhevm.createEncryptedInput(contractAddress, signers.alice.address).add8(a).encrypt(); tx = await contract.connect(signers.alice).setA(inputA.handles[0], inputA.inputProof); await tx.wait(); // Alice encrypts and sets `b` as 203 const inputB = await fhevm.createEncryptedInput(contractAddress, signers.alice.address).add8(b).encrypt(); tx = await contract.connect(signers.alice).setB(inputB.handles[0], inputB.inputProof); await tx.wait(); // Why Bob has FHE permissions to execute the operation in this case ? // See `computeAPlusB()` in `FHEAdd.sol` for a detailed answer tx = await contract.connect(bob).computeAPlusB(); await tx.wait(); const encryptedAplusB = await contract.result(); const clearAplusB = await fhevm.userDecryptEuint( FhevmType.euint8, // Specify the encrypted type encryptedAplusB, contractAddress, // The contract address bob, // The user wallet ); expect(clearAplusB).to.equal(a + b); }); }); ``` {% endtab %} {% endtabs %} --- ## examples/fheifthenelse.md > _From `examples/fheifthenelse.md`_ This example demonstrates how to write a simple contract with conditions using FHEVM, in comparison to a simple counter. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="FHEIfThenElse.sol" %} ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, ebool, euint8, externalEuint8 } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; contract FHEIfThenElse is SepoliaConfig { euint8 private _a; euint8 private _b; euint8 private _max; // solhint-disable-next-line no-empty-blocks constructor() {} function setA(externalEuint8 inputA, bytes calldata inputProof) external { _a = FHE.fromExternal(inputA, inputProof); FHE.allowThis(_a); } function setB(externalEuint8 inputB, bytes calldata inputProof) external { _b = FHE.fromExternal(inputB, inputProof); FHE.allowThis(_b); } function computeMax() external { // a >= b // solhint-disable-next-line var-name-mixedcase ebool _a_ge_b = FHE.ge(_a, _b); // a >= b ? a : b _max = FHE.select(_a_ge_b, _a, _b); // For more information about FHE permissions in this case, // read the `computeAPlusB()` commentaries in `FHEAdd.sol`. FHE.allowThis(_max); FHE.allow(_max, msg.sender); } function result() public view returns (euint8) { return _max; } } ``` {% endtab %} {% tab title="FHEIfThenElse.ts" %} ```ts import { FHEIfThenElse, FHEIfThenElse__factory } from "../../../types"; import type { Signers } from "../../types"; import { FhevmType, HardhatFhevmRuntimeEnvironment } from "@fhevm/hardhat-plugin"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; import * as hre from "hardhat"; async function deployFixture() { // Contracts are deployed using the first signer/account by default const factory = (await ethers.getContractFactory("FHEIfThenElse")) as FHEIfThenElse__factory; const fheIfThenElse = (await factory.deploy()) as FHEIfThenElse; const fheIfThenElse_address = await fheIfThenElse.getAddress(); return { fheIfThenElse, fheIfThenElse_address }; } /** * This trivial example demonstrates the FHE encryption mechanism * and highlights a common pitfall developers may encounter. */ describe("FHEIfThenElse", function () { let contract: FHEIfThenElse; let contractAddress: string; let signers: Signers; let bob: HardhatEthersSigner; before(async function () { // Check whether the tests are running against an FHEVM mock environment if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); } const ethSigners: HardhatEthersSigner[] = await ethers.getSigners(); signers = { owner: ethSigners[0], alice: ethSigners[1] }; bob = ethSigners[2]; }); beforeEach(async function () { // Deploy a new contract each time we run a new test const deployment = await deployFixture(); contractAddress = deployment.fheIfThenElse_address; contract = deployment.fheIfThenElse; }); it("a >= b ? a : b should succeed", async function () { const fhevm: HardhatFhevmRuntimeEnvironment = hre.fhevm; let tx; // Let's compute `a >= b ? a : b` const a = 80; const b = 123; // Alice encrypts and sets `a` as 80 const inputA = await fhevm.createEncryptedInput(contractAddress, signers.alice.address).add8(a).encrypt(); tx = await contract.connect(signers.alice).setA(inputA.handles[0], inputA.inputProof); await tx.wait(); // Alice encrypts and sets `b` as 203 const inputB = await fhevm.createEncryptedInput(contractAddress, signers.alice.address).add8(b).encrypt(); tx = await contract.connect(signers.alice).setB(inputB.handles[0], inputB.inputProof); await tx.wait(); // Why Bob has FHE permissions to execute the operation in this case ? // See `computeAPlusB()` in `FHEAdd.sol` for a detailed answer tx = await contract.connect(bob).computeMax(); await tx.wait(); const encryptedMax = await contract.result(); const clearMax = await fhevm.userDecryptEuint( FhevmType.euint8, // Specify the encrypted type encryptedMax, contractAddress, // The contract address bob, // The user wallet ); expect(clearMax).to.equal(a >= b ? a : b); }); }); ``` {% endtab %} {% endtabs %} --- ## examples/legacy/see-all-tutorials.md > _From `examples/legacy/see-all-tutorials.md`_ # See all tutorials ## Solidity smart contracts templates - `fhevm-contracts` (Legacy) The [fhevm-contracts repository](https://github.com/zama-ai/fhevm-contracts) provides a comprehensive collection of secure, pre-tested Solidity templates optimized for FHEVM development. These templates leverage the FHE library to enable encrypted computations while maintaining security and extensibility. The library includes templates for common use cases like tokens and governance, allowing developers to quickly build confidential smart contracts with battle-tested components. For detailed implementation guidance and best practices, refer to the [contracts standard library guide](../smart_contracts/contracts.md). #### Token - [ConfidentialERC20](https://github.com/zama-ai/fhevm-contracts/blob/main/contracts/token/ERC20/ConfidentialERC20.sol): Standard ERC20 with encryption. - [ConfidentialERC20Mintable](https://github.com/zama-ai/fhevm-contracts/blob/main/contracts/token/ERC20/extensions/ConfidentialERC20Mintable.sol): ERC20 with minting capabilities. - [ConfidentialERC20WithErrors](https://github.com/zama-ai/fhevm-contracts/blob/main/contracts/token/ERC20/extensions/ConfidentialERC20WithErrors.sol): ERC20 with integrated error handling. - [ConfidentialERC20WithErrorsMintable](https://github.com/zama-ai/fhevm-contracts/blob/main/contracts/token/ERC20/extensions/ConfidentialERC20WithErrorsMintable.sol): ERC20 with both minting and error handling. #### Governance - [ConfidentialERC20Votes](https://github.com/zama-ai/fhevm-contracts/blob/main/contracts/governance/ConfidentialERC20Votes.sol): Confidential ERC20 governance token implementation. [It is based on Comp.sol](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/Comp.sol). - [ConfidentialGovernorAlpha](https://github.com/zama-ai/fhevm-contracts/blob/main/contracts/governance/ConfidentialGovernorAlpha.sol): A governance contract for managing proposals and votes. [It is based on GovernorAlpha.sol](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/GovernorAlpha.sol). #### Utils - [EncryptedErrors](https://github.com/zama-ai/fhevm-contracts/blob/main/contracts/utils/EncryptedErrors.sol): Provides error management utilities for encrypted contracts. ## Code examples on GitHub - [Blind Auction](https://github.com/zama-ai/dapps/tree/main/hardhat/contracts/auctions): A smart contract for conducting blind auctions where bids are encrypted and the winning bid remains private. - [Decentralized ID](https://github.com/zama-ai/dapps/tree/main/hardhat/contracts/decIdentity): A blockchain-based identity management system using smart contracts to store and manage encrypted personal data. - [FheWordle](https://github.com/zama-ai/dapps/tree/main/hardhat/contracts/fheWordle): A privacy-preserving implementation of the popular word game Wordle where players guess a secret encrypted word through encrypted letter comparisons. - [Cipherbomb](https://github.com/immortal-tofu/cipherbomb): A multiplayer game where players must defuse an encrypted bomb by guessing the correct sequence of numbers. - [Voting example](https://github.com/allemanfredi/suffragium): Suffragium is a secure, privacy-preserving voting system that combines zero-knowledge proofs (ZKP) and Fully Homomorphic Encryption (FHE) to create a trustless and tamper-resistant voting platform. ## Frontend examples - [Cipherbomb UI](https://github.com/immortal-tofu/cipherbomb-ui): A multiplayer game where players must defuse an encrypted bomb by guessing the correct sequence of numbers. ## Blog tutorials - [Suffragium: An Encrypted Onchain Voting System Leveraging ZK and FHE Using fhevm](https://www.zama.ai/post/encrypted-onchain-voting-using-zk-and-fhe-with-zama-fhevm) - Nov 2024 ## Video tutorials - [How to do Confidential Transactions Directly on Ethereum?](https://www.youtube.com/watch?v=aDv2WYOpVqA) - Nov 2024 - [Zama - FHE on Ethereum (Presentation at The Zama CoFHE Shop during EthCC 7)](https://www.youtube.com/watch?v=WngC5cvV_fc&ab_channel=Zama) - Jul 2024 ### Legacy - Not compatible with latest FHEVM - [Build an Encrypted Wordle Game Onchain using FHE and FHEVM](https://www.zama.ai/post/build-an-encrypted-wordle-game-onchain-using-fhe-and-zama-fhevm) - February 2024 - [Programmable Privacy and Onchain Compliance using Homomorphic Encryption](https://www.zama.ai/post/programmable-privacy-and-onchain-compliance-using-homomorphic-encryption) - November 2023 - [Confidential DAO Voting Using Homomorphic Encryption](https://www.zama.ai/post/confidential-dao-voting-using-homomorphic-encryption) - October 2023 - [On-chain Blind Auctions Using Homomorphic Encryption and the FHEVM](https://www.zama.ai/post/on-chain-blind-auctions-using-homomorphic-encryption) - July 2023 - [Confidential ERC-20 Tokens Using Homomorphic Encryption and the FHEVM](https://www.zama.ai/post/confidential-erc-20-tokens-using-homomorphic-encryption) - June 2023 - [Using asynchronous decryption in Solidity contracts with FHEVM](https://www.zama.ai/post/video-tutorial-using-asynchronous-decryption-in-solidity-contracts-with-FHEVM) - April 2024 - [Accelerate your code testing and get code coverage using FHEVM mocks](https://www.zama.ai/post/video-tutorial-accelerate-your-code-testing-and-get-code-coverage-using-fhevm-mocks) - January 2024 - [Use the CMUX operator on fhevm](https://www.youtube.com/watch?v=7icM0EOSvU0) - October 2023 - [\[Video tutorial\] How to Write Confidential Smart Contracts Using fhevm](https://www.zama.ai/post/video-tutorial-how-to-write-confidential-smart-contracts-using-zamas-fhevm) - October 2023 - [Workshop during ETHcc: Homomorphic Encryption in the EVM](https://www.youtube.com/watch?v=eivfVykPP8U) - July 2023 --- ## examples/openzeppelin/erc7984-tutorial.md > _From `examples/openzeppelin/erc7984-tutorial.md`_ This tutorial explains how to create a confidential fungible token using Fully Homomorphic Encryption (FHE) and the OpenZeppelin smart contract library. By following this guide, you will learn how to build a token where balances and transactions remain encrypted while maintaining full functionality. ## Why FHE for confidential tokens? Confidential tokens make sense in many real-world scenarios: - **Privacy**: Users can transact without revealing their exact balances or transaction amounts - **Regulatory Compliance**: Maintains privacy while allowing for selective disclosure when needed - **Business Intelligence**: Companies can keep their token holdings private from competitors - **Personal Privacy**: Individuals can participate in DeFi without exposing their financial position - **Audit Trail**: All transactions are still recorded on-chain, just in encrypted form FHE enables these benefits by allowing computations on encrypted data without decryption, ensuring privacy while maintaining the security and transparency of blockchain. # Project Setup Before starting this tutorial, ensure you have: 1. Installed the FHEVM hardhat template 2. Set up the OpenZeppelin confidential contracts library For help with these steps, refer to the following tutorial: - [Setting up OpenZeppelin confidential contracts](./openzeppelin/README.md) ## Understanding the architecture Our confidential token will inherit from several key contracts: 1. **`ERC7984`** - OpenZeppelin's base for confidential tokens 2. **`Ownable2Step`** - Access control for minting and administrative functions 3. **`SepoliaConfig`** - FHE configuration for the Sepolia testnet ## The base smart contract Let's create our confidential token contract in `contracts/ERC7984Example.sol`. This contract will demonstrate the core functionality of ERC7984 tokens. A few key points about this implementation: - The contract mints an initial supply with a clear (non-encrypted) amount during deployment - The initial mint is done once during construction, establishing the token's total supply - All subsequent transfers will be fully encrypted, preserving privacy - The contract inherits from ERC7984 for confidential token functionality and Ownable2Step for secure access control While this example uses a clear initial mint for simplicity, in production you may want to consider: - Using encrypted minting for complete privacy from genesis - Implementing a more sophisticated minting schedule - Overriding some privacy assumptions ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {FHE, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol"; import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; import {ERC7984} from "@openzeppelin/confidential-contracts/token/ERC7984.sol"; contract ERC7984Example is SepoliaConfig, ERC7984, Ownable2Step { constructor( address owner, uint64 amount, string memory name_, string memory symbol_, string memory tokenURI_ ) ERC7984(name_, symbol_, tokenURI_) Ownable(owner) { euint64 encryptedAmount = FHE.asEuint64(amount); _mint(owner, encryptedAmount); } } ``` ## Test workflow Now let's test the token transfer process. We'll create a test that: 1. Encrypts a transfer amount 2. Sends tokens from owner to recipient 3. Verifies the transfer was successful by checking balance handles Create a new file `test/ERC7984Example.test.ts` with the following test: ```ts import { expect } from 'chai'; import { ethers, fhevm } from 'hardhat'; describe('ERC7984Example', function () { let token: any; let owner: any; let recipient: any; let other: any; const INITIAL_AMOUNT = 1000; const TRANSFER_AMOUNT = 100; beforeEach(async function () { [owner, recipient, other] = await ethers.getSigners(); // Deploy ERC7984Example contract token = await ethers.deployContract('ERC7984Example', [ owner.address, INITIAL_AMOUNT, 'Confidential Token', 'CTKN', 'https://example.com/token' ]); }); describe('Confidential Transfer Process', function () { it('should transfer tokens from owner to recipient', async function () { // Create encrypted input for transfer amount const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(TRANSFER_AMOUNT) .encrypt(); // Perform the confidential transfer await expect(token .connect(owner) ['confidentialTransfer(address,bytes32,bytes)']( recipient.address, encryptedInput.handles[0], encryptedInput.inputProof )).to.not.be.reverted; // Check that both addresses have balance handles (without decryption for now) const recipientBalanceHandle = await token.confidentialBalanceOf(recipient.address); const ownerBalanceHandle = await token.confidentialBalanceOf(owner.address); expect(recipientBalanceHandle).to.not.be.undefined; expect(ownerBalanceHandle).to.not.be.undefined; }); }); }); ``` To run the tests, use: ```bash npx hardhat test test/ERC7984Example.test.ts ``` ## Advanced features and extensions The basic ERC7984Example contract provides core functionality, but you can extend it with additional features. For example: ### Minting functions **Visible Mint** - Allows the owner to mint tokens with a clear amount: ```solidity function mint(address to, uint64 amount) external onlyOwner { _mint(to, FHE.asEuint64(amount)); } ``` - **When to use**: Prefer this for public/tokenomics-driven mints where transparency is desired (e.g., scheduled emissions). - **Privacy caveat**: The minted amount is visible in calldata and events; use `confidentialMint` for privacy. - **Access control**: Consider replacing `onlyOwner` with role-based access via `AccessControl` (e.g., `MINTER_ROLE`) for multi-signer workflows. - **Supply caps**: If you need a hard cap, add a check before `_mint` and enforce it consistently for both visible and confidential flows. **Confidential Mint** - Allows minting with encrypted amounts for enhanced privacy: ```solidity function confidentialMint( address to, externalEuint64 encryptedAmount, bytes calldata inputProof ) external onlyOwner returns (euint64 transferred) { return _mint(to, FHE.fromExternal(encryptedAmount, inputProof)); } ``` - **Inputs**: `encryptedAmount` and `inputProof` are produced off-chain with the SDK. Always validate and revert on malformed inputs. - **Gas considerations**: Confidential operations cost more gas; batch mints sparingly and prefer fewer larger mints to reduce overhead. - **Auditing**: While amounts stay private, you still get a verifiable audit trail of mints (timestamps, sender, recipient). - **Example (Hardhat SDK)**: ```ts const enc = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(1_000) .encrypt(); await token.confidentialMint(recipient.address, enc.handles[0], enc.inputProof); ``` ### Burning functions **Visible Burn** - Allows the owner to burn tokens with a clear amount: ```solidity function burn(address from, uint64 amount) external onlyOwner { _burn(from, FHE.asEuint64(amount)); } ``` **Confidential Burn** - Allows burning with encrypted amounts: ```solidity function confidentialBurn( address from, externalEuint64 encryptedAmount, bytes calldata inputProof ) external onlyOwner returns (euint64 transferred) { return _burn(from, FHE.fromExternal(encryptedAmount, inputProof)); } ``` - **Authorization**: Burning from arbitrary accounts is powerful; consider stronger controls (roles, multisig, timelocks) or user-consented burns. - **Event strategy**: Decide whether to emit custom events revealing intent (not amounts) for better observability and offchain indexing. - **Error surfaces**: Expect balance/allowance-like failures if encrypted amount exceeds balance; test both success and revert paths. - **Example (Hardhat SDK)**: ```ts const enc = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(250) .encrypt(); await token.confidentialBurn(holder.address, enc.handles[0], enc.inputProof); ``` ### Total supply visibility If you want the owner to be able to view the total supply (useful for administrative purposes): ```solidity function _update(address from, address to, euint64 amount) internal virtual override returns (euint64 transferred) { transferred = super._update(from, to, amount); FHE.allow(confidentialTotalSupply(), owner()); } ``` - **What this does**: Grants the `owner` permission to decrypt the latest total supply handle after every state-changing update. - **Operational model**: The owner can call `confidentialTotalSupply()` and use their off-chain key material to decrypt the returned handle. - **Security considerations**: - If ownership changes, ensure only the new owner can decrypt going forward. With `Ownable2Step`, this function will automatically allow the current `owner()`. - Be mindful of compliance: granting supply visibility may be considered privileged access; document who holds the key and why. - **Alternatives**: If you want organization-wide access, grant via a dedicated admin contract that holds decryption authority instead of a single EOA. --- ## examples/openzeppelin/erc7984.md > _From `examples/openzeppelin/erc7984.md`_ This example demonstrates how to create a confidential token using OpenZeppelin's smart contract library powered by ZAMA's FHEVM. {% hint style="info" %} To run this example correctly, make sure you clone the [fhevm-hardhat-template](https://github.com/zama-ai/fhevm-hardhat-template) and that the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="ERC7984Example.sol" %} ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {FHE, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol"; import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; import {ERC7984} from "@openzeppelin/confidential-contracts/token/ERC7984.sol"; contract ERC7984Example is SepoliaConfig, ERC7984, Ownable2Step { constructor( address owner, uint64 amount, string memory name_, string memory symbol_, string memory tokenURI_ ) ERC7984(name_, symbol_, tokenURI_) Ownable(owner) { euint64 encryptedAmount = FHE.asEuint64(amount); _mint(owner, encryptedAmount); } } ``` {% endtab %} {% tab title="ERC7984Example.test.ts" %} ```ts import { expect } from 'chai'; import { ethers, fhevm } from 'hardhat'; describe('ERC7984Example', function () { let token: any; let owner: any; let recipient: any; let other: any; const INITIAL_AMOUNT = 1000; const TRANSFER_AMOUNT = 100; beforeEach(async function () { [owner, recipient, other] = await ethers.getSigners(); // Deploy ERC7984Example contract token = await ethers.deployContract('ERC7984Example', [ owner.address, INITIAL_AMOUNT, 'Confidential Token', 'CTKN', 'https://example.com/token' ]); }); describe('Initialization', function () { it('should set the correct name', async function () { expect(await token.name()).to.equal('Confidential Token'); }); it('should set the correct symbol', async function () { expect(await token.symbol()).to.equal('CTKN'); }); it('should set the correct token URI', async function () { expect(await token.tokenURI()).to.equal('https://example.com/token'); }); it('should mint initial amount to owner', async function () { // Verify that the owner has a balance (without decryption for now) const balanceHandle = await token.confidentialBalanceOf(owner.address); expect(balanceHandle).to.not.be.undefined; }); }); describe('Transfer Process', function () { it('should transfer tokens from owner to recipient', async function () { // Create encrypted input for transfer amount const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(TRANSFER_AMOUNT) .encrypt(); // Perform the transfer await expect(token .connect(owner) ['confidentialTransfer(address,bytes32,bytes)']( recipient.address, encryptedInput.handles[0], encryptedInput.inputProof )).to.not.be.reverted; // Check that both addresses have balance handles (without decryption for now) const recipientBalanceHandle = await token.confidentialBalanceOf(recipient.address); const ownerBalanceHandle = await token.confidentialBalanceOf(owner.address); expect(recipientBalanceHandle).to.not.be.undefined; expect(ownerBalanceHandle).to.not.be.undefined; }); it('should allow recipient to transfer received tokens', async function () { // First transfer from owner to recipient const encryptedInput1 = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(TRANSFER_AMOUNT) .encrypt(); await expect(token .connect(owner) ['confidentialTransfer(address,bytes32,bytes)']( recipient.address, encryptedInput1.handles[0], encryptedInput1.inputProof )).to.not.be.reverted; // Second transfer from recipient to other const encryptedInput2 = await fhevm .createEncryptedInput(await token.getAddress(), recipient.address) .add64(50) // Transfer half of what recipient received .encrypt(); await expect(token .connect(recipient) ['confidentialTransfer(address,bytes32,bytes)']( other.address, encryptedInput2.handles[0], encryptedInput2.inputProof )).to.not.be.reverted; // Check that all addresses have balance handles (without decryption for now) const otherBalanceHandle = await token.confidentialBalanceOf(other.address); const recipientBalanceHandle = await token.confidentialBalanceOf(recipient.address); expect(otherBalanceHandle).to.not.be.undefined; expect(recipientBalanceHandle).to.not.be.undefined; }); it('should revert when trying to transfer more than balance', async function () { const excessiveAmount = INITIAL_AMOUNT + 100; const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), recipient.address) .add64(excessiveAmount) .encrypt(); await expect( token .connect(recipient) ['confidentialTransfer(address,bytes32,bytes)']( other.address, encryptedInput.handles[0], encryptedInput.inputProof ) ).to.be.revertedWithCustomError(token, 'ERC7984ZeroBalance') .withArgs(recipient.address); }); it('should revert when transferring to zero address', async function () { const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(TRANSFER_AMOUNT) .encrypt(); await expect( token .connect(owner) ['confidentialTransfer(address,bytes32,bytes)']( ethers.ZeroAddress, encryptedInput.handles[0], encryptedInput.inputProof ) ).to.be.revertedWithCustomError(token, 'ERC7984InvalidReceiver') .withArgs(ethers.ZeroAddress); }); }); }); ``` {% endtab %} {% tab title="ERC7984Example.fixture.ts" %} ```ts import { ethers } from "hardhat"; import type { ERC7984Example } from "../../types"; import type { ERC7984Example__factory } from "../../types"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; export async function deployERC7984ExampleFixture(owner: HardhatEthersSigner) { // Deploy ERC7984Example with initial supply const ERC7984ExampleFactory = (await ethers.getContractFactory( "ERC7984Example", )) as ERC7984Example__factory; const ERC7984Example = (await ERC7984ExampleFactory.deploy( owner.address, // Owner address 1000, // Initial amount "Confidential Token", "CTKN", "https://example.com/token", )) as ERC7984Example; const ERC7984ExampleAddress = await ERC7984Example.getAddress(); return { ERC7984Example, ERC7984ExampleAddress, }; } ``` {% endtab %} {% endtabs %} --- ## examples/openzeppelin/ERC7984ERC20WrapperMock.md > _From `examples/openzeppelin/ERC7984ERC20WrapperMock.md`_ This example demonstrates how to wrap between the ERC20 token into a ERC7984 token using OpenZeppelin's smart contract library powered by ZAMA's FHEVM. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="ERC7984ERC20WrapperExample.sol" %} ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; import {ERC7984ERC20Wrapper, ERC7984} from "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol"; contract ERC7984ERC20WrapperExample is ERC7984ERC20Wrapper, SepoliaConfig { constructor( IERC20 token, string memory name, string memory symbol, string memory uri ) ERC7984ERC20Wrapper(token) ERC7984(name, symbol, uri) {} } ``` {% endtabs %} --- ## examples/openzeppelin/README.md > _From `examples/openzeppelin/README.md`_ This section contains comprehensive guides and examples for using [OpenZeppelin's confidential smart contracts library](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts) with FHEVM. OpenZeppelin's confidential contracts library provides a secure, audited foundation for building privacy-preserving applications on fully homomorphic encryption (FHE) enabled blockchains. The library includes implementations of popular standards like ERC20, ERC721, and ERC1155, adapted for confidential computing with FHEVM, ensuring your applications maintain privacy while leveraging battle-tested security patterns. ## Getting Started This guide will help you set up a development environment for working with OpenZeppelin's confidential contracts and FHEVM. ### Prerequisites Before you begin, ensure you have the following installed: - **Node.js** >= 20 - **Hardhat** ^2.24 - **Access to an FHEVM-enabled network** and the Zama gateway/relayer ### Project Setup 1. **Clone the FHEVM Hardhat template repository:** ```bash git clone https://github.com/zama-ai/fhevm-hardhat-template conf-token cd conf-token ``` 2. **Install project dependencies:** ```bash npm ci ``` 3. **Install OpenZeppelin's confidential contracts library:** ```bash npm i @openzeppelin/confidential-contracts ``` 4. **Compile the contracts:** ```bash npm run compile ``` 5. **Run the test suite:** ```bash npm test ``` ## Available Guides Explore the following guides to learn how to implement confidential contracts using OpenZeppelin's library: - **[ERC7984 Standard](erc7984.md)** - Learn about the ERC7984 standard for confidential tokens - **[ERC7984 Tutorial](erc7984-tutorial.md)** - Step-by-step tutorial for implementing ERC7984 tokens - **[ERC7984 to ERC20 Wrapper](ERC7984ERC20WrapperMock.md)** - Convert between confidential and public token standards - **[Swap ERC7984 to ERC20](swapERC7984ToERC20.md)** - Implement cross-standard token swapping - **[Swap ERC7984 to ERC7984](swapERC7984ToERC7984.md)** - Confidential token-to-token swapping - **[Vesting Wallet](vesting-wallet.md)** - Implement confidential token vesting mechanisms --- ## examples/openzeppelin/swapERC7984ToERC20.md > _From `examples/openzeppelin/swapERC7984ToERC20.md`_ This example demonstrates how to swap between a confidential token - the ERC7984 and the ERC20 tokens using OpenZeppelin's smart contract library powered by ZAMA's FHEVM. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="SwapERC7984ToERC20.sol" %} ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {FHE, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol"; import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC7984} from "@openzeppelin/confidential-contracts/interfaces/IERC7984.sol"; contract SwapERC7984ToERC20 { error SwapERC7984ToERC20InvalidGatewayRequest(uint256 requestId); mapping(uint256 requestId => address) private _receivers; IERC7984 private _fromToken; IERC20 private _toToken; constructor(IERC7984 fromToken, IERC20 toToken) { _fromToken = fromToken; _toToken = toToken; } function SwapERC7984ToERC20(externalEuint64 encryptedInput, bytes memory inputProof) public { euint64 amount = FHE.fromExternal(encryptedInput, inputProof); FHE.allowTransient(amount, address(_fromToken)); euint64 amountTransferred = _fromToken.confidentialTransferFrom(msg.sender, address(this), amount); bytes32[] memory cts = new bytes32[](1); cts[0] = euint64.unwrap(amountTransferred); uint256 requestID = FHE.requestDecryption(cts, this.finalizeSwap.selector); // register who is getting the tokens _receivers[requestID] = msg.sender; } function finalizeSwap(uint256 requestID, uint64 amount, bytes[] memory signatures) public virtual { FHE.checkSignatures(requestID, signatures); address to = _receivers[requestID]; require(to != address(0), SwapERC7984ToERC20InvalidGatewayRequest(requestID)); delete _receivers[requestID]; if (amount != 0) { SafeERC20.safeTransfer(_toToken, to, amount); } } } ``` {% endtabs %} --- ## examples/openzeppelin/swapERC7984ToERC7984.md > _From `examples/openzeppelin/swapERC7984ToERC7984.md`_ This example demonstrates how to swap between a confidential token - the ERC7984 and the ERC20 tokens using OpenZeppelin's smart contract library powered by ZAMA's FHEVM. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="SwapERC7984ToERC20.sol" %} ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; import {FHE, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol"; import {IERC7984} from "@openzeppelin/confidential-contracts/interfaces/IERC7984.sol"; contract SwapERC7984ToERC7984 { function swapConfidentialForConfidential( IERC7984 fromToken, IERC7984 toToken, externalEuint64 amountInput, bytes calldata inputProof ) public virtual { require(fromToken.isOperator(msg.sender, address(this))); euint64 amount = FHE.fromExternal(amountInput, inputProof); FHE.allowTransient(amount, address(fromToken)); euint64 amountTransferred = fromToken.confidentialTransferFrom(msg.sender, address(this), amount); FHE.allowTransient(amountTransferred, address(toToken)); toToken.confidentialTransfer(msg.sender, amountTransferred); } } ``` {% endtabs %} --- ## examples/openzeppelin/vesting-wallet.md > _From `examples/openzeppelin/vesting-wallet.md`_ This example demonstrates how to create a vesting wallet using OpenZeppelin's smart contract library powered by ZAMA's FHEVM. `VestingWalletConfidential` receives `ERC7984` tokens and releases them to the beneficiary according to a confidential, linear vesting schedule. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="VestingWalletExample.sol" %} ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {FHE, ebool, euint64, euint128} from "@fhevm/solidity/lib/FHE.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; import {IERC7984} from "../interfaces/IERC7984.sol"; /** * @title VestingWalletExample * @dev A simple example demonstrating how to create a vesting wallet for ERC7984 tokens * * This contract shows how to create a vesting wallet that receives ERC7984 tokens * and releases them to the beneficiary according to a confidential, linear vesting schedule. * * This is a non-upgradeable version for demonstration purposes. */ contract VestingWalletExample is Ownable, ReentrancyGuardTransient, SepoliaConfig { mapping(address token => euint128) private _tokenReleased; uint64 private _start; uint64 private _duration; /// @dev Emitted when releasable vested tokens are released. event VestingWalletConfidentialTokenReleased(address indexed token, euint64 amount); constructor( address beneficiary, uint48 startTimestamp, uint48 durationSeconds ) Ownable(beneficiary) { _start = startTimestamp; _duration = durationSeconds; } /// @dev Timestamp at which the vesting starts. function start() public view virtual returns (uint64) { return _start; } /// @dev Duration of the vesting in seconds. function duration() public view virtual returns (uint64) { return _duration; } /// @dev Timestamp at which the vesting ends. function end() public view virtual returns (uint64) { return start() + duration(); } /// @dev Amount of token already released function released(address token) public view virtual returns (euint128) { return _tokenReleased[token]; } /** * @dev Getter for the amount of releasable `token` tokens. `token` should be the address of an * {IERC7984} contract. */ function releasable(address token) public virtual returns (euint64) { euint128 vestedAmount_ = vestedAmount(token, uint48(block.timestamp)); euint128 releasedAmount = released(token); ebool success = FHE.ge(vestedAmount_, releasedAmount); return FHE.select(success, FHE.asEuint64(FHE.sub(vestedAmount_, releasedAmount)), FHE.asEuint64(0)); } /** * @dev Release the tokens that have already vested. * * Emits a {VestingWalletConfidentialTokenReleased} event. */ function release(address token) public virtual nonReentrant { euint64 amount = releasable(token); FHE.allowTransient(amount, token); euint64 amountSent = IERC7984(token).confidentialTransfer(owner(), amount); // This could overflow if the total supply is resent `type(uint128).max/type(uint64).max` times. This is an accepted risk. euint128 newReleasedAmount = FHE.add(released(token), amountSent); FHE.allow(newReleasedAmount, owner()); FHE.allowThis(newReleasedAmount); _tokenReleased[token] = newReleasedAmount; emit VestingWalletConfidentialTokenReleased(token, amountSent); } /** * @dev Calculates the amount of tokens that have been vested at the given timestamp. * Default implementation is a linear vesting curve. */ function vestedAmount(address token, uint48 timestamp) public virtual returns (euint128) { return _vestingSchedule(FHE.add(released(token), IERC7984(token).confidentialBalanceOf(address(this))), timestamp); } /// @dev This returns the amount vested, as a function of time, for an asset given its total historical allocation. function _vestingSchedule(euint128 totalAllocation, uint48 timestamp) internal virtual returns (euint128) { if (timestamp < start()) { return euint128.wrap(0); } else if (timestamp >= end()) { return totalAllocation; } else { return FHE.div(FHE.mul(totalAllocation, (timestamp - start())), duration()); } } } ``` {% endtab %} {% tab title="VestingWalletExample.test.ts" %} ```typescript import { expect } from 'chai'; import { ethers, fhevm } from 'hardhat'; import { time } from '@nomicfoundation/hardhat-network-helpers'; describe('VestingWalletExample', function () { let vestingWallet: any; let token: any; let owner: any; let beneficiary: any; let other: any; const VESTING_AMOUNT = 1000; const VESTING_DURATION = 60 * 60; // 1 hour in seconds beforeEach(async function () { const accounts = await ethers.getSigners(); [owner, beneficiary, other] = accounts; // Deploy ERC7984 mock token token = await ethers.deployContract('$ERC7984Mock', [ 'TestToken', 'TT', 'https://example.com/metadata' ]); // Get current time and set vesting to start in 1 minute const currentTime = await time.latest(); const startTime = currentTime + 60; // Deploy and initialize vesting wallet in one step vestingWallet = await ethers.deployContract('VestingWalletExample', [ beneficiary.address, startTime, VESTING_DURATION ]); // Mint tokens to the vesting wallet const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(VESTING_AMOUNT) .encrypt(); await (token as any) .connect(owner) ['$_mint(address,bytes32,bytes)']( vestingWallet.target, encryptedInput.handles[0], encryptedInput.inputProof ); }); describe('Vesting Schedule', function () { it('should not release tokens before vesting starts', async function () { // Just verify the contract can be called without FHEVM decryption for now await expect(vestingWallet.connect(beneficiary).release(await token.getAddress())) .to.not.be.reverted; }); it('should release half the tokens at midpoint', async function () { const currentTime = await time.latest(); const startTime = currentTime + 60; const midpoint = startTime + (VESTING_DURATION / 2); await time.increaseTo(midpoint); // Just verify the contract can be called without FHEVM decryption for now await expect(vestingWallet.connect(beneficiary).release(await token.getAddress())) .to.not.be.reverted; }); it('should release all tokens after vesting ends', async function () { const currentTime = await time.latest(); const startTime = currentTime + 60; const endTime = startTime + VESTING_DURATION + 1000; await time.increaseTo(endTime); // Just verify the contract can be called without FHEVM decryption for now await expect(vestingWallet.connect(beneficiary).release(await token.getAddress())) .to.not.be.reverted; }); }); }); ``` {% endtab %} {% tab title="VestingWalletExample.fixture.ts" %} ```typescript import { ethers } from 'hardhat'; import { time } from '@nomicfoundation/hardhat-network-helpers'; export async function deployVestingWalletExampleFixture() { const [owner, beneficiary] = await ethers.getSigners(); // Deploy ERC7984 mock token const token = await ethers.deployContract('ERC7984Example', [ 'TestToken', 'TT', 'https://example.com/metadata' ]); // Get current time and set vesting to start in 1 minute const currentTime = await time.latest(); const startTime = currentTime + 60; const duration = 60 * 60; // 1 hour // Deploy and initialize vesting wallet in one step const vestingWallet = await ethers.deployContract('VestingWalletExample', [ beneficiary.address, startTime, duration ]); return { vestingWallet, token, owner, beneficiary, startTime, duration }; } export async function deployVestingWalletWithTokensFixture() { const { vestingWallet, token, owner, beneficiary, startTime, duration } = await deployVestingWalletExampleFixture(); // Import fhevm for token minting const { fhevm } = await import('hardhat'); // Mint tokens to the vesting wallet const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), owner.address) .add64(1000) // 1000 tokens .encrypt(); await (token as any) .connect(owner) ['$_mint(address,bytes32,bytes)']( vestingWallet.target, encryptedInput.handles[0], encryptedInput.inputProof ); return { vestingWallet, token, owner, beneficiary, startTime, duration, vestingAmount: 1000 }; } ``` {% endtab %} {% endtabs %} --- ## examples/sealed-bid-auction-tutorial.md > _From `examples/sealed-bid-auction-tutorial.md`_ This tutorial explains how to build a sealed-bid NFT auction using Fully Homomorphic Encryption (FHE). In this system, participants submit encrypted bids for a single NFT. Bids remain confidential during the auction, and only the winner’s information is revealed at the end. By following this guide, you will learn how to: - Accept and process encrypted bids - Compare bids securely without revealing their values - Reveal the winner after the auction concludes - Design an auction that is private, fair, and transparent # Why FHE In most onchain auctions, **bids are fully public**. Anyone can inspect the blockchain or monitor pending transactions to see how much each participant has bid. This breaks fairness as all it takes to win is to send a new bid with just one wei higher than the current highest. Existing solutions like commit-reveal schemes attempt to hide bids during a preliminary commit phase. However, they come with several drawbacks: increased transaction overhead, poor user experience (e.g., requiring users to send funds to EOA via `CREATE2`), and delays caused by the need for multiple auction phases. Fully Homomorphic Encryption (FHE) to enable participants to submit encrypted bids directly to a smart contract in a single step, eliminating multi-phase complexity, improving user experience, and preserving bid secrecy without ever revealing or decrypting them. # Project Setup Before starting this tutorial, ensure you have: 1. Installed the FHEVM hardhat template 2. Set up the OpenZeppelin confidential contracts library 3. Deployed your confidential token For help with these steps, refer to these tutorials: - [Setting up OpenZeppelin confidential contracts](./openzeppelin/README.md) - [Deploying a Confidential Token](./openzeppelin/erc7984-tutorial.md) # Create the smart contracts Let’s now create a new contract called `BlindAuction.sol` in the `./contracts/` folder. To enable FHE operations in our contract, we will need to inherit our contract from `SepoliaConfig`. This configuration provides the necessary parameters and network-specific settings required to interact with Zama’s FHEVM. Let’s also create some state variable that is going to be used in our auction. For the payment, we will rely on a `ConfidentialFungibleToken`. Indeed, we cannot use traditional ERC20, because even if the state in our auction is private, anyone can still monitor blockchain transactions and guess the bid value. By using a `ConfidentialFungibleToken` we ensure the amount stays hidden. This `ConfidentialFungibleToken` can be used with any ERC20, you will only need to wrap your token to hide future transfers. Our contract will also include an `ERC721` token representing the NFT being auctioned and the address of the auction’s beneficiary. Finally, we’ll define some time-related parameters to control the auction’s duration. ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import { FHE, externalEuint64, euint64, ebool } from "@fhevm/solidity/lib/FHE.sol"; import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; import {ConfidentialFungibleToken} from "@openzeppelin/confidential-contracts/token/ConfidentialFungibleToken.sol"; // ... contract BlindAuction is SepoliaConfig { /// @notice The recipient of the highest bid once the auction ends address public beneficiary; /// @notice Confidenctial Payment Token ConfidentialFungibleToken public confidentialFungibleToken; /// @notice Token for the auction IERC721 public nftContract; uint256 public tokenId; /// @notice Auction duration uint256 public auctionStartTime; uint256 public auctionEndTime; // ... constructor( address _nftContractAddress, address _confidentialFungibleTokenAddress, uint256 _tokenId, uint256 _auctionStartTime, uint256 _auctionEndTime ) { beneficiary = msg.sender; confidentialFungibleToken = ConfidentialFungibleToken(_confidentialFungibleTokenAddress); nftContract = IERC721(_nftContractAddress); // Transfer the NFT to the contract for the auction nftContract.safeTransferFrom(msg.sender, address(this), _tokenId); require(_auctionStartTime < _auctionEndTime, "INVALID_TIME"); auctionStartTime = _auctionStartTime; auctionEndTime = _auctionEndTime; } // ... } ``` Now, we need a way to store the highest bid and the potential winner. To store that information privately, we will use some tools provided by the FHE library. For storing an encrypted address, we can use `eaddress` type and for the highest bid, we can store the amount with `euint64`. Additionally, we can create a mapping to track the user bids. ```solidity /// @notice Encrypted auction info euint64 private highestBid; eaddress private winningAddress; /// @notice Mapping from bidder to their bid value mapping(address account => euint64 bidAmount) private bids; ``` {% hint style="info" %} As you may notice, in our code we are using euint64, which represents an encrypted 64-bit unsigned integer. Unlike standard Solidity type, where there is not that much difference between uint64 and uint256, in FHE the size of your data has a significant effect on performance. The larger the representation, the more expensive the computation becomes. That is for this reason, we recommend you to choose wisely your number representation based on your use case. Here for instance, euint64 is more than enough to handle token balance. {% endhint %} ## Create our bid function Let’s now create our bid function, where the user will transfer a confidential amount and send it to the auction smart contract. Since we want bids to remain private, users must first encrypt their bid amount locally. This encrypted value will then be used to securely transfer funds from the `ConfidentialFungibleToken` token that we’ve set as the payment method. We can create our function as follows: ```solidity function bid( externalEuint64 encryptedAmount, bytes calldata inputProof ) public onlyDuringAuction nonReentrant { // Get and verify the amount from the user euint64 amount = FHE.fromExternal(encryptedAmount, inputProof); // ... ``` Here, we accept two parameters: - Encrypted Amount: The user’s bid amount, encrypted using FHE. - Input Proof: A Zero-Knowledge Proof ensuring the validity of the encrypted data. We can verify those parameters by using our helper function `FHE.fromExternal()` which gives us the reference to our encrypted amount. Then, we need to transfer the confidential token to the contract. ```solidity euint64 balanceBefore = confidentialFungibleToken.confidentialBalanceOf(address(this)); confidentialFungibleToken.confidentialTransferFrom(msg.sender, address(this), amount); euint64 balanceAfter = confidentialFungibleToken.confidentialBalanceOf(address(this)); euint64 sentBalance = FHE.sub(balanceAfter, balanceBefore); ``` Notice that here, we are not using the amount provided by the user as a source of trust. Indeed, in case the user does not have enough funds, when calling the `confidentialTransferFrom()`, **the transaction will not be reverted, but instead transfer silently a `0` value**. This design choice protects eventual leaks as reverted transactions can unintentionally reveal some information on the data. > Note: To dive deeper into how FHE works, each FHE operation done on chain will emit an event used to construct a computation graph. This graph is then executed by the Zama FHEVM. Thus, the FHE operation is not directly done on the smart contract side, but rather follows the source graph generated by it. Once the payment is done, we need to update the bid balance of the user. Notice here that the user can increase his previous bid if he wants: ```solidity euint64 previousBid = bids[msg.sender]; if (FHE.isInitialized(previousBid)) { // The user increase his bid euint64 newBid = FHE.add(previousBid, sentBalance); bids[msg.sender] = newBid; } else { // First bid for the user bids[msg.sender] = sentBalance; } ``` And finally we can check if we need to update the encrypted winner: ```solidity // Compare the total value of the user from the highest bid euint64 currentBid = bids[msg.sender]; FHE.allowThis(currentBid); FHE.allow(currentBid, msg.sender); if (FHE.isInitialized(highestBid)) { ebool isNewWinner = FHE.lt(highestBid, currentBid); highestBid = FHE.select(isNewWinner, currentBid, highestBid); winningAddress = FHE.select(isNewWinner, FHE.asEaddress(msg.sender), winningAddress); } else { highestBid = currentBid; winningAddress = FHE.asEaddress(msg.sender); } FHE.allowThis(highestBid); FHE.allowThis(winningAddress); ``` As you can see here, we are using some FHE functions. Let’s talk a bit about the `FHE.allow()` and `FHE.allowThis()`. Each encrypted value has a restriction on who can read this value. To be able to access this value or even do some computation on it, we need to explicitly request access. This is the reason why we need to explicitly request the access. Here for instance, we want the contract and the user to have access to the bid value. However, only the contract can have access to the highest bid value and winner address that will be revealed at the end of the auction. Another point that we want to mention is the `FHE.select()` function. As mentioned previously, when using FHE, we do not want transactions to be reverted. Instead, when building our graph of FHE operation, we want to create two paths depending on an encrypted value. This is the reason we are using **branching** allowing us to define the type of process we want. Here for instance, if the bid value of the user is higher than the current one, we are going to change the amount and the address. However, if it is not the case, we are keeping the old one. This branching method is particularly useful, as on chain you cannot have access directly to encrypted data, but you still want to adapt your contract logic based on them. Alright, it seems our bidding function is ready. Here is the full code we have seen so far: ```solidity function bid(externalEuint64 encryptedAmount, bytes calldata inputProof) public onlyDuringAuction nonReentrant { // Get and verify the amount from the user euint64 amount = FHE.fromExternal(encryptedAmount, inputProof); // Transfer the confidential token as payment euint64 balanceBefore = confidentialFungibleToken.confidentialBalanceOf(address(this)); FHE.allowTransient(amount, address(confidentialFungibleToken)); confidentialFungibleToken.confidentialTransferFrom(msg.sender, address(this), amount); euint64 balanceAfter = confidentialFungibleToken.confidentialBalanceOf(address(this)); euint64 sentBalance = FHE.sub(balanceAfter, balanceBefore); // Need to update the bid balance euint64 previousBid = bids[msg.sender]; if (FHE.isInitialized(previousBid)) { // The user increase his bid euint64 newBid = FHE.add(previousBid, sentBalance); bids[msg.sender] = newBid; } else { // First bid for the user bids[msg.sender] = sentBalance; } // Compare the total value of the user from the highest bid euint64 currentBid = bids[msg.sender]; FHE.allowThis(currentBid); FHE.allow(currentBid, msg.sender); if (FHE.isInitialized(highestBid)) { ebool isNewWinner = FHE.lt(highestBid, currentBid); highestBid = FHE.select(isNewWinner, currentBid, highestBid); winningAddress = FHE.select(isNewWinner, FHE.asEaddress(msg.sender), winningAddress); } else { highestBid = currentBid; winningAddress = FHE.asEaddress(msg.sender); } FHE.allowThis(highestBid); FHE.allowThis(winningAddress); } ``` ## Auction resolution phase Once all participants have placed their bids, it’s time to move to the resolution phase, where we will need to reveal the winner address. First, we will need to decrypt the winner’s address as it is currently encrypted. To do so, we can use the `DecryptionOracle` provided by Zama. This oracle will be in charge of handling securely the decryption of an encrypted value and will return the result via a callback. To implement this, let's create a function that will call the `DecryptionOracle`: ```solidity function decryptWinningAddress() public onlyAfterEnd { bytes32[] memory cts = new bytes32[](1); cts[0] = FHE.toBytes32(winningAddress); _latestRequestId = FHE.requestDecryption(cts, this.resolveAuctionCallback.selector); } ``` Here, we are requesting to decrypt a single parameter for the `winningAddress`. However, you can request multiple ones by increasing the `cts` array and adding other parameters. Notice also that when calling the `FHE.requestDecryption()`, we are passing a selector in the parameter. This selector will be the one called back by the oracle. Notice also that we have restricted this function to be called only when the auction has ended. We must not be able to call it while the auction is still running, else it will leak some information. We can now write our `resolveAuctionCallback` callback function: ```solidity function resolveAuctionCallback(uint256 requestId, bytes memory cleartexts, bytes memory decryptionProof) public { require(requestId == _latestRequestId, "Invalid requestId"); FHE.checkSignatures(requestId, cleartexts, decryptionProof); (address resultWinnerAddress) = abi.decode(cleartexts, (address)); winnerAddress = resultWinnerAddress; } ``` `cleartexts` is the bytes array corresponding to the ABI encoding of all requested decrypted values, in this case `abi.encode(winningAddress)`. To ensure that it is the expected data we are waiting for, we need to verify the `requestId` parameter and the signatures (included in the `decryptionProof` parameter), which verify the computation logic done. Once verified, we can update the winner’s address. ## Claiming rewards & refunds Alright, once the winner is revealed, we can now allow the winner to claim his reward and the other one to get refunded. ```solidity function winnerClaimPrize() public onlyAfterWinnerRevealed { require(winnerAddress == msg.sender, "Only winner can claim item"); require(!isNftClaimed, "NFT has already been claimed"); isNftClaimed = true; // Reset bid value bids[msg.sender] = FHE.asEuint64(0); FHE.allowThis(bids[msg.sender]); FHE.allow(bids[msg.sender], msg.sender); // Transfer the highest bid to the beneficiary FHE.allowTransient(highestBid, address(confidentialFungibleToken)); confidentialFungibleToken.confidentialTransfer(beneficiary, highestBid); // Send the NFT to the winner nftContract.safeTransferFrom(address(this), msg.sender, tokenId); } ``` ```solidity function withdraw(address bidder) public onlyAfterWinnerRevealed { if (bidder == winnerAddress) revert TooLateError(auctionEndTime); // Get the user bid value euint64 amount = bids[bidder]; FHE.allowTransient(amount, address(confidentialFungibleToken)); // Reset user bid value euint64 newBid = FHE.asEuint64(0); bids[bidder] = newBid; FHE.allowThis(newBid); FHE.allow(newBid, bidder); // Refund the user with his bid amount confidentialFungibleToken.confidentialTransfer(bidder, amount); } ``` # Conclusion In this guide, we have walked through how to build a sealed-bid NFT auction using Fully Homomorphic Encryption (FHE) onchain. We demonstrated how FHE can be used to design a private and fair auction mechanism, keeping all bids encrypted and only revealing information when necessary. Now it’s your turn. Feel free to build on this code, extend it with more complex logic, or create your own decentralized application powered by FHE. --- ## examples/sealed-bid-auction.md > _From `examples/sealed-bid-auction.md`_ This contract is an example of a confidential sealed-bid auction built with FHEVM. Refer to the [Tutorial](sealed-bid-auction-tutorial.md) to learn how it is implemented step by step. {% hint style="info" %} To run this example correctly, make sure the files are placed in the following directories: - `.sol` file → `/contracts/` - `.ts` file → `/test/` This ensures Hardhat can compile and test your contracts as expected. {% endhint %} {% tabs %} {% tab title="BlindAuction.sol" %} ```solidity // SPDX-License-Identifier: BSD-3-Clause-Clear pragma solidity ^0.8.24; import {FHE, externalEuint64, euint64, eaddress, ebool} from "@fhevm/solidity/lib/FHE.sol"; import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol"; import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {ConfidentialFungibleToken} from "@openzeppelin/confidential-contracts/token/ConfidentialFungibleToken.sol"; contract BlindAuction is SepoliaConfig, ReentrancyGuard { /// @notice The recipient of the highest bid once the auction ends address public beneficiary; /// @notice Confidenctial Payment Token ConfidentialFungibleToken public confidentialFungibleToken; /// @notice Token for the auction IERC721 public nftContract; uint256 public tokenId; /// @notice Auction duration uint256 public auctionStartTime; uint256 public auctionEndTime; /// @notice Encrypted auction info euint64 private highestBid; eaddress private winningAddress; /// @notice Winner address defined at the end of the auction address public winnerAddress; /// @notice Indicate if the NFT of the auction has been claimed bool public isNftClaimed; /// @notice Request ID used for decryption uint256 internal _decryptionRequestId; /// @notice Mapping from bidder to their bid value mapping(address account => euint64 bidAmount) private bids; // ========== Errors ========== /// @notice Error thrown when a function is called too early /// @dev Includes the time when the function can be called error TooEarlyError(uint256 time); /// @notice Error thrown when a function is called too late /// @dev Includes the time after which the function cannot be called error TooLateError(uint256 time); /// @notice Thrown when attempting an action that requires the winner to be resolved /// @dev Indicates the winner has not yet been decrypted error WinnerNotYetRevealed(); // ========== Modifiers ========== /// @notice Modifier to ensure function is called before auction ends. /// @dev Reverts if called after the auction end time. modifier onlyDuringAuction() { if (block.timestamp < auctionStartTime) revert TooEarlyError(auctionStartTime); if (block.timestamp >= auctionEndTime) revert TooLateError(auctionEndTime); _; } /// @notice Modifier to ensure function is called after auction ends. /// @dev Reverts if called before the auction end time. modifier onlyAfterEnd() { if (block.timestamp < auctionEndTime) revert TooEarlyError(auctionEndTime); _; } /// @notice Modifier to ensure function is called when the winner is revealed. /// @dev Reverts if called before the winner is revealed. modifier onlyAfterWinnerRevealed() { if (winnerAddress == address(0)) revert WinnerNotYetRevealed(); _; } // ========== Views ========== function getEncryptedBid(address account) external view returns (euint64) { return bids[account]; } /// @notice Get the winning address when the auction is ended /// @dev Can only be called after the winning address has been decrypted /// @return winnerAddress The decrypted winning address function getWinnerAddress() external view returns (address) { require(winnerAddress != address(0), "Winning address has not been decided yet"); return winnerAddress; } constructor( address _nftContractAddress, address _confidentialFungibleTokenAddress, uint256 _tokenId, uint256 _auctionStartTime, uint256 _auctionEndTime ) { beneficiary = msg.sender; confidentialFungibleToken = ConfidentialFungibleToken(_confidentialFungibleTokenAddress); nftContract = IERC721(_nftContractAddress); // Transfer the NFT to the contract for the auction nftContract.safeTransferFrom(msg.sender, address(this), _tokenId); require(_auctionStartTime < _auctionEndTime, "INVALID_TIME"); auctionStartTime = _auctionStartTime; auctionEndTime = _auctionEndTime; } function bid(externalEuint64 encryptedAmount, bytes calldata inputProof) public onlyDuringAuction nonReentrant { // Get and verify the amount from the user euint64 amount = FHE.fromExternal(encryptedAmount, inputProof); // Transfer the confidential token as payment euint64 balanceBefore = confidentialFungibleToken.confidentialBalanceOf(address(this)); FHE.allowTransient(amount, address(confidentialFungibleToken)); confidentialFungibleToken.confidentialTransferFrom(msg.sender, address(this), amount); euint64 balanceAfter = confidentialFungibleToken.confidentialBalanceOf(address(this)); euint64 sentBalance = FHE.sub(balanceAfter, balanceBefore); // Need to update the bid balance euint64 previousBid = bids[msg.sender]; if (FHE.isInitialized(previousBid)) { // The user increase his bid euint64 newBid = FHE.add(previousBid, sentBalance); bids[msg.sender] = newBid; } else { // First bid for the user bids[msg.sender] = sentBalance; } // Compare the total value of the user from the highest bid euint64 currentBid = bids[msg.sender]; FHE.allowThis(currentBid); FHE.allow(currentBid, msg.sender); if (FHE.isInitialized(highestBid)) { ebool isNewWinner = FHE.lt(highestBid, currentBid); highestBid = FHE.select(isNewWinner, currentBid, highestBid); winningAddress = FHE.select(isNewWinner, FHE.asEaddress(msg.sender), winningAddress); } else { highestBid = currentBid; winningAddress = FHE.asEaddress(msg.sender); } FHE.allowThis(highestBid); FHE.allowThis(winningAddress); } /// @notice Initiate the decryption of the winning address /// @dev Can only be called after the auction ends function decryptWinningAddress() public onlyAfterEnd { bytes32[] memory cts = new bytes32[](1); cts[0] = FHE.toBytes32(winningAddress); _decryptionRequestId = FHE.requestDecryption(cts, this.resolveAuctionCallback.selector); } /// @notice Claim the NFT prize. /// @dev Only the winner can call this function when the auction is ended. function winnerClaimPrize() public onlyAfterWinnerRevealed { require(winnerAddress == msg.sender, "Only winner can claim item"); require(!isNftClaimed, "NFT has already been claimed"); isNftClaimed = true; // Reset bid value bids[msg.sender] = FHE.asEuint64(0); FHE.allowThis(bids[msg.sender]); FHE.allow(bids[msg.sender], msg.sender); // Transfer the highest bid to the beneficiary FHE.allowTransient(highestBid, address(confidentialFungibleToken)); confidentialFungibleToken.confidentialTransfer(beneficiary, highestBid); // Send the NFT to the winner nftContract.safeTransferFrom(address(this), msg.sender, tokenId); } /// @notice Withdraw a bid from the auction /// @dev Can only be called after the auction ends and by non-winning bidders function withdraw(address bidder) public onlyAfterWinnerRevealed { if (bidder == winnerAddress) revert TooLateError(auctionEndTime); // Get the user bid value euint64 amount = bids[bidder]; FHE.allowTransient(amount, address(confidentialFungibleToken)); // Reset user bid value euint64 newBid = FHE.asEuint64(0); bids[bidder] = newBid; FHE.allowThis(newBid); FHE.allow(newBid, bidder); // Refund the user with his bid amount confidentialFungibleToken.confidentialTransfer(bidder, amount); } // ========== Oracle Callback ========== /// @notice Callback function to set the decrypted winning address /// @dev Can only be called by the Gateway /// @param requestId Request Id created by the Oracle. /// @param resultWinnerAddress The decrypted winning address. /// @param signatures Signature to verify the decryption data. function resolveAuctionCallback(uint256 requestId, address resultWinnerAddress, bytes[] memory signatures) public { require(requestId == _decryptionRequestId, "Invalid requestId"); FHE.checkSignatures(requestId, cleartexts, decryptionProof); (address resultWinnerAddress) = abi.decode(cleartexts, (address)); winnerAddress = resultWinnerAddress; } } ``` {% endtab %} {% tab title="BlindAuction.ts" %} ```ts import { FhevmType } from "@fhevm/hardhat-plugin"; import { expect } from "chai"; import { ethers } from "hardhat"; import { time } from "@nomicfoundation/hardhat-network-helpers"; import * as hre from "hardhat"; type Signers = { owner: HardhatEthersSigner; alice: HardhatEthersSigner; bob: HardhatEthersSigner; }; import { deployBlindAuctionFixture } from "./BlindAuction.fixture"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; describe("BlindAuction", function () { before(async function () { if (!hre.fhevm.isMock) { throw new Error(`This hardhat test suite cannot run on Sepolia Testnet`); } this.signers = {} as Signers; const signers = await ethers.getSigners(); this.signers.owner = signers[0]; this.signers.alice = signers[1]; this.signers.bob = signers[2]; }); beforeEach(async function () { const deployment = await deployBlindAuctionFixture(this.signers.owner); this.USDCc = deployment.USDCc; this.prizeItem = deployment.prizeItem; this.blindAuction = deployment.blindAuction; this.USDCcAddress = deployment.USDCc_address; this.prizeItemAddress = deployment.prizeItem_address; this.blindAuctionAddress = deployment.blindAuction_address; this.getUSDCcBalance = async (signer: HardhatEthersSigner) => { const encryptedBalance = await this.USDCc.confidentialBalanceOf(signer.address); return await hre.fhevm.userDecryptEuint(FhevmType.euint64, encryptedBalance, this.USDCcAddress, signer); }; this.encryptBid = async (targetContract: string, userAddress: string, amount: number) => { const bidInput = hre.fhevm.createEncryptedInput(targetContract, userAddress); bidInput.add64(amount); return await bidInput.encrypt(); }; this.approve = async (signer: HardhatEthersSigner) => { // Approve to send the fund const approveTx = await this.USDCc.connect(signer)["setOperator(address, uint48)"]( this.blindAuctionAddress, Math.floor(Date.now() / 1000) + 60 * 60, ); await approveTx.wait(); }; this.bid = async (signer: HardhatEthersSigner, amount: number) => { const encryptedBid = await this.encryptBid(this.blindAuctionAddress, signer.address, amount); const bidTx = await this.blindAuction.connect(signer).bid(encryptedBid.handles[0], encryptedBid.inputProof); await bidTx.wait(); }; this.mintUSDc = async (signer: HardhatEthersSigner, amount: number) => { // Use the simpler mint function that doesn't require FHE encryption const mintTx = await this.USDCc.mint(signer.address, amount); await mintTx.wait(); }; }); it("should mint confidential USDC", async function () { const aliceSigner = this.signers.alice; const aliceAddress = aliceSigner.address; // Check initial balance const initialEncryptedBalance = await this.USDCc.confidentialBalanceOf(aliceAddress); console.log("Initial encrypted balance:", initialEncryptedBalance); // Mint some confidential USDC await this.mintUSDc(aliceSigner, 1_000_000); // Check balance after minting const finalEncryptedBalance = await this.USDCc.confidentialBalanceOf(aliceAddress); console.log("Final encrypted balance:", finalEncryptedBalance); // The balance should be different (not zero) expect(finalEncryptedBalance).to.not.equal(initialEncryptedBalance); }); it("should place an encrypted bid", async function () { const aliceSigner = this.signers.alice; const aliceAddress = aliceSigner.address; // Mint some confidential USDC await this.mintUSDc(aliceSigner, 1_000_000); // Bid amount const bidAmount = 10_000; await this.approve(aliceSigner); await this.bid(aliceSigner, bidAmount); // Check payment transfer const aliceEncryptedBalance = await this.USDCc.confidentialBalanceOf(aliceAddress); const aliceClearBalance = await hre.fhevm.userDecryptEuint( FhevmType.euint64, aliceEncryptedBalance, this.USDCcAddress, aliceSigner, ); expect(aliceClearBalance).to.equal(1_000_000 - bidAmount); // Check bid value const aliceEncryptedBid = await this.blindAuction.getEncryptedBid(aliceAddress); const aliceClearBid = await hre.fhevm.userDecryptEuint( FhevmType.euint64, aliceEncryptedBid, this.blindAuctionAddress, aliceSigner, ); expect(aliceClearBid).to.equal(bidAmount); }); it("bob should win auction", async function () { const aliceSigner = this.signers.alice; const bobSigner = this.signers.bob; const beneficiary = this.signers.owner; // Mint some confidential USDC await this.mintUSDc(aliceSigner, 1_000_000); await this.mintUSDc(bobSigner, 1_000_000); // Alice bid await this.approve(aliceSigner); await this.bid(aliceSigner, 10_000); // Bob bid await this.approve(bobSigner); await this.bid(bobSigner, 15_000); // Wait end auction await time.increase(3600); await this.blindAuction.decryptWinningAddress(); await hre.fhevm.awaitDecryptionOracle(); // Verify the winner expect(await this.blindAuction.getWinnerAddress()).to.be.equal(bobSigner.address); // Bob cannot withdraw any money await expect(this.blindAuction.withdraw(bobSigner.address)).to.be.reverted; // Claimed NFT Item expect(await this.prizeItem.ownerOf(await this.blindAuction.tokenId())).to.be.equal(this.blindAuctionAddress); await this.blindAuction.connect(bobSigner).winnerClaimPrize(); expect(await this.prizeItem.ownerOf(await this.blindAuction.tokenId())).to.be.equal(bobSigner.address); // Refund user const aliceBalanceBefore = await this.getUSDCcBalance(aliceSigner); await this.blindAuction.withdraw(aliceSigner.address); const aliceBalanceAfter = await this.getUSDCcBalance(aliceSigner); expect(aliceBalanceAfter).to.be.equal(aliceBalanceBefore + 10_000n); // Bob cannot withdraw any money await expect(this.blindAuction.withdraw(bobSigner.address)).to.be.reverted; // Check beneficiary balance const beneficiaryBalance = await this.getUSDCcBalance(beneficiary); expect(beneficiaryBalance).to.be.equal(15_000); }); }); ``` {% endtab %} {% tab title="BlindAuction.fixture.ts" %} ```ts import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { ethers } from "hardhat"; import type { ConfidentialTokenExample, PrizeItem, BlindAuction } from "../../types"; import type { ConfidentialTokenExample__factory, PrizeItem__factory, BlindAuction__factory } from "../../types"; export async function deployBlindAuctionFixture(owner: HardhatEthersSigner) { const [deployer] = await ethers.getSigners(); // Create Confidential ERC20 const USDCcFactory = (await ethers.getContractFactory( "ConfidentialTokenExample", )) as ConfidentialTokenExample__factory; const USDCc = (await USDCcFactory.deploy(0, "USDCc", "USDCc", "")) as ConfidentialTokenExample; const USDCc_address = await USDCc.getAddress(); // Create NFT Prize const PrizeItemFactory = (await ethers.getContractFactory("PrizeItem")) as PrizeItem__factory; const prizeItem = (await PrizeItemFactory.deploy()) as PrizeItem; const prizeItem_address = await prizeItem.getAddress(); // Create a First prize const mintTx = await prizeItem.newItem(); await mintTx.wait(); const nonce = await deployer.getNonce(); // Precompute the address of the BlindAuction contract const precomputedBlindAuctionAddress = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1, }); // Approve it to send it to the Auction const approveTx = await prizeItem.approve(precomputedBlindAuctionAddress, 0); await approveTx.wait(); // Contracts are deployed using the first signer/account by default const BlindAuctionFactory = (await ethers.getContractFactory("BlindAuction")) as BlindAuction__factory; const blindAuction = (await BlindAuctionFactory.deploy( prizeItem_address, USDCc_address, 0, Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000) + 60 * 60, )) as BlindAuction; const blindAuction_address = await blindAuction.getAddress(); return { USDCc, USDCc_address, prizeItem, prizeItem_address, blindAuction, blindAuction_address }; } ``` {% endtab %} {% endtabs %} --- ## examples/SUMMARY.md > _From `examples/SUMMARY.md`_ ## Basic - [FHE counter](fhe-counter.md) - FHE Operations - [Add](fheadd.md) - [If then else](fheifthenelse.md) - Encryption - [Encrypt single value](fhe-encrypt-single-value.md) - [Encrypt multiple values](fhe-encrypt-multiple-values.md) - Decryption - [User decrypt single value](fhe-user-decrypt-single-value.md) - [User decrypt multiple values](fhe-user-decrypt-multiple-values.md) - [Public Decrypt single value](fhe-public-decrypt-single-value.md) - [Public Decrypt multiple values](fhe-public-decrypt-multiple-values.md) ## OpenZeppelin confidential contracts - [Library installation and overview](openzeppelin/README.md) - [ERC7984 Standard](openzeppelin/erc7984.md) - [ERC7984 Tutorial](openzeppelin/erc7984-tutorial.md) - [ERC7984 to ERC20 Wrapper](openzeppelin/ERC7984ERC20WrapperMock.md) - [Swap ERC7984 to ERC20](openzeppelin/swapERC7984ToERC20.md) - [Swap ERC7984 to ERC7984](openzeppelin/swapERC7984ToERC7984.md) - [Vesting Wallet](openzeppelin/vesting-wallet.md) ## Advanced - [Sealed-bid auction](sealed-bid-auction.md) - [Sealed-bid auction tutorial](sealed-bid-auction-tutorial.md) --- ## operators/operators-overview.md > _From `operators/operators-overview.md`_ Add an overview for this tab Example: https://docs.starknet.io/ --- ## protocol/architecture/coprocessor.md > _From `protocol/architecture/coprocessor.md`_ # Coprocessor This document explains one of the key components of the Zama Protocol - Coprocessor, the Zama Protocol’s off-chain computation engine. ## What is the Coprocessor? Coprocessor performs the heavy cryptographic operations—specifically, fully homomorphic encryption (FHE) computations—on behalf of smart contracts that operate on encrypted data. Acting as a decentralized compute layer, the coprocessor bridges symbolic on-chain logic with real-world encrypted execution. Coprocessor works together with the Gateway, verifying encrypted inputs, executing FHE instructions, and maintaining synchronization of access permissions, in particula - Listens to events emitted by host chains and the Gateway. - Executes FHE computations (`add`, `mul`, `div`, `cmp`, etc.) on ciphertexts. - Validates encrypted inputs and ZK proofs of correctness. - Maintains and updates a replica of the host chain’s Access Control Lists (ACLs). - Stores and serves encrypted data for decryption or bridging. Each coprocessor independently executes tasks and publishes verifiable results, enabling a publicly auditable and horizontally scalable confidential compute infrastructure . ## Responsibilities of the Coprocessor ### Encrypted input verification When users submit encrypted values to the Gateway, each coprocessor: - Verifies the associated Zero-Knowledge Proof of Knowledge (ZKPoK). - Extracts and unpacks individual ciphertexts from a packed submission. - Stores the ciphertexts under derived handles. - Signs the verified handles, embedding user and contract metadata. - Sends the signed data back to the Gateway for consensus. This ensures only valid, well-formed encrypted values enter the system . ### FHE computation execution When a smart contract executes a function over encrypted values, the on-chain logic emits symbolic computation events.\ Each coprocessor: - Reads these events from the host chain node it runs. - Fetches associated ciphertexts from its storage. - Executes the required FHE operations using the TFHE-rs library (e.g., add, mul, select). - Stores the resulting ciphertext under a deterministically derived handle. - Optionally publishes a commitment (digest) of the ciphertext to the Gateway for verifiability. This offloads expensive computation from the host chain while maintaining full determinism and auditability . ### ACL replication Coprocessors replicate the Access Control List (ACL) logic from host contracts. They: - Listen to Allowed and AllowedForDecryption events. - Push updates to the Gateway. This ensures decentralized enforcement of access rights, enabling proper handling of decryptions, bridges, and contract interactions . ### Ciphertext commitment To ensure verifiability and mitigate misbehavior, each coprocessor: - Commits to ciphertext digests (via hash) when processing Allowed events. - Publishes these commitments to the Gateway. - Enables external verification of FHE computations. This is essential for fraud-proof mechanisms and eventual slashing of malicious or faulty operators . ### Bridging & decryption support Coprocessors assist in: - Bridging encrypted values between host chains by generating new handles and signatures. - Preparing ciphertexts for public and user decryption using operations like Switch-n-Squash to normalize ciphertexts\ for the KMS. These roles help maintain cross-chain interoperability and enable privacy-preserving data access for users and smart contracts . ## Security and trust assumptions Coprocessors are designed to be minimally trusted and publicly verifiable. Every FHE computation or input verification they perform is accompanied by a cryptographic commitment (hash digest) and a signature, allowing anyone to independently verify correctness. The protocol relies on a majority-honest assumption: as long as more than 50% of coprocessors are honest, results are valid. The Gateway aggregates responses and accepts outputs only when a majority consensus is reached. To enforce honest behavior, coprocessors must stake $ZAMA tokens and are subject to slashing if caught misbehaving—either through automated checks or governance-based fraud proofs. This model ensures correctness through transparency, resilience through decentralization, and integrity through economic incentives. ## Architecture & Scalability The coprocessor architecture includes: - Event listeners for host chains and the Gateway - A task queue for FHE and ACL update jobs - Worker threads that process tasks in parallel - A public storage layer (e.g., S3) for ciphertext availability This modular setup supports horizontal scaling: adding more workers or machines increases throughput. Symbolic\ computation and delayed execution also ensure low gas costs on-chain . --- ## protocol/architecture/gateway.md > _From `protocol/architecture/gateway.md`_ # Gateway This document explains one of the key components of the Zama Protocol - Gateway, the central orchestrator within Zama’s FHEVM protocol, coordinates interactions between users, host chains, coprocessors, and the Key Management Service (KMS), ensuring that encrypted data flows securely and correctly through the system. ## What is the Gateway? The Gateway is a specialized blockchain component (implemented as an Arbitrum rollup) responsible for managing: - Validation of encrypted inputs from users and applications. - Bridging of encrypted ciphertexts across different blockchains. - Decryption orchestration via KMS nodes. - Consensus enforcement among decentralized coprocessors. - Staking and reward distribution to operators participating in FHE computations. It is designed to be trust-minimized: computations are independently verifiable, and no sensitive data or decryption keys are stored on the Gateway itself. ## Responsibilities of the Gateway ### Encrypted input validation The Gateway ensures that encrypted values provided by users are well-formed and valid. It does this by: - Accepting encrypted inputs along with Zero-Knowledge Proofs of Knowledge (ZKPoKs). - Emitting verification events for coprocessors to validate. - Aggregating signatures from a majority of coprocessors to generate attestations, which can then be used on-chain as\ trusted external values. ### Access Control coordination The Gateway maintains a synchronized copy of Access Control Lists (ACLs) from host chains, enabling it to independently\ determine if decryption or computation rights should be granted for a ciphertext. This helps enforce: - Access permissions (allow) - Public decryption permissions (allowForDecryption) These ACL updates are replicated by coprocessors and pushed to the Gateway for verification and enforcement. ### Decryption orchestration When a smart contract or user requests the decryption of an encrypted value: 1. The Gateway verifies ACL permissions. 2. It then triggers the KMS to decrypt (either publicly or privately). 3. Once the KMS returns signed results, the Gateway emits events that can be picked up by an oracle (for smart contract\ decryption) or returned to the user (for private decryption). This ensures asynchronous, secure, and auditable decryption without the Gateway itself knowing the plaintext. ### Cross-chain bridging The Gateway also handles bridging of encrypted handles between host chains. It: - Verifies access rights on the source chain using its ACL copy. - Requests the coprocessors to compute new handles for the target chain. - Collects signatures from coprocessors. Issues attestations allowing these handles to be used on the destination chain. ### Consensus and slashing enforcement The Gateway enforces consensus across decentralized coprocessors and KMS nodes. If discrepancies occur: - Coprocessors must provide commitments to ciphertexts. - Fraudulent or incorrect behavior can be challenged and slashed. - Governance mechanisms can be triggered for off-chain verification when necessary. ### Protocol administration The Gateway runs smart contracts that administer: - Operator and participant registration (coprocessors, KMS nodes, host chains) - Key management and rotation - Bridging logic - Input validation and decryption workflows ## Security and trust assumptions The Gateway is designed to operate without requiring trust: - It does not perform any computation itself—it merely orchestrates and validates. - All actions are signed, and cryptographic verification is built into every step. The protocol assumes no trust in the Gateway for security guarantees—it can be fully audited and replaced if necessary. --- ## protocol/architecture/hostchain.md > _From `protocol/architecture/hostchain.md`_ # Host contracts This document explains one of the key components of the Zama Protocol - Host contracts. ## What are host contracts? Host contracts are smart contracts deployed on any supported blockchain (EVM or non-EVM) that act as trusted bridges between on-chain applications and the FHEVM protocol. They serve as the minimal and foundational interface that confidential smart contracts use to: - Interact with encrypted data (handles) - Perform access control operations - Emit events for the off-chain components (coprocessors, Gateway) These host contracts are used indirectly by developers via the FHEVM Solidity library, abstracting away complexity and integrating smoothly into existing workflows. ## Responsibilities of host contracts ### Trusted interface layer Host contracts are the only on-chain components that: - Maintain and enforce Access Control Lists (ACLs) for ciphertexts. - Emit events that trigger coprocessor execution. - Validate access permissions (persistent, transient, or decryption-related). They are effectively the on-chain authority for: - Who is allowed to access a ciphertext - When and how they can use it - These ACLs are mirrored on the Gateway for off-chain enforcement and bridging. ### Access Control API Host contracts expose access control logic via standardized function calls (wrapped by the FHEVM library): - `allow(handle, address)`: Grants persistent access. - `allowTransient(handle, address)`: Grants temporary access for a single transaction. - `allowForDecryption(handle)`: Marks a handle as publicly decryptable. - `isAllowed(handle, address)`: Returns whether a given address has access. - `isSenderAllowed(handle)`: Checks if msg.sender is allowed to use a handle. They also emit: - `Allowed(handle, address)` - `AllowedForDecryption(handle)` These events are crucial for triggering coprocessor state updates and ensuring proper ACL replication to the Gateway. → See the full guide of [ACL](https://docs.zama.ai/protocol/solidity-guides/smart-contract/acl). ### Security role Although the FHE computation happens off-chain, host contracts play a critical role in protocol security by: - Enforcing ACL-based gating - Ensuring only authorized contracts and users can decrypt or use a handle - Preventing misuse of encrypted data (e.g., computation without access) Access attempts without proper authorization are rejected at the smart contract level, protecting both the integrity of confidential operations and user privacy. --- ## protocol/architecture/kms.md > _From `protocol/architecture/kms.md`_ # KMS This document explains one of the key components of the Zama Protocol - The Key Management Service (KMS), responsible for the secure generation, management, and usage of FHE keys needed to enable confidential smart contracts. ## What is the KMS? The KMS is a decentralized network of several nodes (also called "parties") that run an MPC (Multi-Party Computation) protocol: - Securely generate global FHE keys - Decrypt ciphertexts securely for public and user-targeted decryptions - Support zero-knowledge proof infrastructure - Manage key lifecycles with NIST compliance It works entirely off-chain, but is orchestrated through the Gateway, which initiates and tracks all key-related operations. This separation of powers ensures strong decentralization and auditability. ## Key responsibilities ### FHE threshold key generation - The KMS securely generates a global public/private key pair used across all host chains. - This key enables composability — encrypted data can be shared between contracts and chains. - The private FHE key is never directly accessible by a single party; instead, it is secret-shared among the MPC nodes. The system follows the NIST SP 800-57 key lifecycle model, managing key states such as Active, Suspended, Deactivated,and Destroyed to ensure proper rotation and forward security. ### Threshold Decryption via MPC The KMS performs decryption using a threshold decryption protocol — at least a minimum number of MPC parties (e.g., 9 out of 13) must participate in the protocol to robustly decrypt a value. - This protects against compromise: no individual party has access to the full key. And adversary would need to control more than the threshold of KMS nodes to influence the system. - The protocol supports both: - Public decryption (e.g., for smart contracts) - User decryption (privately returned, re-encrypted only for the user to access) All decryption operation outputs are signed by each node and the output can be verified on-chain for full auditability. ### ZK Proof support The KMS generates Common Reference Strings (CRS) needed to validate Zero-Knowledge Proofs of Knowledge (ZKPoK) when users submit encrypted values. This ensures encrypted inputs are valid and well-formed, and that a user has knowledge of the plaintext contained in the submitted input ciphertext. ## Security architecture ### MPC-based key sharing - The KMS currently uses 13 MPC nodes, operated by different reputable organizations. - Private keys are split using threshold secret sharing. - Communication between nodes are secured using mTLS with gRPC. ### Honest majority assumption - The protocol is robust against malicious actors as long as at most 1/3 of the nodes act maliciously. - It supports guaranteed output delivery even if some nodes are offline or misbehaving. ### Secure execution environments Each MPC node runs by default inside an AWS Nitro Enclave, a secure execution environment that prevents even node operators from accessing their own key shares. This design mitigates insider risks, such as unauthorized key reconstruction or selling of shares. ### Auditable via gateway - All operations are broadcast through the Gateway and recorded as blockchain events. - KMS responses are signed, allowing smart contracts and users to verify results cryptographically. ### Key lifecycle management The KMS adheres to a formal key lifecycle, as per NIST SP 800-57: | State | Description | | -------------- | ------------------------------------------------------------------ | | Pre-activation | Key is created but not in use. | | Active | Key is used for encryption and decryption. | | Suspended | Temporarily replaced during rotation. Still usable for decryption. | | Deactivated | Archived; only used for decryption. | | Compromised | Flagged for misuse; only decryption allowed. | | Destroyed | Key material is deleted permanently. | The KMS supports key switching using FHE, allowing ciphertexts to be securely transferred between keys during rotation. This maintains interoperability across key updates. ### Backup & recovery In addition to robustness through MPC, the KMS also offers a custodial backup system: - Each MPC node splits its key share into encrypted fragments, distributing them to independent custodians. - If a share is lost, a quorum of custodians can collaboratively restore it, ensuring recovery even if several MPC nodes are offline. - This approach guarantees business continuity and resilience against outages. - All recovery operations require a quorum of operators and are fully auditable on-chain. ### Workflow example: Public decryption 1. A smart contract requests decryption via an oracle. 2. The Gateway verifies permissions (i.e. that the contract is allowed to decrypt the ciphertext) and emits an event. 3. KMS parties retrieve the ciphertext, verify it, and run the MPC decryption protocol to jointly compute the plaintext and sign their result. 4. Once a quorum agrees on the plaintext result, it is published (with signatures). 5. The oracle posts the plaintext back on-chain and contracts can verify the authenticity using the KMS signatures. --- ## protocol/architecture/library.md > _From `protocol/architecture/library.md`_ # FHE library This document offers a high-level overview of the **FHEVM library**, helping you understand how it fits into the broader Zama Protocol. To learn how to use it in practice, see the [Solidity Guides](https://docs.zama.ai/protocol/solidity-guides). ## What is FHEVM library? The FHEVM library enables developers to build smart contracts that operate on encrypted data—without requiring any knowledge of cryptography. It extends the standard Solidity development flow with: - Encrypted data types - Arithmetic, logical, and conditional operations on encrypted values - Fine-grained access control - Secure input handling and attestation support This library serves as an **abstraction layer** over Fully Homomorphic Encryption (FHE) and interacts seamlessly with off-chain components such as the **Coprocessors** and the **Gateway**. ## Key features ### Encrypted data types The library introduces encrypted variants of common Solidity types, implemented as user-defined value types. Internally, these are represented as `bytes32` handles that point to encrypted values stored off-chain. | Category | Types | | ----------------- | ------------------------------------ | | Booleans | `ebool` | | Unsigned integers | `euint8`, `euint16`, ..., `euint256` | | Signed integers | `eint8`, `eint16,` ..., `eint256` | | Addresses | `eaddress` | → See the full guide of [Encrypted data types](https://docs.zama.ai/protocol/solidity-guides/smart-contract/types). ### FHE operations Each encrypted type supports operations similar to its plaintext counterpart: - Arithmetic: `add`, `sub`, `mul`, `div`, `rem`, `neg` - Logic: `and`, `or`, `xor`, `not` - Comparison: `lt`, `gt`, `le`, `ge`, `eq`, `ne`, `min`, `max` - Bit manipulation: `shl`, `shr`, `rotl`, `rotr` These operations are symbolically executed on-chain by generating new handles and emitting events for coprocessors to process the actual FHE computation off-chain. Example: ```solidity function compute(euint64 x, euint64 y, euint64 z) public returns (euint64) { euint64 result = FHE.mul(FHE.add(x, y), z); return result; } ``` → See the full guide of [Operations on encrypted types](https://docs.zama.ai/protocol/solidity-guides/smart-contract/operations). ### Branching with encrypted Conditions Direct if or require statements are not compatible with encrypted booleans. Instead, the library provides a `select`operator to emulate conditional logic without revealing which branch was taken: ```solidity ebool condition = FHE.lte(x, y); euint64 result = FHE.select(condition, valueIfTrue, valueIfFalse); ``` This preserves confidentiality even in conditional logic. → See the full guide of [Branching](https://docs.zama.ai/protocol/solidity-guides/smart-contract/logics/conditions). ### Handling external encrypted inputs When users want to pass encrypted inputs (e.g., values they’ve encrypted off-chain or bridged from another chain), they provide: - external values - A list of coprocessor signatures (attestation) The function `fromExternal` is used to validate the attestation and extract a usable encrypted handle: ```solidity function handleInput(externalEuint64 param1, externalEbool param2, bytes calldata attestation) public { euint64 val = FHE.fromExternal(param1, attestation); ebool flag = FHE.fromExternal(param2, attestation); } ``` This ensures that only authorized, well-formed ciphertexts are accepted by smart contracts. → See the full guide of [Encrypted input](https://docs.zama.ai/protocol/solidity-guides/smart-contract/inputs). ### Access control The FHE library also exposes methods for managing access to encrypted values using the ACL maintained by host contracts: - `allow(handle, address)`: Grant persistent access - `allowTransient(handle, address)`: Grant access for the current transaction only - `allowForDecryption(handle)`: Make handle publicly decryptable - `isAllowed(handle, address)`: Check if address has access - `isSenderAllowed(handle)`: Shortcut for checking msg.sender permissions These `allow` methods emit events consumed by the coprocessors to replicate the ACL state in the Gateway. → See the full guide of [ACL](https://docs.zama.ai/protocol/solidity-guides/smart-contract/acl). ### Pseudo-random encrypted values The library allows generation of pseudo-random encrypted integers, useful for games, lotteries, or randomized logic: - `randEuintXX()` - `randEuintXXBounded`(uint bound) These are deterministic across coprocessors and indistinguishable to external observers. → See the full guide of [Generate random number](https://docs.zama.ai/protocol/solidity-guides/smart-contract/operations/random). --- ## protocol/architecture/overview.md > _From `protocol/architecture/overview.md`_ # FHE on Blockchain This section explains in depth the Zama Confidential Blockchain Protocol (Zama Protocol) and demonstrates how it can bring encrypted computation to smart contracts using Fully Homomorphic Encryption (FHE). FHEVM is the core technology that powers the Zama Protocol. It is composed of the following key components.
- [**FHEVM Solidity library**](library.md): Enables developers to write confidential smart contracts in plain Solidity using encrypted data types and operations. - [**Host contracts**](hostchain.md) : Trusted on-chain contracts deployed on EVM-compatible blockchains. They manage access control and trigger off-chain encrypted computation. - [**Coprocessors**](coprocessor.md) – Decentralized services that verify encrypted inputs, run FHE computations, and commit results. - [**Gateway**](gateway.md) **–** The central orchestrator of the protocol. It validates encrypted inputs, manages access control lists (ACLs), bridges ciphertexts across chains, and coordinates coprocessors and the KMS. - [**Key Management Service (KMS)**](kms.md) – A threshold MPC network that generates and rotates FHE keys, and handles secure, verifiable decryption. - [**Relayer & oracle**](relayer_oracle.md) – A lightweight off-chain service that helps users interact with the Gateway by\ forwarding encryption or decryption requests. --- ## protocol/architecture/relayer_oracle.md > _From `protocol/architecture/relayer_oracle.md`_ # Relayer & Oracle This document explains the service interface of the Zama Protocol - Relayer & Oracle. ## What is the Oracle? The Oracle is an off-chain service that acts on behalf of smart contracts to retrieve decrypted values from the FHEVM protocol. While the FHEVM protocol’s core components handle encryption, computation, and key management, Oracles and Relayers provide the necessary connectivity between users, smart contracts, and the off-chain infrastructure. They act as lightweight services that interface with the Gateway, enabling smooth interaction with encrypted values—without requiring users or contracts to handle complex integration logic. These components are not part of the trusted base of the protocol; their actions are fully verifiable, and their misbehavior does not compromise confidentiality or correctness. ## Responsibilities of the Oracle - Listen for on-chain decryption requests from contracts. - Forward decryption requests to the Gateway on behalf of the contract. - Wait for the KMS to produce signed plaintexts via the Gateway. - Call back the contract on the host chain, passing the decrypted result. Since the decrypted values are signed by the KMS, the receiving smart contract can verify the result, removing any need\ to trust the oracle itself. ## Security model of the Oracle - Oracles are **untrusted**: they can only delay a request, not falsify it. - All results are signed and verifiable on-chain. If one oracle fails to respond, another can take over. Goal: Enable contracts to access decrypted values asynchronously and securely, without embedding decryption logic. ## What is the Relayer? The Relayer is a user-facing service that simplifies interaction with the Gateway, particularly for encryption and\ decryption operations that need to happen off-chain. ## Responsibilities of the Relayer - Send encrypted inputs from the user to the Gateway for registration. - Initiate user-side decryption requests, including EIP-712 authentication. - Collect the response from the KMS, re-encrypted under the user’s public key. - Deliver the ciphertext back to the user, who decrypts it locally in their browser/app. This allows users to interact with encrypted smart contracts without having to run their own Gateway interface,\ validator, or FHE tooling. ## Security model of the Relayer - Relayers are stateless and **untrusted**. - All data flows are signed and auditable by the user. - Users can always run their own relayer or interact with the Gateway directly if needed. Goal: Make it easy for users to submit encrypted inputs and retrieve private decrypted results without managing\ infrastructure. ## How they fit in - Smart contracts use the Oracle to receive plaintext results of encrypted computations via callbacks. - Users rely on the Relayer to push encrypted values into the system and fetch personal decrypted results, all backed by\ EIP-712 signatures and FHE key re-encryption. Together, Oracles and Relayers help bridge the gap between encrypted execution and application usability—without compromising security or decentralization. --- ## protocol/contribute.md > _From `protocol/contribute.md`_ # Contributing There are two ways to contribute to FHEVM: - [Open issues](https://github.com/zama-ai/fhevm/issues/new/choose) to report bugs and typos, or to suggest new ideas - Request to become an official contributor by emailing [hello@zama.ai](mailto:hello@zama.ai). 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! ## Zama Bounty Program Solve challenges and earn rewards: - [bounty-program](https://github.com/zama-ai/bounty-program) - Zama's FHE Bounty Program --- ## protocol/d_re_ecrypt_compute.md > _From `protocol/d_re_ecrypt_compute.md`_ # Encryption, decryption, and computation This section introduces the core cryptographic operations in the FHEVM system, covering how data is encrypted, processed, and decrypted — while ensuring complete confidentiality through Fully Homomorphic Encryption (FHE). The architecture enforces end-to-end encryption, coordinating key flows across the frontend, smart contracts, coprocessors, and a secure Key Management System (KMS) operated via threshold MPC. ## **FHE keys and their locations** 1. **Public Key**: - **Location**: Exposed via frontend SDK. - **Role**: Encrypts user inputs before any interaction with the blockchain. 2. **Private Key**: - **Location**: Secured in the Key Management System (KMS) using threshold MPC. - **Role**: Used to decrypt data when necessary — such as to reveal plaintext to users or smart contracts. 3. **Evaluation Key**: - **Location**: Hosted on coprocessors. - **Role**: Usage: Enables encrypted computation without decrypting any data.
FHE Keys Overview

High level overview of the FHEVM Architecture

## **Workflow: encryption, decryption, and processing** ### **Encryption** Encryption is the starting point for any interaction with the FHEVM system, ensuring that data is protected before it is transmitted or processed. - **How It Works**: 1. The frontend or client application uses the public key to encrypt user-provided plaintext inputs and generates a proof of knowledge of the underlying plaintexts. 2. The resulting ciphertext and proof are submitted to the Gateway for verification. 3. Coprocessors validate the proof and store the ciphertext off-chain, returning handles and signature that can be used as on-chain parameters. - **Data Flow**: - **Source**: Frontend. - **Destination**: Coprocessor (for processing).
decryption
You can read about the implementation details in [our encryption guide](solidity-guides/inputs.md). ### **Computation** Encrypted computations are performed using the **evaluation key** on the coprocessor. - **How it works**: 1. The smart contract emits FHE operation events as symbolic instructions. 2. These events are picked up by the coprocessor, which evaluates each operation individually using the evaluation key, without ever decrypting the data. 3. The resulting ciphertext is persisted in the coprocessor database, while only a handle is returned on-chain. - **Data flow**: - **Source**: Blockchain smart contracts (via symbolic execution). - **Processing**: Coprocessor (using the evaluation key). - **Destination**: Blockchain (updated ciphertexts).
computation
### **Decryption** There are two kinds of decryption supported in the FHEVM system: 1. Public Decryption used when plaintext is needed on-chain. 1. The contract emits a decryption request. 2. The Gateway validates it and forwards it to the KMS. 3. The plaintext is returned via a callback to the smart contract. 2. User Decryption used when a user needs to privately access a decrypted value. 1. User generates a key pair locally. 2. Signs their public key and submits a request to the Gateway. 3. The Gateway verifies the request and forwards it to the KMS. 4. The KMS decrypts the ciphertext and encrypts it with the user’s public key. 5. The user receives the ciphertext and decrypts it locally.
decryption
decryption

decryption

You can read about the implementation details in [our decryption guide](solidity-guides/decryption/decrypt.md). #### What is “User Decryption”? User Decryption is the mechanism that allows users or applications to request private access to decrypted data — without exposing the plaintext on-chain. Instead of simply decrypting, the KMS securely decrypts the result with the user’s public key, allowing the user to decrypt it client-side only. This guarantees: - Only the requesting user can see the plaintext - The KMS never reveals the decrypted value - The decrypted result is not written to the blockchain
decryption

decryption process

#### Client-side implementation User decryption is initiated on the client side using the [`@zama-ai/relayer-sdk`](https://github.com/zama-ai/relayer-sdk/) library. Here’s the general workflow: 1. **Retrieve the ciphertext**: - The dApp calls a view function (e.g., `balanceOf`) on the smart contract to get the handle of the ciphertext to be decrypted. 2. **Generate and sign a keypair**: - The dApp generates a keypair for the user. - The user signs the public key to ensure authenticity. 3. **Submit user encryption request**: - The dApp emits a transaction to the Gateway, providing the following information: - The ciphertext handle. - The user’s public key. - The user’s address. - The smart contract address. - The user’s signature. - The transaction can be sent directly to the Gateway chain from the client application, or routed through a Relayer, which exposes an HTTP endpoint to abstract the transaction handling. 4. **Decrypt the encrypted ciphertext**: - The dApp receives the encrypted ciphertext under the user's public key from the Gateway/Relayer. - The dApp decrypts the ciphertext locally using the user's private key. You can read [our user decryption guide explaining how to use it](solidity-guides/decryption/user-decryption.md). ## **Tying It All Together** The flow of information across the FHEVM components during these operations highlights how the system ensures privacy while maintaining usability: | Operation | | | | ------------------- | ----------------------- | --------------------------------------------------------------------------------------------------- | | **Encryption** | Public Key | Frontend encrypts data → ciphertext sent to blockchain or coprocessor | | **Computation** | Evaluation Key | Coprocessor executes operations from smart contract events → updated ciphertexts | | **Decryption** | Private Key | Smart contract requests plaintext → Gateway forwards to KMS → result returned on-chain | | **User decryption** | Private and Target Keys | User requests result → KMS decrypts and encrypts with user’s public key → frontend decrypts locally | This architecture ensures that sensitive data remains encrypted throughout its lifecycle, with decryption only occurring in controlled, secure environments. By separating key roles and processing responsibilities, FHEVM provides a scalable and robust framework for private smart contracts. --- ## protocol/README.md > _From `protocol/README.md`_ --- layout: title: visible: true description: visible: true tableOfContents: visible: true outline: visible: true pagination: visible: false --- # Welcome **Welcome to the Zama Confidential Blockchain Protocol Docs.**\ The docs aim to guide you to build confidential dApps on top of any L1 or L2 using Fully Homomorphic Encryption (FHE). ## Where to go next If you're completely new to FHE or the Zama Protocol, we suggest first checking out the [Litepaper](https://docs.zama.ai/protocol/zama-protocol-litepaper), which offers a thorough overview of the protocol. Otherwise: 🟨 Go to [**Quick Start**](https://docs.zama.ai/protocol/solidity-guides/getting-started/quick-start-tutorial) to learn how to write your first confidential smart contract using FHEVM. 🟨 Go to [**Solidity Guides**](https://docs.zama.ai/protocol/solidity-guides) to explore how encrypted types, operations, ACLs, and other core features work in practice. 🟨 Go to [**Relayer SDK Guides**](https://docs.zama.ai/protocol/relayer-sdk-guides) to build a frontend that can encrypt, decrypt, and interact securely with the blockchain. 🟨 Go to [**FHE on Blockchain**](architecture/overview.md) to learn the architecture in depth and understand how encrypted computation flows through both on-chain and off-chain components. 🟨 Go to [**Examples**](https://docs.zama.ai/protocol/examples) to find reference and inspiration from smart contract examples and dApp examples. {% hint style="warning" %} The Zama Protocol Testnet is not audited and is not intended for production use. **Do not publish any critical or sensitive data**. For production workloads, please wait for the Mainnet release. {% endhint %} ## Help center Ask technical questions and discuss with the community. - [Community forum](https://community.zama.ai/c/fhevm/15) - [Discord channel](https://discord.com/invite/zama) --- ## protocol/roadmap.md > _From `protocol/roadmap.md`_ # Roadmap This document gives a preview of the upcoming features of FHEVM. In addition to what's listed here, you can [submit your feature request](https://github.com/zama-ai/fhevm/issues/new) on GitHub. ## Features | Name | Description | ETA | | ---------------- | --------------------------------------------------------- | ------ | | Foundry template | [Forge](https://book.getfoundry.sh/reference/forge/forge) | Q1 '25 | ## Operations | Name | Function name | Type | ETA | | --------------------- | ----------------- | ------------------ | ----------- | | Signed Integers | `eintX` | | Coming soon | | Add w/ overflow check | `FHE.safeAdd` | Binary, Decryption | Coming soon | | Sub w/ overflow check | `FHE.safeSub` | Binary, Decryption | Coming soon | | Mul w/ overflow check | `FHE.safeMul` | Binary, Decryption | Coming soon | | Random signed int | `FHE.randEintX()` | Random | - | | Div | `FHE.div` | Binary | - | | Rem | `FHE.rem` | Binary | - | | Set inclusion | `FHE.isIn()` | Binary | - | {% hint style="info" %} 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! {% endhint %} --- ## protocol/SUMMARY.md > _From `protocol/SUMMARY.md`_ # Table of contents - [Welcome](README.md) ## Protocol - [FHE on blockchain](architecture/overview.md) - [FHE library](architecture/library.md) - [Host contracts](architecture/hostchain.md) - [Coprocessor](architecture/coprocessor.md) - [Gateway](architecture/gateway.md) - [KMS](architecture/kms.md) - [Relayer & Oracle](architecture/relayer_oracle.md) - [Roadmap](roadmap.md) ## Developer - [Change Log](https://docs.zama.ai/change-log) - [Confidential contracts by OpenZeppelin](https://docs.zama.ai/protocol/examples/openzeppelin-confidential-contracts/) - [Feature request](https://github.com/zama-ai/fhevm/issues/new?assignees=&labels=enhancement&projects=&template=feature-request.md&title=) - [Bug report](https://github.com/zama-ai/fhevm/issues/new?assignees=&labels=bug&projects=&template=bug_report_fhevm.md&title=) - [Status](https://status.zama.ai/) - [White paper](https://github.com/zama-ai/fhevm/blob/main/fhevm-whitepaper.pdf) - [Release note](https://github.com/zama-ai/fhevm/releases) - [Previous docs (v0.6)](https://docs.zama.ai/fhevm/0.6) - [Contributing](contribute.md) --- ## sdk-guides/cli.md > _From `sdk-guides/cli.md`_ # Using the CLI The `fhevm` Command-Line Interface (CLI) tool provides a simple and efficient way to encrypt data for use with the blockchain's Fully Homomorphic Encryption (FHE) system. This guide explains how to install and use the CLI to encrypt integers and booleans for confidential smart contracts. ## Installation Ensure you have [Node.js](https://nodejs.org/) installed on your system before proceeding. Then, globally install the `@zama-fhe/relayer-sdk` package to enable the CLI tool: ```bash npm install -g @zama-fhe/relayer-sdk ``` Once installed, you can access the CLI using the `relayer` command. Verify the installation and explore available commands using: ```bash relayer help ``` ## Encrypting Data The CLI allows you to encrypt integers and booleans for use in smart contracts. Encryption is performed using the blockchain's FHE public key, ensuring the confidentiality of your data. ### Syntax ```bash relayer encrypt --node ... ``` - **`--node`**: Specifies the RPC URL of the blockchain node (e.g., `http://localhost:8545`). - **``**: The address of the contract interacting with the encrypted data. - **``**: The address of the user associated with the encrypted data. - **``**: The data to encrypt, followed by its type: - `:64` for 64-bit integers - `:1` for booleans ### Example Usage Encrypt the integer `71721075` (64-bit) and the boolean `1` for the contract at `0x8Fdb26641d14a80FCCBE87BF455338Dd9C539a50` and the user at `0xa5e1defb98EFe38EBb2D958CEe052410247F4c80`: ```bash relayer encrypt 0x8Fdb26641d14a80FCCBE87BF455338Dd9C539a50 0xa5e1defb98EFe38EBb2D958CEe052410247F4c80 71721075:64 1:1 ``` --- ## sdk-guides/initialization.md > _From `sdk-guides/initialization.md`_ # Setup The use of `@zama-fhe/relayer-sdk` requires a setup phase. This consists of the instantiation of the `FhevmInstance`. This object holds all the configuration and methods needed to interact with an FHEVM using a Relayer. It can be created using the following code snippet: ```ts import { createInstance } from "@zama-fhe/relayer-sdk"; const instance = await createInstance({ // ACL_CONTRACT_ADDRESS (FHEVM Host chain) aclContractAddress: "0x687820221192C5B662b25367F70076A37bc79b6c", // KMS_VERIFIER_CONTRACT_ADDRESS (FHEVM Host chain) kmsContractAddress: "0x1364cBBf2cDF5032C47d8226a6f6FBD2AFCDacAC", // INPUT_VERIFIER_CONTRACT_ADDRESS (FHEVM Host chain) inputVerifierContractAddress: "0xbc91f3daD1A5F19F8390c400196e58073B6a0BC4", // DECRYPTION_ADDRESS (Gateway chain) verifyingContractAddressDecryption: "0xb6E160B1ff80D67Bfe90A85eE06Ce0A2613607D1", // INPUT_VERIFICATION_ADDRESS (Gateway chain) verifyingContractAddressInputVerification: "0x7048C39f048125eDa9d678AEbaDfB22F7900a29F", // FHEVM Host chain id chainId: 11155111, // Gateway chain id gatewayChainId: 55815, // Optional RPC provider to host chain network: "https://eth-sepolia.public.blastapi.io", // Relayer URL relayerUrl: "https://relayer.testnet.zama.cloud", }); ``` or the even simpler: ```ts import { createInstance, SepoliaConfig } from "@zama-fhe/relayer-sdk"; const instance = await createInstance(SepoliaConfig); ``` The information regarding the configuration of Sepolia's FHEVM and associated Relayer maintained by Zama can be found in the `SepoliaConfig` object or in the [contract addresses page](https://docs.zama.ai/protocol/solidity-guides/smart-contract/configure/contract_addresses). The `gatewayChainId` is `55815`. The `chainId` is the chain-id of the FHEVM chain, so for Sepolia it would be `11155111`. {% hint style="info" %} For more information on the Relayer's part in the overall architecture please refer to [the Relayer's page in the architecture documentation](https://docs.zama.ai/protocol/protocol/overview/relayer_oracle). {% endhint %} --- ## sdk-guides/input.md > _From `sdk-guides/input.md`_ # Input registration This document explains how to register ciphertexts to the FHEVM. Registering ciphertexts to the FHEVM allows for future use on-chain using the `FHE.fromExternal` solidity function. All values encrypted for use with the FHEVM are encrypted under a public key of the protocol. ```ts // We first create a buffer for values to encrypt and register to the fhevm const buffer = instance.createEncryptedInput( // The address of the contract allowed to interact with the "fresh" ciphertexts contractAddress, // The address of the entity allowed to import ciphertexts to the contract at `contractAddress` userAddress, ); // We add the values with associated data-type method buffer.add64(BigInt(23393893233)); buffer.add64(BigInt(1)); // buffer.addBool(false); // buffer.add8(BigInt(43)); // buffer.add16(BigInt(87)); // buffer.add32(BigInt(2339389323)); // buffer.add128(BigInt(233938932390)); // buffer.addAddress('0xa5e1defb98EFe38EBb2D958CEe052410247F4c80'); // buffer.add256(BigInt('2339389323922393930')); // This will encrypt the values, generate a proof of knowledge for it, and then upload the ciphertexts using the relayer. // This action will return the list of ciphertext handles. const ciphertexts = await buffer.encrypt(); ``` With a contract `MyContract` that implements the following it is possible to add two "fresh" ciphertexts. ```solidity contract MyContract { ... function add( externalEuint64 a, externalEuint64 b, bytes calldata proof ) public virtual returns (euint64) { return FHE.add(FHE.fromExternal(a, proof), FHE.fromExternal(b, proof)) } } ``` With `my_contract` the contract in question using `ethers` it is possible to call the add function as following. ```js my_contract.add(ciphertexts.handles[0], ciphertexts.handles[1], ciphertexts.inputProof); ``` --- ## sdk-guides/public-decryption.md > _From `sdk-guides/public-decryption.md`_ # Public Decryption This document explains how to perform public decryption of FHEVM ciphertexts. Public decryption is required when you want everyone to see the value in a ciphertext, for example the result of private auction. Public decryption can be done with either the Relayer HTTP endpoint or calling the on-chain decryption oracle. ## HTTP Public Decrypt Calling the public decryption endpoint of the Relayer can be done easily using the following code snippet. ```ts // A list of ciphertexts handles to decrypt const handles = [ "0x830a61b343d2f3de67ec59cb18961fd086085c1c73ff0000000000aa36a70000", "0x98ee526413903d4613feedb9c8fa44fe3f4ed0dd00ff0000000000aa36a70400", "0xb837a645c9672e7588d49c5c43f4759a63447ea581ff0000000000aa36a70700", ]; // The list of decrypted values // { // '0x830a61b343d2f3de67ec59cb18961fd086085c1c73ff0000000000aa36a70000': true, // '0x98ee526413903d4613feedb9c8fa44fe3f4ed0dd00ff0000000000aa36a70400': 242n, // '0xb837a645c9672e7588d49c5c43f4759a63447ea581ff0000000000aa36a70700': '0xfC4382C084fCA3f4fB07c3BCDA906C01797595a8' // } const values = instance.publicDecrypt(handles); ``` ## Onchain Public Decrypt For more details please refer to the on [onchain Oracle public decryption page](https://docs.zama.ai/protocol/solidity-guides/smart-contract/oracle). --- ## sdk-guides/sdk-overview.md > _From `sdk-guides/sdk-overview.md`_ # Relayer SDK **Welcome to the Relayer SDK Docs.** This section provides an overview of the key features of Zama’s FHEVM Relayer JavaScript SDK. The SDK lets you interact with FHEVM smart contracts without dealing directly with the [Gateway Chain](https://docs.zama.ai/protocol/protocol/overview/gateway). With the Relayer, FHEVM clients only need a wallet on the FHEVM host chain. All interactions with the Gateway chain are handled through HTTP calls to Zama's Relayer, which pays for it on the Gateway chain. ## Where to go next If you’re new to the Zama Protocol, start with the [Litepaper](https://docs.zama.ai/protocol/zama-protocol-litepaper) or the [Protocol Overview](https://docs.zama.ai/protocol) to understand the foundations. Otherwise: 🟨 Go to [**Setup guide**](initialization.md) to learn how to configure the Relayer SDK for your project. 🟨 Go to [**Input registration**](input.md) to see how to register new encrypted inputs for your smart contracts. 🟨 Go to [**User decryption**](user-decryption.md) to enable users to decrypt data with their own keys, once permissions have been granted via Access Control List(ACL). 🟨 Go to [**Public decryption**](public-decryption.md) to learn how to decrypt outputs that are publicly accessible, either via HTTP or onchain Oracle. 🟨 Go to [**Solidity ACL Guide**](https://docs.zama.ai/protocol/solidity-guides/smart-contract/acl) for more detailed instructions about access control. ## Help center Ask technical questions and discuss with the community. - [Community forum](https://community.zama.ai/c/zama-protocol/15) - [Discord channel](https://discord.com/invite/zama) --- ## sdk-guides/SUMMARY.md > _From `sdk-guides/SUMMARY.md`_ - [Overview](sdk-overview.md) ## FHEVM Relayer - [Initialization](initialization.md) - [Input](input.md) - Decryption - [User decryption](user-decryption.md) - [Public decryption](public-decryption.md) ## Development Guide - [Web applications](webapp.md) - [Debugging](webpack.md) - [CLI](cli.md) --- ## sdk-guides/user-decryption.md > _From `sdk-guides/user-decryption.md`_ # User decryption This document explains how to perform user decryption. User decryption is required when you want a user to access their private data without it being exposed to the blockchain. User decryption in FHEVM enables the secure sharing or reuse of encrypted data under a new public key without exposing the plaintext. This feature is essential for scenarios where encrypted data must be transferred between contracts, dApps, or users while maintaining its confidentiality. ## When to use user decryption User decryption is particularly useful for **allowing individual users to securely access and decrypt their private data**, such as balances or counters, while maintaining data confidentiality. ## Overview The user decryption process involves retrieving ciphertext from the blockchain and performing user-decryption on the client-side. In other words we take the data that has been encrypted by the KMS, decrypt it and encrypt it with the user's private key, so only he can access the information. This ensures that the data remains encrypted under the blockchain’s FHE key but can be securely shared with a user by re-encrypting it under the user’s NaCl public key. User decryption is facilitated by the **Relayer** and the **Key Management System (KMS)**. The workflow consists of the following: 1. Retrieving the ciphertext from the blockchain using a contract’s view function. 2. Re-encrypting the ciphertext client-side with the user’s public key, ensuring only the user can decrypt it. ## Step 1: retrieve the ciphertext To retrieve the ciphertext that needs to be decrypted, you can implement a view function in your smart contract. Below is an example implementation: ```solidity import "@fhevm/solidity/lib/FHE.sol"; contract ConfidentialERC20 { ... function balanceOf(account address) public view returns (euint64) { return balances[msg.sender]; } ... } ``` Here, `balanceOf` allows retrieval of the user’s encrypted balance handle stored on the blockchain. Doing this will return the ciphertext handle, an identifier for the underlying ciphertext. {% hint style="warning" %} For the user to be able to user decrypt (also called re-encrypt) the ciphertext value the access control (ACL) needs to be set properly using the `FHE.allow(ciphertext, address)` function in the solidity contract holding the ciphertext. For more details on the topic please refer to [the ACL documentation](../solidity-guides/acl/README.md). {% endhint %} ## Step 2: decrypt the ciphertext Using that ciphertext handle user decryption is performed client-side using the `@zama-fhe/relayer-sdk` library. The user needs to have created an instance object prior to that (for more context see [the relayer-sdk setup page](./initialization.md)). ```ts // instance: [`FhevmInstance`] from `zama-fhe/relayer-sdk` // signer: [`Signer`] from ethers (could a [`Wallet`]) // ciphertextHandle: [`string`] // contractAddress: [`string`] const keypair = instance.generateKeypair(); const handleContractPairs = [ { handle: ciphertextHandle, contractAddress: contractAddress, }, ]; const startTimeStamp = Math.floor(Date.now() / 1000).toString(); const durationDays = "10"; // String for consistency const contractAddresses = [contractAddress]; const eip712 = instance.createEIP712(keypair.publicKey, contractAddresses, startTimeStamp, durationDays); const signature = await signer.signTypedData( eip712.domain, { UserDecryptRequestVerification: eip712.types.UserDecryptRequestVerification, }, eip712.message, ); const result = await instance.userDecrypt( handleContractPairs, keypair.privateKey, keypair.publicKey, signature.replace("0x", ""), contractAddresses, signer.address, startTimeStamp, durationDays, ); const decryptedValue = result[ciphertextHandle]; ``` --- ## sdk-guides/webapp.md > _From `sdk-guides/webapp.md`_ # Build a web application This document guides you through building a web application using the `@zama-fhe/relayer-sdk` library. ## Using directly the library ### Step 1: Setup the library `@zama-fhe/relayer-sdk` consists of multiple files, including WASM files and WebWorkers, which can make packaging these components correctly in your setup cumbersome. To simplify this process, especially if you're developing a dApp with server-side rendering (SSR), we recommend using our CDN. #### Using UMD CDN Include this line at the top of your project. ```html ``` In your project, you can use the bundle import if you install `@zama-fhe/relayer-sdk` package: ```javascript import { initSDK, createInstance, SepoliaConfig } from "@zama-fhe/relayer-sdk/bundle"; ``` #### Using ESM CDN If you prefer You can also use the `@zama-fhe/relayer-sdk` as a ES module: ```html ``` #### Using npm package Install the `@zama-fhe/relayer-sdk` library to your project: ```bash # Using npm npm install @zama-fhe/relayer-sdk # Using Yarn yarn add @zama-fhe/relayer-sdk # Using pnpm pnpm add @zama-fhe/relayer-sdk ``` `@zama-fhe/relayer-sdk` uses ESM format. You need to set the [type to "module" in your package.json](https://nodejs.org/api/packages.html#type). If your node project use `"type": "commonjs"` or no type, you can force the loading of the web version by using `import { createInstance } from '@zama-fhe/relayer-sdk/web';` ```javascript import { initSDK, createInstance, SepoliaConfig } from "@zama-fhe/relayer-sdk"; ``` ### Step 2: Initialize your project To use the library in your project, you need to load the WASM of [TFHE](https://www.npmjs.com/package/tfhe) first with `initSDK`. ```javascript import { initSDK } from "@zama-fhe/relayer-sdk/bundle"; const init = async () => { await initSDK(); // Load needed WASM }; ``` ### Step 3: Create an instance Once the WASM is loaded, you can now create an instance. ```javascript import { initSDK, createInstance, SepoliaConfig } from "@zama-fhe/relayer-sdk/bundle"; const init = async () => { await initSDK(); // Load FHE const config = { ...SepoliaConfig, network: window.ethereum }; return createInstance(config); }; init().then((instance) => { console.log(instance); }); ``` You can now use your instance to [encrypt parameters](./input.md), perform [user decryptions](./user-decryption.md) or [public decryptions](./public-decryption.md). --- ## sdk-guides/webpack.md > _From `sdk-guides/webpack.md`_ # Common webpack errors This document provides solutions for common Webpack errors encountered during the development process. Follow the steps below to resolve each issue. ## Can't resolve 'tfhe_bg.wasm' **Error message:** `Module not found: Error: Can't resolve 'tfhe_bg.wasm'` **Cause:** In the codebase, there is a `new URL('tfhe_bg.wasm')` which triggers a resolve by Webpack. **Possible solutions:** You can add a fallback for this file by adding a resolve configuration in your `webpack.config.js`: ```javascript resolve: { fallback: { 'tfhe_bg.wasm': require.resolve('tfhe/tfhe_bg.wasm'), }, }, ``` ## Buffer not defined **Error message:** `ReferenceError: Buffer is not defined` **Cause:** This error occurs when the Node.js `Buffer` object is used in a browser environment where it is not natively available. **Possible solutions:** To resolve this issue, you need to provide browser-compatible fallbacks for Node.js core modules. Install the necessary browserified npm packages and configure Webpack to use these fallbacks. ```javascript 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 **Error message:** Issues with importing ESM version **Cause:** With a bundler such as Webpack or Rollup, imports will be replaced with the version mentioned in the `"browser"` field of the `package.json`. This can cause issues with typing. **Possible solutions:** - If you encounter issues with typing, you can use this [tsconfig.json](https://github.com/zama-ai/fhevm-react-template/blob/main/tsconfig.json) using TypeScript 5. - If you encounter any other issue, you can force import of the browser package. ## Use bundled version **Error message:** Issues with bundling the library, especially with SSR frameworks. **Cause:** The library may not bundle correctly with certain frameworks, leading to errors during the build or runtime process. **Possible solutions:** Use the [prebundled version available](./webapp.md) with `@zama-fhe/relayer-sdk/bundle`. Embed the library with a `