4. Test the FHEVM contract
In this tutorial, you’ll learn how to migrate a standard Hardhat test suite - from Counter.ts
to its FHEVM-compatible version FHECounter.ts
— and progressively enhance it to support Fully Homomorphic Encryption using Zama’s FHEVM library.
Set up the FHEVM testing environment
Create a test script test/FHECounter.ts
test/FHECounter.ts
Go to your project's test
directory
cd <your-project-root-directory>/test
From there, create a new file named FHECounter.ts
and copy/paste the following Typescript skeleton code in it.
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("should be deployed", async function () {
console.log(`FHECounter has been deployed at address ${fheCounterContractAddress}`);
// Test the deployed address is valid
expect(ethers.isAddress(fheCounterContractAddress)).to.eq(true);
});
// 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);
// });
});
What’s Different from Counter.ts
?
Counter.ts
?This test file is structurally similar to the original
Counter.ts
, but it uses the FHEVM-compatible smart contractFHECounter
instead of the regularCounter
.
– For clarity, the Counter
unit tests are included as comments, allowing you to better understand how each part is adapted during the migration to FHEVM.
While the test logic remains the same, this version is now set up to support encrypted computations via the FHEVM library — enabling tests that manipulate confidential values directly on-chain.
Test functions
Now everything is up and running, you can start testing your contract functions.
Call the contract getCount()
view function
getCount()
view functionReplace the commented‐out test for the legacy Counter
contract:
// 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);
// });
with its FHEVM equivalent:
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);
});
What’s different?
– encryptedCount
is no longer a plain TypeScript number. It is now a hexadecimal string representing a Solidity bytes32
value, known as an FHEVM handle. This handle points to an encrypted FHEVM primitive of type euint32
, which internally represents an encrypted Solidity uint32
primitive type.
encryptedCount
is equal to0x0000000000000000000000000000000000000000000000000000000000000000
which means thatencryptedCount
is uninitialized, and does not reference to any encrypted value at this point.
Run the test
From your project's root directory, run:
npx hardhat test
Expected Output
Counter
Counter has been deployed at address 0x7553CB9124f974Ee475E5cE45482F90d5B6076BC
✔ should be deployed
✔ encrypted count should be uninitialized after deployment
2 passing (7ms)
Setup the increment()
function unit test
increment()
function unit testWe’ll migrate the increment()
unit test to FHEVM step by step. To start, let’s handle the value of the counter before the first increment. As explained above, the counter is initially a bytes32
value equal to zero, meaning the FHEVM euint32
variable is uninitialized.
We’ll interpret this as if the underlying clear value is 0.
Replace the commented‐out test for the legacy Counter
contract:
// 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);
// });
with the following:
it("increment the counter by 1", async function () {
const encryptedCountBeforeInc = await fheCounterContract.getCount();
expect(encryptedCountBeforeInc).to.eq(ethers.ZeroHash);
const clearCountBeforeInc = 0;
// const tx = await counterContract.connect(signers.alice).increment(1);
// await tx.wait();
// const countAfterInc = await counterContract.getCount();
// expect(countAfterInc).to.eq(countBeforeInc + 1n);
});
Encrypt the increment()
function argument
increment()
function argumentThe increment()
function takes a single argument: the value by which the counter should be incremented. In the initial version of Counter.sol
, this value is a clear uint32
.
We’ll switch to passing an encrypted value instead, using FHEVM externalEuint32
primitive type. This allows us to securely increment the counter without revealing the input value on-chain.
Replace :
it("increment the counter by 1", async function () {
const encryptedCountBeforeInc = await fheCounterContract.getCount();
expect(encryptedCountBeforeInc).to.eq(ethers.ZeroHash);
const clearCountBeforeInc = 0;
// const tx = await counterContract.connect(signers.alice).increment(1);
// await tx.wait();
// const countAfterInc = await counterContract.getCount();
// expect(countAfterInc).to.eq(countBeforeInc + 1n);
});
with the following:
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 counterContract.connect(signers.alice).increment(1);
// await tx.wait();
// const countAfterInc = await counterContract.getCount();
// expect(countAfterInc).to.eq(countBeforeInc + 1n);
});
Call the increment()
function with the encrypted argument
increment()
function with the encrypted argumentNow that we have an encrypted argument, we can call the increment()
function with it.
Below, you’ll notice that the updated increment()
function now takes two arguments instead of one.
This is because the FHEVM requires both:
The
externalEuint32
— the encrypted value itselfAn accompanying Zero-Knowledge Proof of Knowledge (
inputProof
) — which verifies that the encrypted input is securely bound to:the caller (Alice, the transaction signer), and
the target smart contract (where
increment()
is being executed)
This ensures that the encrypted value cannot be reused in a different context or by a different user, preserving confidentiality and integrity.
Replace :
// const tx = await counterContract.connect(signers.alice).increment(1);
// await tx.wait();
with the following:
const tx = await fheCounterContract.connect(signers.alice).increment(encryptedOne.handles[0], encryptedOne.inputProof);
await tx.wait();
At this point the counter has been successfully incremented by 1 using a Fully Homomorphic Encryption (FHE). In the next step, we will retrieve the updated encrypted counter value and decrypt it locally. But before we move on, let’s quickly run the tests to make sure everything is working correctly.
Run the test
From your project's root directory, run:
npx hardhat test
Expected Output
FHECounter
FHECounter has been deployed at address 0x7553CB9124f974Ee475E5cE45482F90d5B6076BC
✔ should be deployed
✔ encrypted count should be uninitialized after deployment
✔ increment the counter by 1
3 passing (7ms)
Call the getCount()
function and Decrypt the value
getCount()
function and Decrypt the valueNow that the counter has been incremented using an encrypted input, it's time to read the updated encrypted value from the smart contract and decrypt it using the userDecryptEuint
function provided by the FHEVM Hardhat Plugin.
The userDecryptEuint
function takes four parameters:
FhevmType: The integer type of the FHE-encrypted value. In this case, we're using
FhevmType.euint32
because the counter is auint32
.Encrypted handle: A 32-byte FHEVM handle representing the encrypted value you want to decrypt.
Smart contract address: The address of the contract that has permission to access the encrypted handle.
User signer: The signer (e.g., signers.alice) who has permission to access the handle.
Replace :
// const countAfterInc = await counterContract.getCount();
// expect(countAfterInc).to.eq(countBeforeInc + 1n);
with the following:
const encryptedCountAfterInc = await fheCounterContract.getCount();
const clearCountAfterInc = await fhevm.userDecryptEuint(
FhevmType.euint32,
encryptedCountAfterInc,
fheCounterContractAddress,
signers.alice,
);
expect(clearCountAfterInc).to.eq(clearCountBeforeInc + clearOne);
Run the test
From your project's root directory, run:
npx hardhat test
Expected Output
FHECounter
FHECounter has been deployed at address 0x7553CB9124f974Ee475E5cE45482F90d5B6076BC
✔ should be deployed
✔ encrypted count should be uninitialized after deployment
✔ increment the counter by 1
3 passing (7ms)
Call the contract decrement()
function
decrement()
functionSimilarly to the previous test, we’ll now call the decrement()
function using an encrypted input.
Replace :
// 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);
// });
with the following:
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);
});
Run the test
From your project's root directory, run:
npx hardhat test
Expected Output
FHECounter
FHECounter has been deployed at address 0x7553CB9124f974Ee475E5cE45482F90d5B6076BC
✔ should be deployed
✔ encrypted count should be uninitialized after deployment
✔ increment the counter by 1
✔ decrement the counter by 1
4 passing (7ms)
Congratulations! You've completed the full tutorial.
You have successfully written and tested your FHEVM-based counter smart contract. By now, your project should include the following files:
contracts/FHECounter.sol
— your Solidity smart contracttest/FHECounter.ts
— your Hardhat test suite written in TypeScript
Last updated