Scaffold-ETH 2 (Now With CoFHE)
The CoFHE Scaffold-ETH 2 template adds support for Fully Homomorphic Encryption (FHE) operations to the standard Scaffold-ETH 2 template.
To get up and testing, clone and open the repo, then:
- Start up the local hardhat node (you will see the mocks getting deployed, explained below)
yarn chain- Deploy
FHECounter.sol
yarn deploy:local- Start the NextJS webapp
yarn start- Open the dApp, and start exploring the FHECounter.
-
Hardhat
@fhenixprotocol/cofhe-contracts- Package containingFHE.sol.FHE.solis a library that exposes FHE arithmetic operations likeFHE.addandFHE.mulalong with access control functions.@cofhe/mock-contracts- The CoFHE coprocessor exists off-chain.@cofhe/mock-contractsare a fully on-chain drop-in replacement for the off-chain components. These mocks allow better developer and testing experience when working with FHE. Is transparently used as a dependency of@cofhe/hardhat-plugin@cofhe/hardhat-plugin- A hardhat plugin responsible for deploying the mock contracts on the hardhat network and during tests. Also exposes testing utility functions inhre.cofhesdk.___.@cofhe/sdk- Primary connection to the CoFHE coprocessor. Exposes functions likeencryptInputs(for sealing) anddecryptHandle(for unsealing). Manages access permits. Automatically plays nicely with the mock environment.
-
Nextjs
@cofhe/sdk- Primary connection to the CoFHE coprocessor. Exposes functions likeencryptInputs(for sealing) anddecryptHandle(for unsealing). Manages access permits. Automatically plays nicely with the mock environment.
-
import 'cofhe-hardhat-plugin' module.exports = { solidity: '0.8.25', evmVersion: 'cancun', // ... other config }
-
{ "compilerOptions": { "target": "es2020", "module": "Node16", "moduleResolution": "Node16" } } -
Multicall3 Deployment: The Multicall3 contract is deployed on the hardhat node to support the
useReadContractshook from viem. This allows efficient batch reading of contract data in the mock environment.
The FHECounter.sol contract demonstrates the use of Fully Homomorphic Encryption (FHE) to perform encrypted arithmetic operations. The counter value is stored in encrypted form, allowing for private increments, decrements, and value updates.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
contract FHECounter {
/// @notice The encrypted counter value
euint32 public count;
/// @notice A constant encrypted value of 1 used for increments/decrements (gas saving)
euint32 private ONE;
constructor() {
ONE = FHE.asEuint32(1);
count = FHE.asEuint32(0);
// Allows anyone to read the initial encrypted value (0)
// Also allows anyone to perform an operation USING the initial value
FHE.allowGlobal(count);
// Allows this contract to perform operations using the constant ONE
FHE.allowThis(ONE);
}
function increment() public {
// Performs an encrypted addition of count and ONE
count = FHE.add(count, ONE);
// Only this contract and the sender can read the new value
FHE.allowThis(count);
FHE.allowSender(count);
}
function decrement() public {
count = FHE.sub(count, ONE);
FHE.allowThis(count);
FHE.allowSender(count);
}
function set(InEuint32 memory value) public {
count = FHE.asEuint32(value);
FHE.allowThis(count);
FHE.allowSender(count);
}
}Key concepts in FHE contract development:
-
Encrypted Types:
- Use
euint32,ebool, etc. for encrypted values - These types support FHE operations while keeping values private
- Use
-
FHE Operations:
FHE.add(a, b): Add two encrypted valuesFHE.sub(a, b): Subtract encrypted valuesFHE.mul(a, b): Multiply encrypted valuesFHE.div(a, b): Divide encrypted values- See
FHE.solfor the full list of available operations
-
Access Control:
FHE.allow(value, address): Allowaddressto read the valueFHE.allowThis(value): Allow the contract to read the valueFHE.allowSender(value): Allow the transaction sender to read the valueFHE.allowGlobal(value): Allow anyone to read the value- Access control must be explicitly set after each operation that modifies an encrypted value
The FHECounter.test.ts file demonstrates testing FHE contracts using the mock environment. Before using cofhesdkClient.encryptInput to prepare input variables, or cofhesdkClient.decryptHandle to read encrypted data, cofhe must be initialized and connected. In a hardhat environment there is an exposed utility function:
const [bob] = await hre.ethers.getSigners()
// `hre.cofhesdk.createBatteriesIncludedCofhesdkClient` is used to initialize FHE with a Hardhat signer
// Initialization is required before any `encrypt` or `decrypt` operations can be performed
// `createBatteriesIncludedCofhesdkClient` is a helper function that initializes FHE with a Hardhat signer
// Returns a `Promise<CofhesdkClient>` type.
const client = await hre.cofhesdk.createBatteriesIncludedCofhesdkClient(bob);To verify the value of an encrypted variable, we can use:
// Get the encrypted count variable
const count = await counter.count();
// `hre.cofhesdk.mocks.expectPlaintext` is used to verify that the encrypted value is 0
// This uses the encrypted variable `count` and retrieves the plaintext value from the on-chain mock contracts
// This kind of test can only be done in a mock environment where the plaintext value is known
await hre.cofhesdk.mocks.expectPlaintext(count, 0n);To read the encrypted variable directly, we can use cofhesdkClient.decryptHandle:
const count = await counter.count();
// `decryptHandle` is used to unseal the encrypted value
// the client must be initialized and connected before `unseal` can be called
const unsealedResult = await client.decryptHandle(count, FheTypes.Uint32).decrypt();To encrypt a variable for use as an InEuint* we can use cofhesdkClient.encryptInputs:
// `encryptInputs` is used to encrypt the value
// the client must be initialized and connected before `encryptInputs` can be called
const encryptResult = await client.encryptInputs([Encryptable.uint32(5n)]).encrypt();
const [encryptedInput] = await hre.cofhesdk.expectResultSuccess(encryptResult);
await hre.cofhesdk.mocks.expectPlaintext(encryptedInput.ctHash, 5n);
await counter.connect(bob).set(encryptedInput);
const count = await counter.count();
await hre.cofhesdk.mocks.expectPlaintext(count, 5n);When global logging is needed we can use the utilities:
hre.cofhesdk.mocks.enableLogs()
hre.cofhesdk.mocks.disableLogs()or we can use targeted logging like this:
await hre.cofhesdk.mocks.withLogs('counter.increment()', async () => {
await counter.connect(bob).increment()
})which will result in logs like this:
ββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββ
β [COFHE-MOCKS] β "counter.increment()" logs:
ββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββ
β FHE.add | euint32(4473..3424)[0] + euint32(1157..3648)[1] => euint32(1106..1872)[1]
β FHE.allowThis | euint32(1106..1872)[1] -> 0x663f3ad617193148711d28f5334ee4ed07016602
β FHE.allow | euint32(1106..1872)[1] -> 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
euint32(4473..3424)[0] represents an encrypted variable in the format type(ct..hash)[plaintext]
The frontend initialization begins in ScaffoldEthAppWithProviders.tsx where the useInitializeCofhe hook is called:
/**
* CoFHE Initialization
*
* The CoFHE SDK client is initialized in two steps.
* The client is constructed synchronously, with `supportedChains` provided at construction time.
* The useInitializeCofhe hook then makes sure the CoFHE SDK client is connected to the current wallet and is ready to function.
* It performs the following key functions:
* - Connects the CoFHE SDK client to the current provider and signer
* - Initializes the FHE keys
* - Configures the wallet client for encrypted operations
* - Handles initialization errors with user notifications
*
* This hook is essential for enabling FHE (Fully Homomorphic Encryption) operations
* throughout the application. It automatically refreshes when the connected wallet
* or chain changes to maintain proper configuration.
*/
useInitializeCofhe()This hook handles the complete setup of the CoFHE system, including environment detection, wallet client configuration, and permit management initialization. It runs automatically when the wallet or chain changes, ensuring the FHE system stays properly configured.
The CofhePortal component provides a dropdown interface for managing CoFHE permits and viewing system status. It's integrated into the Header component as a shield icon button:
/**
* CoFHE Portal Integration
*
* The CofhePortal component is integrated into the header to provide easy access to
* CoFHE permit management functionality. It appears as a shield icon button that opens
* a dropdown menu containing:
* - System initialization status
* - Active permit information
* - Permit management controls
*
* This placement ensures the portal is always accessible while using the application,
* allowing users to manage their permits and monitor system status from any page.
*/
<CofhePortal />The portal displays:
- Connection Status: Shows whether CoFHE is connected, the connected account, and current network
- Active Permit: Displays details about the currently active permit including name, ID, issuer, and expiration
- Permit Management: Allows users to create new permits, switch between existing permits, and delete unused permits
The FHECounterComponent demonstrates how to interact with FHE-enabled smart contracts in a React application:
/**
* FHECounterComponent - A demonstration of Fully Homomorphic Encryption (FHE) in a web application
*
* This component showcases how to:
* 1. Read encrypted values from a smart contract
* 2. Display encrypted values using a specialized component
* 3. Encrypt user input before sending to the blockchain
* 4. Interact with FHE-enabled smart contracts
*
* The counter value is stored as an encrypted uint32 on the blockchain,
* meaning the actual value is never revealed on-chain.
*/- Reading Encrypted Values: Uses
useScaffoldReadContractto read the encrypted counter value from the smart contract - Displaying Encrypted Data: Uses the
EncryptedValuecomponent to handle decryption and display - Encrypting User Input: Demonstrates the process of encrypting user input before sending to the blockchain
- Contract Interactions: Shows how to call increment, decrement, and set functions on the FHE contract
/**
* SetCounterRow Component
*
* Demonstrates the process of encrypting user input before sending it to the blockchain:
* 1. User enters a number in the input field
* 2. When "Set" is clicked, the number is encrypted using cofhe SDK
* 3. The encrypted value is then sent to the smart contract
*
* This ensures the actual value is never exposed on the blockchain,
* maintaining privacy while still allowing computations.
*/
const encryptedResult = await cofhesdkClient.encryptInputs([encryptable]).encrypt();
// encryptedResult is a result object with success status and data/errorThe CofhePermitModal allows users to generate cryptographic permits for accessing encrypted data. This modal automatically opens when a user attempts to decrypt a value in the EncryptedValue component without a valid permit:
/**
* CoFHE Permit Generation Modal
*
* This modal allows users to generate cryptographic permits for accessing encrypted data in the CoFHE system.
* Permits are required because they provide a secure way to verify identity and control access to sensitive
* encrypted data without revealing the underlying data itself.
*
* The modal provides the following options:
* - Name: An optional identifier for the permit (max 24 chars)
* - Expiration: How long the permit remains valid (1 day, 1 week (default), or 1 month)
* - Recipient: (Currently unsupported) Option to share the permit with another address
*
* When generated, the permit requires a wallet signature (EIP712) to verify ownership.
* This signature serves as proof that the user controls the wallet address associated with the permit.
*/The modal opens in two scenarios:
- When clicking "Generate Permit" in the CoFHE Portal
- When attempting to decrypt an encrypted value without a valid permit
The EncryptedValueCard provides components for displaying and interacting with encrypted values:
EncryptedValue Component:
- Displays encrypted values with appropriate UI states (encrypted, decrypting, decrypted, error)
- Handles permit validation and automatically opens the permit modal when needed
- Manages the decryption process using the
useDecryptValuehook - Shows different visual states based on the decryption status
EncryptedZone Component:
- Provides a visual wrapper with gradient borders to indicate encrypted content
- Includes a shield icon to clearly mark encrypted data areas
The useCofhe.ts file provides comprehensive React hooks for FHE operations:
Initialization Hooks:
// Hook to initialize cofhe with the connected wallet and chain configuration
// Handles initialization errors and displays toast notifications on success or error
// Refreshes when connected wallet or chain changes
useInitializeCofhe()
// Hook to check if cofhe is connected (provider, and signer)
// This is used to determine if the user is ready to use the FHE library
// FHE based interactions (encrypt / decrypt) should be disabled until this is true
useCofheConnected()
// Hook to get the current account connected to cofhe
useCofheAccount()Status Hooks:
// Hook to get the complete status of cofhe
// Returns Object containing chainId, account, and initialization status
// Refreshes when any of the underlying values change
useCofheStatus()
// Hook to check if the currently connected chain is supported by the application
// Returns boolean indicating if the current chain is in the target networks list
// Refreshes when chainId changes
useIsConnectedChainSupported()Permit Management Hooks:
// Hook to create a new permit
// Returns Async function to create a permit with optional options
// Refreshes when chainId, account, or initialization status changes
useCofheCreatePermit()
// Hook to remove a permit
// Returns Async function to remove a permit by its hash
// Refreshes when chainId, account, or initialization status changes
useCofheRemovePermit()
// Hook to select the active permit
// Returns Async function to set the active permit by its hash
// Refreshes when chainId, account, or initialization status changes
useCofheSetActivePermit()
// Hook to get the active permit object
// Returns The active permit object or null if not found/valid
// Refreshes when active permit hash changes
useCofheActivePermit()
// Hook to check if the active permit is valid
// Returns boolean indicating if the active permit is valid
// Refreshes when permit changes
useCofheIsActivePermitValid()
// Hook to get all permit objects for the current chain and account
// Returns Array of permit objects
// Refreshes when permit hashes change
useCofheAllPermits()The useDecrypt.ts file provides utilities for handling encrypted value decryption:
/**
* Hook to decrypt a value using cofhe
* @param fheType - The type of the value to decrypt
* @param ctHash - The hash of the encrypted value
* @returns Object containing a function to decrypt the value and the result of the decryption
*/
useDecryptValue(fheType, ctHash)DecryptionResult States:
"no-data": No encrypted value provided"encrypted": Value is encrypted and ready for decryption"pending": Decryption is in progress"success": Decryption completed successfully with the decrypted value"error": Decryption failed with error message
The hook automatically handles:
- Initialization status checking
- Account validation
- Zero value handling (returns appropriate default values)
- Error handling and state management
- Automatic reset when the encrypted value changes
π§ͺ An open-source, up-to-date toolkit for building decentralized applications (dapps) on the Ethereum blockchain. It's designed to make it easier for developers to create and deploy smart contracts and build user interfaces that interact with those contracts.
βοΈ Built using NextJS, RainbowKit, Hardhat, Wagmi, Viem, and Typescript.
- β Contract Hot Reload: Your frontend auto-adapts to your smart contract as you edit it.
- πͺ Custom hooks: Collection of React hooks wrapper around wagmi to simplify interactions with smart contracts with typescript autocompletion.
- π§± Components: Collection of common web3 components to quickly build your frontend.
- π₯ Burner Wallet & Local Faucet: Quickly test your application with a burner wallet and local faucet.
- π Integration with Wallet Providers: Connect to different wallet providers and interact with the Ethereum network.
Before you begin, you need to install the following tools:
- Node (>= v20.18.3)
- Yarn (v1 or v2+)
- Git
To get started with Scaffold-ETH 2, follow the steps below:
- Install dependencies if it was skipped in CLI:
cd my-dapp-example
yarn install
- Run a local network in the first terminal:
yarn chain
This command starts a local Ethereum network using Hardhat. The network runs on your local machine and can be used for testing and development. You can customize the network configuration in packages/hardhat/hardhat.config.ts.
- On a second terminal, deploy the test contract:
yarn deploy
This command deploys a test smart contract to the local network. The contract is located in packages/hardhat/contracts and can be modified to suit your needs. The yarn deploy command uses the deploy script located in packages/hardhat/deploy to deploy the contract to the network. You can also customize the deploy script.
- On a third terminal, start your NextJS app:
yarn start
Visit your app on: http://localhost:3000. You can interact with your smart contract using the Debug Contracts page. You can tweak the app config in packages/nextjs/scaffold.config.ts.
Run smart contract test with yarn hardhat:test
- Edit your smart contracts in
packages/hardhat/contracts - Edit your frontend homepage at
packages/nextjs/app/page.tsx. For guidance on routing and configuring pages/layouts checkout the Next.js documentation. - Edit your deployment scripts in
packages/hardhat/deploy
Visit our docs to learn how to start building with Scaffold-ETH 2.
To know more about its features, check out our website.
We welcome contributions to Scaffold-ETH 2!
Please see CONTRIBUTING.MD for more information and guidelines for contributing to Scaffold-ETH 2.
