DEV Community

Cover image for Build Your First FHE Smart Contract with Fhenix: A Hands-On Beginner's Guide
Azeez Abidoye
Azeez Abidoye

Posted on

Build Your First FHE Smart Contract with Fhenix: A Hands-On Beginner's Guide

Gm Devs 🤗

Welcome to this beginner-friendly tutorial on smart contract using FHE (Fully Homomorphic Encryption). This guide will help you understand how encrypted variables and operations work in Solidity.

What is FHE (Fully Homomorphic Encryption)?

Fully Homomorphic Encryption (FHE) is a cryptographic breakthrough that allows computations to be performed on encrypted data without decrypting it first. In traditional smart contracts, all data is public. Even private variables are visible on the blockchain. With FHE, the data remains encrypted at all times during computation, enabling privacy-preserving applications simultaneously with smart contract transparency.

For a beginner: Imagine a locked opaque box with arm holes. You can stick your hands in the holes and manipulate objects inside the box, but you can't tell what the objects look like, and nobody else can see them either. The contents remain secure, yet you can still work with them.

In this contract, we use FHE to keep our counter values entirely secret, demonstrating how to handle sensitive user data on the blockchain.


Prerequisites 🛠️

  • NodeJs (v20 or later)
  • PNPM (recommended)
  • MetaMask
  • Testnet Ethers
  • Fundamental knowledge of Solidity & Hardhat

Let the hacking begin... 👨‍💻

Step 1: Initialize the FHE project & navigate into the new project directory

git clone https://github.com/fhenixprotocol/cofhe-hardhat-starter.git
cd cofhe-hardhat-starter
Enter fullscreen mode Exit fullscreen mode

Step 2: Install dependencies

pnpm install
Enter fullscreen mode Exit fullscreen mode

Folder Structure 📚

  • Contracts: This directory contains all of your project's smart contract files. It currently contains a Counter.sol smart contract written in Solidity.
  • deployments: Structured to save the contract address after deployment. Though currently empty, the json file with the contract address will automatically save in this directory after deployment.
  • tasks: The directory includes five Typescript files with various configurations and execution logics:

    • deploy-counter.ts: Contains all of the logic run for the successful deployment of the Solidity smart contract.
    • increment-counter.ts: Contains the code executed to interact with the deployed contract by calling the increment() function.
    • index.ts: This is the configuration file that exports all written contract interaction logics.
    • reset-counter.ts: Contains the code executed to interact with the deployed contract using the reset() function
    • utils.ts: Utility file containing code logic that generates a new file automatically after contract deployment to save deployment information in the ./deployments directory
  • test: Contract test files are saved in this directory, which currently contains the Counter.test.ts file.

    • Counter.test.ts: Contains all of the test logics for the solidity smart contract.

Code Walkthrough: Line by Line 🔍

