Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/0xchriswilder/journey/llms.txt

Use this file to discover all available pages before exploring further.

Week 2: FHEVM Smart Contract Development

Subtitle: Write, test, and deploy confidential smart contracts Estimated Time: 10 hours of lessons + 5 hours homework

Objectives

First Contract

Write your first FHEVM smart contract with encrypted state

Design Patterns

Understand FHEVM contract design patterns and best practices

Testing

Test contracts using Hardhat with FHEVM mocks

Deployment

Deploy contracts to Sepolia testnet and verify on explorer

Milestone

Deployed and tested EncryptedCounter contract on Sepolia
Passing Hardhat test suite
Understanding of FHEVM contract patterns

Lessons

Lesson 2.1: Your First FHEVM Contract

Duration: 60 minutes
  • Create a Solidity contract that imports and uses FHE types
  • Declare encrypted state variables (euint8, euint64)
  • Implement functions that accept encrypted inputs
  • Use FHE.fromExternal() to handle client-encrypted data
  • Apply FHE.allowThis() for decryption permissions
A Plain Solidity Counter (Review)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Counter {
    uint8 public count;

    function increment() external {
        count += 1;
    }

    function getCount() external view returns (uint8) {
        return count;
    }
}
Notice: count is public — anyone can read it on-chain. What if the count itself is sensitive?
Converting to an Encrypted Counter
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import { FHE, euint32, externalEuint32 } from "@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";

contract EncryptedCounter is ZamaEthereumConfig {
    euint32 private counter;  // Encrypted! No one can read this directly.
    uint32 public revealedCount;
    bool public isRevealed;
    bool public revealRequested;

    event CounterIncremented(address indexed by);
    event RevealRequested(bytes32 counterHandle);

    constructor() {
        counter = FHE.asEuint32(0);  // Initialize with encrypted zero
        FHE.allowThis(counter);       // Allow contract to work with it
    }

    function increment(
        externalEuint32 encryptedAmount,
        bytes calldata proof
    ) external {
        // Convert external encrypted input to internal type
        euint32 amount = FHE.fromExternal(encryptedAmount, proof);

        // Homomorphic addition - no decryption needed!
        counter = FHE.add(counter, amount);

        // Update permission for the new ciphertext
        FHE.allowThis(counter);

        isRevealed = false;
        revealRequested = false;
        emit CounterIncremented(msg.sender);
    }

    // v0.9+: Mark counter as publicly decryptable
    function requestReveal() external {
        require(!revealRequested, "Already requested");
        revealRequested = true;
        counter = FHE.makePubliclyDecryptable(counter);
        bytes32 handle = FHE.toBytes32(counter);
        emit RevealRequested(handle);
    }

    // v0.9+: Client submits decrypted value + proof
    function resolveReveal(
        bytes memory cleartexts,
        bytes memory decryptionProof
    ) external {
        require(revealRequested, "Reveal not requested");
        bytes32[] memory handlesList = new bytes32[](1);
        handlesList[0] = FHE.toBytes32(counter);
        FHE.checkSignatures(handlesList, cleartexts, decryptionProof);
        revealedCount = abi.decode(cleartexts, (uint32));
        isRevealed = true;
    }
}
1

State Visibility

uint8 public counteuint32 private counterThe encrypted version cannot be read by anyone on-chain.
2

Input Handling

Instead of plain uint8 parameters, we accept externalEuint32 + proof bytesThe client encrypts values before sending.
3

Operations

count += 1FHE.add(counter, amount)Same logic, but operating on ciphertexts.
4

Permissions

After each mutation, call FHE.allowThis(counter)So the contract can later request decryption.
5

Reading Values

Instead of a simple getter, use v0.9+ self-relaying flow:makePubliclyDecryptable → off-chain publicDecrypt → submit proof on-chain with checkSignatures()
ImportsThe FHE library contains all encrypted operations. euint32 is a 32-bit encrypted integer (stored as a bytes32 handle on-chain). externalEuint32 is for client-supplied encrypted data.ConstructorWe initialize with FHE.asEuint32(0) and FHE.allowThis(counter) so the contract can use that value later.increment() — Three Steps
  1. Turn the client’s encrypted input into something the contract can use: FHE.fromExternal(encryptedAmount, proof)
  2. Add it to the counter: FHE.add(counter, amount) — returns a new handle
  3. Call FHE.allowThis(counter) on the new value
If you skip step 3, the next call will fail because the new handle has no permissions!
requestReveal() and resolveReveal()These implement the v0.9+ self-relaying pattern:
  • requestReveal() makes the counter eligible for public decryption
  • Frontend calls publicDecrypt() in the Relayer SDK
  • resolveReveal() verifies the proof and stores the plaintext result
Skipping allowThis after an FHE opAny time you create or overwrite an encrypted value, you need FHE.allowThis() on that new value.
Using external types without verifyingValues from the client aren’t trusted until you run them through FHE.fromExternal(handle, proof).
Assuming getters return numbersA function that returns euint32 returns a handle, not plaintext. The client must decrypt off-chain.
Revealing out of orderFor v0.9 you must call requestReveal first, then the client decrypts and calls resolveReveal.

Lesson 2.2: Contract Patterns & Architecture

Duration: 45 minutes
  • Analyze the SimpleVoting.sol contract line by line
  • Identify the session-based pattern for managing FHE state
  • Understand the encrypted input → compute → controlled decrypt lifecycle
  • Learn best practices for FHEVM contract architecture
