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.sol
is a library that exposes FHE arithmetic operations likeFHE.add
andFHE.mul
along with access control functions.@fhenixprotocol/cofhe-mock-contracts
- The CoFHE coprocessor exists off-chain.cofhe-mock-contracts
are a fully on-chain drop in replacement for the off-chain components. These mocks allow better developer and testing experience when working with FHE.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.cofhe.___
.cofhejs
- Primary connection to the CoFHE coprocessor. Exposes functions likeencrypt
andunseal
. Manages access permits. Automatically plays nicely with the mock environment.
-
Nextjs
cofhejs
- Primary connection to the CoFHE coprocessor. Exposes functions likeencrypt
andunseal
. 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
useReadContracts
hook 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.sol
for the full list of available operations
-
Access Control:
FHE.allow(value, address)
: Allowaddress
to 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 cofhejs.encrypt
to prepare input variables, or cofhejs.unseal
to read encrypted data, cofhejs must be initialized. In a hardhat environment there is an exposed utility function:
const [bob] = await hre.ethers.getSigners()
// ^? HardhatEthersSigner
// `hre.cofhe.initializeWithHardhatSigner` is used to initialize FHE with a Hardhat signer
// Initialization is required before any `cofhejs.unseal` or `cofhejs.encrypt` operations can be performed
// `initializeWithHardhatSigner` is a helper function that initializes FHE with a Hardhat signer
// It returns a `Promise<Result<>>` type.
// The `Result<T>` type looks like this:
// {
// success: boolean,
// data: T (Permit | undefined in the case of initializeWithHardhatSigner),
// error: CofhejsError | null,
// }
const initializeResult = await hre.cofhe.initializeWithHardhatSigner(bob)
// `hre.cofhe.expectResultSuccess` is used to verify that the `Result` is successful (success: true)
// If the `Result` is not successful, the test will fail
await hre.cofhe.expectResultSuccess(initializeResult)
To verify the value of an encrypted variable, we can use:
// Get the encrypted count variable
const count = await counter.count()
// `hre.cofhe.mocks.expectPlaintext` is used to verify that the encrypted count 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.cofhe.mocks.expectPlaintext(count, 0n)
To read the encrypted variable directly, we can use cofhejs.unseal
:
const count = await counter.count()
// `cofhejs.unseal` is used to unseal the encrypted value
// cofhejs must be initialized before `unseal` can be called
// `FheType.Uint32` tells `unseal` the type of the encrypted variable
const unsealedResult = await cofhejs.unseal(count, FheTypes.Uint32)
To encrypt a variable for use as an InEuint*
we can use cofhejs.encrypt
:
// `cofhejs.encrypt` is used to encrypt the value
// cofhejs must be initialized before `encrypt` can be called
// This accepts an array of values to be encrypted, using the `Encryptable` type from `cofhejs`
const encryptResult = await cofhejs.encrypt([Encryptable.uint32(5n)] as const)
const [encryptedInput] = await hre.cofhe.expectResultSuccess(encryptResult)
await counter.connect(bob).set(encryptedInput)
// Check that the count was updated correctly
const count = await counter.count()
await hre.cofhe.mocks.expectPlaintext(count, 5n)
When global logging is needed we can use the utilities:
hre.cofhe.mocks.enableLogs()
hre.cofhe.mocks.disableLogs()
or we can use targeted logging like this:
await hre.cofhe.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 useInitializeCofhejs
hook is called:
/**
* CoFHE Initialization
*
* The useInitializeCofhejs hook initializes the CoFHE system with the connected wallet and chain configuration.
* It performs the following key functions:
* - Sets up the FHE environment based on the current network (MAINNET, TESTNET, or MOCK)
* - Initializes the FHE keys, provider, and signer
* - 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.
*/
useInitializeCofhejs()
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 CofhejsPortal
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 CofhejsPortal 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.
*/
<CofhejsPortal />
The portal displays:
- Initialization Status: Shows whether CoFHE is initialized, 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
useScaffoldReadContract
to read the encrypted counter value from the smart contract - Displaying Encrypted Data: Uses the
EncryptedValue
component 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 cofhejs
* 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 cofhejs.encrypt([Encryptable.uint32(input)])
// cofhejs.encrypt returns a result object with success status and data/error
The CofhejsPermitModal
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
useDecryptValue
hook - 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 useCofhejs.ts
file provides comprehensive React hooks for FHE operations:
Initialization Hooks:
// Hook to initialize cofhejs 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
useInitializeCofhejs()
// Hook to check if cofhejs is fully initialized (FHE keys, 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
useCofhejsInitialized()
// Hook to get the current account initialized in cofhejs
useCofhejsAccount()
Status Hooks:
// Hook to get the complete status of cofhejs
// Returns Object containing chainId, account, and initialization status
// Refreshes when any of the underlying values change
useCofhejsStatus()
// 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
useCofhejsCreatePermit()
// Hook to remove a permit
// Returns Async function to remove a permit by its hash
// Refreshes when chainId, account, or initialization status changes
useCofhejsRemovePermit()
// 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
useCofhejsSetActivePermit()
// Hook to get the active permit object
// Returns The active permit object or null if not found/valid
// Refreshes when active permit hash changes
useCofhejsActivePermit()
// Hook to check if the active permit is valid
// Returns boolean indicating if the active permit is valid
// Refreshes when permit changes
useCofhejsIsActivePermitValid()
// Hook to get all permit objects for the current chain and account
// Returns Array of permit objects
// Refreshes when permit hashes change
useCofhejsAllPermits()
The useDecrypt.ts
file provides utilities for handling encrypted value decryption:
/**
* Hook to decrypt a value using cofhejs
* @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.