Let's break down the Counter.sol contract to see how it operates.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;
Enter fullscreen mode Exit fullscreen mode
  • Line 1: Declares the software license (UNLICENSED means it isn't licensed for general public distribution).
  • Line 2: Defines the required Solidity compiler version (^0.8.25), meaning any version starting from 0.8.25 up to 0.8.99 will work.
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
Enter fullscreen mode Exit fullscreen mode
  • Line 4: Imports the FHE library from the Fhenix protocol. This library provides all the specialized tools to create, manipulate, and decrypt encrypted values safely on the blockchain.
contract Counter {
Enter fullscreen mode Exit fullscreen mode
  • Line 6: Begins the declaration of our smart contract named Counter.
    euint32 public count;
    euint32 public ONE;
    ebool public isInitialized;
Enter fullscreen mode Exit fullscreen mode
  • Lines 7-9: We declare our state variables. Instead of the standard uint32 or bool, we use euint32 (encrypted unsigned integer of 32 bits) and ebool (encrypted boolean). These variables store encrypted data directly on the blockchain.
    • count: Stores the encrypted current count.
    • ONE: Stores an encrypted value of 1, to be used in math operations.
    • isInitialized: A flag to keep track of if our contract has been set up.
    constructor() {
        ONE = FHE.asEuint32(1);
        count = FHE.asEuint32(0);
Enter fullscreen mode Exit fullscreen mode
  • Lines 11-13: Contains the constructor function, which runs only once upon deployment. We assign encrypted values to our state variables using FHE.asEuint32(). This securely encrypts 1 into ONE and 0 into count.
        isInitialized = FHE.asEbool(false);
        isInitialized = FHE.asEbool(true);
Enter fullscreen mode Exit fullscreen mode
  • Lines 15-16: Initializes the isInitialized boolean securely.
        FHE.allowThis(count);
        FHE.allowThis(ONE);
Enter fullscreen mode Exit fullscreen mode
  • Lines 18-19: Grants the smart contract itself the permission to use and access the encrypted values count and ONE. Managing viewing rights is crucial for FHE!
        FHE.gte(count, ONE);
Enter fullscreen mode Exit fullscreen mode
  • Line 21: Performs an encrypted "Greater Than or Equal to" (>=) comparison between count and ONE. This line is generally used during test deployments to ensure the FHE comparison coprocessor is running healthily.
        FHE.allowSender(count);
    }
Enter fullscreen mode Exit fullscreen mode
  • Lines 23-24: Grants the deployer of the contract permissions to view the count variable and closes the constructor.
    function increment() public {
        count = FHE.add(count, ONE);
        FHE.allowThis(count);
        FHE.allowSender(count);
    }
Enter fullscreen mode Exit fullscreen mode
  • Lines 26-30: A public function increment() that anyone can call. Instead of standard Solidity additions like count++, it uses FHE.add(count, ONE) to securely add the encrypted variables. It then updates permissions using allowThis and allowSender over the new encrypted result.

✍️ Practical Use Case: Imagine updating user voting tallies secretly without revealing individual votes.

    function decrement() public {
        count = FHE.sub(count, ONE);
        FHE.allowThis(count);
        FHE.allowSender(count);
    }
Enter fullscreen mode Exit fullscreen mode
  • Lines 32-36: A public function decrement(). It uses FHE.sub() to securely subtract the encrypted ONE from count, and re-assigns permissions exactly like the increment() function.
    function reset(InEuint32 memory value) public {
        count = FHE.asEuint32(value);
        FHE.allowThis(count);
        FHE.allowSender(count);
    }
Enter fullscreen mode Exit fullscreen mode
  • Lines 38-42: A public function reset(). It takes a special parameter InEuint32 value (an incoming encrypted number passed directly by a user). It casts this to a persistent encrypted value count = FHE.asEuint32(value) and updates access permissions for it.

✍️ Practical Use Case: A user setting a confidential PIN or private bid in an auction without exposing their true number on the live network.

    function decryptCounter() public {
        FHE.decrypt(count);
    }
Enter fullscreen mode Exit fullscreen mode
  • Lines 44-46: Initiates the decryption process for our encrypted count. For security and computational reasons, actual network decryption is often an asynchronous process in FHE-based blockchains.
    function getDecryptedValue() external view returns(uint256) {
        (uint256 value, bool decrypted) = FHE.getDecryptResultSafe(count);
        if (!decrypted)
            revert("Value is not ready");

        return value;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Lines 48-55: Closes our contract. A function to finally read the decrypted result. It uses FHE.getDecryptResultSafe(count) which returns the plain integer value and a boolean decrypted indicating if the asynchronous decryption process initiated earlier is finished. If it's not ready, it reverts with an error message.

Comparison: FHE vs. Standard Boilerplate 📝

If you are coming from standard development environments like Foundry, you may be familiar with the default Counter.sol boilerplate code:

Standard Boilerplate:

contract Counter {
    uint256 public number;

    function setNumber(uint256 newNumber) public { number = newNumber; }
    function increment() public { number++; }
}
Enter fullscreen mode Exit fullscreen mode

Here's an analysis of the key structural, functional, and architectural differences compared to our FHE Counter:

1. Variables and Data Structure

  • Standard: Uses uint256 which is an entirely public data type. Anyone auditing the blockchain can see its exact value.
  • FHE Counter: Uses euint32 and ebool. These variables are homomorphically encrypted. Their real values are strictly hidden, and the state stored on-chain relies primarily on ciphertexts rather than plain numbers.

2. Performing Math (Functional Differences)

  • Standard: You can use native Solidity operators like number++ or number + 1.
  • FHE Counter: Because data is heavily encrypted, standard EVM commands won't work on them. We must instead use FHE library functions like FHE.add(count, ONE) and FHE.sub(count, ONE). This functionally routes the math to an encrypted state coprocessor.

3. Permissions Structure

  • Standard: Security is primarily established on deciding who can call specific state-changing methods. (e.g. require(msg.sender == owner)).
  • FHE Counter: You must now explicitly manage Data Access Rights. Whenever a secure variable is interacted with, the Counter re-establishes who can actually read the new encrypted wrapper via FHE.allowThis(count) and FHE.allowSender(count).

4. Reading Storage (Decryption Architecture)

  • Standard: You quickly write a view function or use auto-generated getters like counter.number() to fetch current variable numbers efficiently and instantly.
  • FHE Counter: Exposing data safely requires deliberate multi-step intention. Functioning safely with FHE means first triggering a rigorous on-chain decryption process (decryptCounter), and subsequently retrieving the resulting answers once finished (getDecryptedValue).

Why These Differences Exist & Implications

These changes heavily exist because Fhenix brings off-chain-like computing privacy onto a public architecture. Instead of all validator nodes directly calculating plain logic streams, specialized operations are routed to FHE coprocessors carefully integrated alongside the core EVM.

The primary implication for developers is a major shift in coding mindset: As a programmer, you must now safely and proactively manage data visibility. This paradigm slightly raises code complexity but fundamentally unlocks completely new architectural possibilities such as; private DAOs, confidential NFT bids, and unseen trading executions that were simply never possible on public chains before.


Step 3: Compile the contract

pnpm compile
Enter fullscreen mode Exit fullscreen mode
  • The compilation's output should look like this::
> hardhat compile

Generating typings for: 18 artifacts in dir: typechain-types for target: ethers-v6
Successfully generated 34 typings!
Compiled 8 Solidity files successfully (evm target: cancun).
Enter fullscreen mode Exit fullscreen mode

Step 4: Test with Mock Environment

pnpm test
Enter fullscreen mode Exit fullscreen mode

Step 5: Configure test network

  • By default, @cofhe/hardhat-plugin in the hardhat.config.ts file automatically injects FHE-compatible networks.
  • For this tutorial, we will deploy the contract on the Ethereum-Sepolia test network, with the following configuration:
// some config code here...

defaultNetwork: 'hardhat',
    // defaultNetwork: 'localcofhe',
    networks: {
        // The plugin already provides localcofhe configuration

        // Sepolia testnet configuration
        'eth-sepolia': {
            url: process.env.SEPOLIA_RPC_URL || 'https://ethereum-sepolia.publicnode.com',
            accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
            chainId: 11155111,
            gasMultiplier: 1.2,
            timeout: 60000,
            httpHeaders: {},
        },

        // other network config here...
    },
Enter fullscreen mode Exit fullscreen mode

Step 6: Setup environment variables

  • In the root folder, create a new file named .env, this file contains the environment variables required to run the contract deployment.
touch .env
Enter fullscreen mode Exit fullscreen mode
  • Save your environment variables in the .env file as follows:
PRIVATE_KEY=your_private_key_here
SEPOLIA_RPC_URL=your_sepolia_rpc_url
Enter fullscreen mode Exit fullscreen mode

Step 7: Deploying the contract

  • In this tutorial, we will deploy on the eth-sepolia network using the following command.
pnpm eth-sepolia:deploy-counter
Enter fullscreen mode Exit fullscreen mode

✍️ The command above is a combination of the CLI command pnpm, the network of choice as specified in the hardhat.config.ts file, and the logic provided in the ./tasks/deploy-counter.ts file.

  • The successful deployment should produce the following results:
> hardhat deploy-counter --network eth-sepolia

Deploying Counter to eth-sepolia...
Deploying with account: 0xCD30EA918A09FbdCB7421f5227d5eEB97fDDC25c
Counter deployed to: 0x0ccb4863881Bc1914DDDe6cEa0e8ab903073EB50
Deployment saved to /Users/azeezabidoyemac/Desktop/cofhe-hardhat-starter/deployments/eth-sepolia.json
Enter fullscreen mode Exit fullscreen mode

Yipee...Congratulations 🎉 You have successfully deployed your FHE contract!

✍️ The contract address is automatically provided in a new file named eth-sepolia.json saved in the ./deployments directory, which was previously empty.


Interacting with the contract

  • Execute the following command to call the increment() function provided in the contract
pnpm eth-sepolia:increment-counter
Enter fullscreen mode Exit fullscreen mode
  • The output should be as follows:
// > hardhat increment-counter --network eth-sepolia

Using Counter at 0x0ccb4863881Bc1914DDDe6cEa0e8ab903073EB50 on eth-sepolia
Using account: 0xCD30EA918A09FbdCB7421f5227d5eEB97fDDC25c
Current count: 0x62e83fbfdb28600fc2055640374222e623c6707f29b7ac7f3758fc56bcf08400
Incrementing counter...
Transaction hash: 0x252a28051b8d33bbc45761fb1bd6fffd7864f91c3ca2762c5d9e7dafef9253c4
New count: 0xf4b982aeb531d1c2fdeb3f64b030656a02036cc6ca7d2155bf4a8e927b210400
Unsealing new count...
1n
Enter fullscreen mode Exit fullscreen mode

✍️ The command above is a combination of the CLI command pnpm, the network of choice as specified in the hardhat.config.ts file, and the logic provided in the ./tasks/increment-counter.ts file.

  • Execute the following command to call the reset() function provided in the contract. This resets the count back to the initial value of zero
pnpm eth-sepolia:reset-counter
Enter fullscreen mode Exit fullscreen mode
  • The output should be as follows:
> hardhat reset-counter --network eth-sepolia

Using Counter at 0x0ccb4863881Bc1914DDDe6cEa0e8ab903073EB50 on eth-sepolia
Using account: 0xCD30EA918A09FbdCB7421f5227d5eEB97fDDC25c
Resetting counter...
Transaction hash: 0xe9eeaba8ad045e58ddf992796c54fefc12accf7f5337ce778654ee2747664eea
New count: 0xa1d49b27cccc811029b4874ad8866f8f2682b540729936a7481a79d069220400
Enter fullscreen mode Exit fullscreen mode

Conclusion

This tutorial explained the basics of Fully Homomorphic Encryption (FHE) and how writing Solidity smart contracts changes when you account for encrypted data. It walked through the starter kit step by step to show how FHE-enabled contracts handle variables, encryption, and decryption differently from standard smart contracts. It also covered the key steps needed to successfully deploy an FHE smart contract, helping you avoid common mistakes along the way.

As you continue building, start thinking about how to protect user data on-chain. Explore and use the Fhenix Coprocessor (CoFHE), visit www.fhenix.io to build applications that prioritize privacy while shaping the future of Web3.

Top comments (0)