contract SimpleVoting_uint32 is ZamaEthereumConfig {
    struct Session {
        address creator;
        uint256 endTime;
        euint32 yesVotes;    // Encrypted tally
        euint32 noVotes;     // Encrypted tally
        bool resolved;
        uint32 revealedYes;
        uint32 revealedNo;
        bool revealRequested;
    }

    Session[] public sessions;
    mapping(uint256 => mapping(address => bool)) public hasVoted;

    function createSession(uint256 durationSeconds) external {
        // Creates a new voting session with encrypted zero tallies
    }

    function vote(
        uint256 sessionId,
        externalEuint32 encryptedVote,
        bytes calldata proof
    ) external {
        // Casts an encrypted vote and updates tallies homomorphically
    }

    function requestTallyReveal(uint256 sessionId) external {
        // v0.9+: Calls makePubliclyDecryptable and emits handles
    }

    function resolveTallyCallback(
        uint256 sessionId,
        bytes memory cleartexts,
        bytes memory decryptionProof
    ) external {
        // v0.9+: Verifies proof and stores revealed results
    }
}

Pattern 1: Session-Based State

Group related encrypted values into structs with lifecycle (create → interact → resolve)

Pattern 2: Encrypted Accumulator

Use FHE.add() to build running totals without revealing individual contributions

Pattern 3: FHE.select for Conditionals

Replace if/else on encrypted conditions with select() to choose between encrypted values

Pattern 4: Self-Relaying Reveal (v0.9+)

makePubliclyDecryptable → client decrypts off-chain → submit proof → checkSignatures

Pattern 5: Access-Controlled Decryption

Use require(msg.sender == owner) before allowing decryption requests
Best Practices
Always initialize encrypted state with FHE.asEuintX(0)
Call FHE.allowThis() after every encrypted state mutation
Use events to track encrypted operations (events are public)
Keep decryption requests gated behind access control

Lesson 2.3: Testing & Deployment

Duration: 50 minutes
  • Write Hardhat tests for FHEVM contracts using mock mode
  • Understand the test lifecycle for encrypted operations
  • Deploy an EncryptedCounter to Sepolia testnet
  • Verify deployment and interact with the contract on-chain
Mock ModeFHEVM contracts can be tested locally using mock mode, which simulates FHE operations without actual encryption. This is much faster than testing on a live network.
import { expect } from "chai";
import { ethers } from "hardhat";

describe("EncryptedCounter", function () {
  let counter: any;
  let owner: any;

  beforeEach(async function () {
    [owner] = await ethers.getSigners();
    const Counter = await ethers.getContractFactory("EncryptedCounter");
    counter = await Counter.deploy();
    await counter.waitForDeployment();
  });

  it("should deploy with initial count of 0", async function () {
    const tx = await counter.requestReveal();
    await tx.wait();
    
    // In mock mode, simulate the self-relaying callback
    const cleartexts = ethers.AbiCoder.defaultAbiCoder().encode(["uint32"], [0]);
    const proof = "0x"; // Mock proof
    
    await counter.resolveReveal(cleartexts, proof);
    expect(await counter.revealedCount()).to.equal(0);
    expect(await counter.isRevealed()).to.equal(true);
  });

  it("should increment the counter", async function () {
    // In mock mode, encrypt a value of 1
    const encryptedAmount = ethers.zeroPadValue("0x01", 32);
    const proof = "0x";

    await counter.increment(encryptedAmount, proof);

    // Request reveal and simulate off-chain decrypt
    await (await counter.requestReveal()).wait();
    const cleartexts = ethers.AbiCoder.defaultAbiCoder().encode(["uint32"], [1]);
    await counter.resolveReveal(cleartexts, "0x");
    
    expect(await counter.revealedCount()).to.equal(1);
  });
});
Deployment Script
// scripts/deploy.ts
import { ethers } from "hardhat";

async function main() {
  console.log("Deploying EncryptedCounter...");

  const Counter = await ethers.getContractFactory("EncryptedCounter");
  const counter = await Counter.deploy();
  await counter.waitForDeployment();

  const address = await counter.getAddress();
  console.log("EncryptedCounter deployed to:", address);

  console.log("Verify with:");
  console.log(`npx hardhat verify --network sepolia ${address}`);
}

main().catch(console.error);
Deploy to Sepolia
npx hardhat run scripts/deploy.ts --network sepolia
Make sure your wallet has Sepolia ETH before deploying. Get test ETH from Sepolia Faucet.

Homework: Build a Confidential Counter

Estimated Time: 5 hours Objective: Build, test, and deploy a fully functional EncryptedCounter contract with increment, decrement, and reset capabilities.

Requirements

Write an EncryptedCounter.sol (v0.9+) with:
  • increment(euint32) with encrypted input
  • decrement(euint32) with underflow protection using FHE.select()
  • reset() function
  • requestReveal() using makePubliclyDecryptable
  • resolveReveal() with checkSignatures
Write at least 3 tests:
  1. Initial count is 0
  2. Increment works correctly
  3. Decrement does not underflow below 0
Deploy your contract to Sepolia testnet and submit:
  • Contract address
  • Transaction hash
Write a short (200-word) explanation of:
  • Your contract design decisions
  • Any challenges you faced
  • How you handled underflow protection

Submission

1

Create folder

Create homework-2-[yourname] folder
2

Include files

  • contracts/EncryptedCounter.sol
  • test/EncryptedCounter.test.ts
  • deployment.md with contract address and TX hash
  • writeup.md with design explanation
3

Submit

Zip and submit — [Submission portal coming soon]

Next Steps

Continue to Week 3

Now that you can write and test FHEVM contracts, move on to Week 3 to build a full-stack confidential dApp with React frontend integration.

Build docs developers (and LLMs) love