Back to Fhevm

Prerequisite

docs/solidity-guides/hardhat/write_task.md

0.12.36.3 KB
Original Source

In this section, you'll learn how to write a custom FHEVM Hardhat task.

Writing tasks is a gas-efficient and flexible way to test your FHEVM smart contracts on the Sepolia network. Creating a custom task is straightforward.

Prerequisite

{% stepper %} {% step %}

A Basic Hardhat Task.

Let’s start with a simple example: fetching the current counter value from a basic Counter.sol contract.

If you're already familiar with Hardhat and custom tasks, the TypeScript code below should look familiar and be easy to follow:

ts
task("task:get-count", "Calls the getCount() function of Counter Contract")
  .addOptionalParam("address", "Optionally specify the Counter contract address")
  .setAction(async function (taskArguments: TaskArguments, hre) {
    const { ethers, deployments } = hre;

    const CounterDeployment = taskArguments.address
      ? { address: taskArguments.address }
      : await deployments.get("Counter");
    console.log(`Counter: ${CounterDeployment.address}`);

    const counterContract = await ethers.getContractAt("Counter", CounterDeployment.address);

    const clearCount = await counterContract.getCount();

    console.log(`Clear count    : ${clearCount}`);
});

Now, let’s modify this task to work with FHEVM encrypted values.

{% endstep %} {% step %}

Comment Out Existing Logic and rename

First, comment out the existing logic so we can incrementally add the necessary changes for FHEVM integration.

ts
task("task:get-count", "Calls the getCount() function of Counter Contract")
  .addOptionalParam("address", "Optionally specify the Counter contract address")
  .setAction(async function (taskArguments: TaskArguments, hre) {
    // const { ethers, deployments } = hre;

    // const CounterDeployment = taskArguments.address
    //   ? { address: taskArguments.address }
    //   : await deployments.get("Counter");
    // console.log(`Counter: ${CounterDeployment.address}`);

    // const counterContract = await ethers.getContractAt("Counter", CounterDeployment.address);

    // const clearCount = await counterContract.getCount();

    // console.log(`Clear count    : ${clearCount}`);
});

Next, rename the task by replacing:

ts
task("task:get-count", "Calls the getCount() function of Counter Contract")

With:

ts
task("task:decrypt-count", "Calls the getCount() function of Counter Contract")

This updates the task name from task:get-count to task:decrypt-count, reflecting that it now includes decryption logic for FHE-encrypted values.

{% endstep %} {% step %}

Initialize FHEVM CLI API

Replace the line:

ts
    // const { ethers, deployments } = hre;

With:

ts
    const { ethers, deployments, fhevm } = hre;

    await fhevm.initializeCLIApi();

{% hint style="warning" %} Calling initializeCLIApi() is essential. Unlike built-in Hardhat tasks like test or compile, which automatically initialize the FHEVM runtime environment, custom tasks require you to call this function explicitly. Make sure to call it at the very beginning of your task to ensure the environment is properly set up. {% endhint %}

{% endstep %} {% step %}

Call the view function getCount from the FHECounter contract

Replace the following commented-out lines:

ts
    // const CounterDeployment = taskArguments.address
    //   ? { address: taskArguments.address }
    //   : await deployments.get("Counter");
    // console.log(`Counter: ${CounterDeployment.address}`);

    // const counterContract = await ethers.getContractAt("Counter", CounterDeployment.address);

    // const clearCount = await counterContract.getCount();

With the FHEVM equivalent:

ts
    const FHECounterDeployment = taskArguments.address
      ? { address: taskArguments.address }
      : await deployments.get("FHECounter");
    console.log(`FHECounter: ${FHECounterDeployment.address}`);

    const fheCounterContract = await ethers.getContractAt("FHECounter", FHECounterDeployment.address);

    const encryptedCount = await fheCounterContract.getCount();
    if (encryptedCount === ethers.ZeroHash) {
      console.log(`encrypted count: ${encryptedCount}`);
      console.log("clear count    : 0");
      return;
    }

Here, encryptedCount is an FHE-encrypted euint32 primitive. To retrieve the actual value, we need to decrypt it in the next step.

{% endstep %} {% step %}

Decrypt the encrypted count value.

Now replace the following commented-out line:

ts
    // console.log(`Clear count    : ${clearCount}`);

With the decryption logic:

ts
    const signers = await ethers.getSigners();
    const clearCount = await fhevm.userDecryptEuint(
      FhevmType.euint32,
      encryptedCount,
      FHECounterDeployment.address,
      signers[0],
    );
    console.log(`Encrypted count: ${encryptedCount}`);
    console.log(`Clear count    : ${clearCount}`);

At this point, your custom Hardhat task is fully configured to work with FHE-encrypted values and ready to run!

{% endstep %} {% step %}

Step 6: Run your custom task using Hardhat Node

Start the Local Hardhat Node:

  • Open a new terminal window.
  • From the root project directory, run the following:
sh
npx hardhat node

Deploy the FHECounter smart contract on the local Hardhat Node

sh
npx hardhat deploy --network localhost

Run your custom task

sh
npx hardhat task:decrypt-count --network localhost

{% endstep %} {% step %}

Step 7: Run your custom task using Sepolia

Deploy the FHECounter smart contract on Sepolia Testnet (if not already deployed)

sh
npx hardhat deploy --network sepolia

Execute your custom task

sh
npx hardhat task:decrypt-count --network sepolia

{% endstep %} {% endstepper %}