Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Foundry Plugin

@cofhe/foundry-plugin provides two contracts for writing CoFHE tests with Foundry:

  • CofheTest — abstract base contract. Inherits forge-std/Test. Exposes deployMocks(), createCofheClient(), getPlaintext(), and expectPlaintext().
  • CofheClient — SDK-like client. Manages a private key / account pair and exposes encrypt, decrypt, and ACP helpers.

Installation

npm / pnpm

npm
npm install --save-dev @cofhe/foundry-plugin

Then add the remappings to your foundry.toml:

foundry.toml
[profile.default]
src = "contracts"
out = "out"
libs = ["node_modules", "lib"]
 
remappings = [
  "@cofhe/foundry-plugin/=node_modules/@cofhe/foundry-plugin/",
  "@cofhe/mock-contracts/=node_modules/@cofhe/mock-contracts/",
  "@fhenixprotocol/cofhe-contracts/=node_modules/@fhenixprotocol/cofhe-contracts/",
  "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/",
  "forge-std/=node_modules/forge-std/src/",
]

Git submodule

forge install fhenixprotocol/foundry-plugin          # @cofhe/foundry-plugin
forge install fhenixprotocol/cofhe-mock-contracts    # @cofhe/mock-contracts

Forge will install these under lib/. Add the equivalent remappings pointing to lib/ instead of node_modules/.

Quick start

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
 
import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol";
import { CofheClient } from "@cofhe/foundry-plugin/contracts/CofheClient.sol";
import "@fhenixprotocol/cofhe-contracts/FHE.sol";
 
contract CounterTest is CofheTest {
    Counter private counter;
    CofheClient private cofheClient;
 
    uint256 constant USER_PKEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
 
    function setUp() public {
        deployMocks();                      // deploy full mock stack
        cofheClient = createCofheClient();  // create SDK-like client
        cofheClient.connect(USER_PKEY);     // set account/signer
        counter = new Counter();
    }
 
    function test_increment() public {
        // The last argument binds the input to the contract that will consume it.
        (externalEuint32 hash, bytes memory proof) = cofheClient.createExternalEuint32(5, address(counter));
        externalEuint32[] memory hashes = new externalEuint32[](1);
        hashes[0] = hash;
 
        vm.prank(cofheClient.account());
        counter.setNumberBatch(hashes, proof);
 
        expectPlaintext(counter.numberHash(), 5);
    }
}

CofheTest API

CofheTest is abstract and provides mock infrastructure. Inherit it in your test contract.

FunctionDescription
deployMocks()Deploys the complete CoFHE mock stack: MockTaskManager, MockACL, MockZkVerifier, MockZkVerifierSigner, MockThresholdNetwork, MockThresholdNetworkSigner. Mirrors the Hardhat plugin deployment order.
createCofheClient()Returns a new CofheClient instance. Call connect(pkey) on the result before use.
enableLogs()Turns on plaintext operation logs from MockTaskManager.
disableLogs()Turns off plaintext operation logs.
getPlaintext(bytes32 ctHash)Returns the stored plaintext for a ciphertext hash as uint256. Reverts if the hash is not in mock storage.
getPlaintext(euint32 eValue)Typed overloads — returns the correct Solidity type (bool, uint8uint128, address) for each FHE type. internal visibility only.
expectPlaintext(bytes32, uint256)Asserts ctHash exists in mock storage and equals value.
expectPlaintext(bytes32, uint256, string)Same with a custom failure message.
expectPlaintext(euint32, uint32)Typed overloads for each FHE type.
expectPlaintext(euint32, uint32, string)Typed overloads with failure message.

CofheClient API

CofheClient is a standalone contract deployed by createCofheClient(). It wraps mock FHE operations behind the same conceptual interface as the JS SDK.

Setup

FunctionDescription
connect(uint256 pkey)Stores the private key, derives the account address, and marks the client as ready. Must be called before all other functions.
account()Returns the address derived from the connected private key.

Encryption

CofheClient.createEncryptedInputsBatch is the root of every encryption helper on the client — a whole array of ciphertexts authenticated by one shared signature, verified via ITaskManager.batchVerifyInputs (matching FhenixProtocol/cofhe-contracts#78). createExternalEbool...createExternalEaddress are thin single-item wrappers over a batch of size one, not a separate signing scheme — every hash they return is only valid when consumed via the batch path (FHE.asEuint32s([hash], signature), not the legacy single-item FHE.asEuint32(hash, proof)).

Every helper takes the consuming contract as a required last argument — the verifier binds that address into the signature, matching FhenixProtocol/cofhe-contracts#77. There is no client-level setter; passing it per call lets one test encrypt for several contracts without tracking hidden state. Passing address(0) reverts.

FunctionReturns
createExternalEbool(bool, address consumingContract)(externalEbool, bytes memory)
createExternalEuint8(uint8, address consumingContract)(externalEuint8, bytes memory)
createExternalEuint16(uint16, address consumingContract)(externalEuint16, bytes memory)
createExternalEuint32(uint32, address consumingContract)(externalEuint32, bytes memory)
createExternalEuint64(uint64, address consumingContract)(externalEuint64, bytes memory)
createExternalEuint128(uint128, address consumingContract)(externalEuint128, bytes memory)
createExternalEaddress(address, address consumingContract)(externalEaddress, bytes memory)

Example — note the contract's entry point must accept an array (even for a single value) plus the shared signature:

function testSetValue(uint32 n) public {
    (externalEuint32 hash, bytes memory signature) = cofheClient.createExternalEuint32(n, address(myContract));
    externalEuint32[] memory hashes = new externalEuint32[](1);
    hashes[0] = hash;
 
    vm.prank(cofheClient.account());
    myContract.setValueBatch(hashes, signature);
 
    expectPlaintext(myContract.getValue(), n);
}

Batch variants

For batches of more than one ciphertext, use createEncryptedInputsBatch directly (mixed utypes allowed) or the typed createEuint32sBatch convenience wrapper.

FunctionReturns
createEncryptedInputsBatch(uint8[], uint256[], address consumingContract)(UnsignedEncryptedInput[], bytes memory) — mixed utypes allowed
createEuint32sBatch(uint32[], address consumingContract)(externalEuint32[] memory, bytes memory)

Example:

function testSetValueBatch(uint32[] memory values) public {
    (externalEuint32[] memory hashes, bytes memory signature) = cofheClient.createEuint32sBatch(values, address(myContract));
    vm.prank(cofheClient.account());
    myContract.setValueBatch(hashes, signature);
}

This is the canonical signing path, backed by MockZkVerifierSigner.zkVerifyBatchSign — this repo's own tooling routes all encryption (including batches of size 1) through it, rather than maintaining a second independent signing implementation.

Decryption

FunctionReturnsDescription
decryptForTx_withoutACP(bytes32 ctHash)(bytes32, uint256, bytes)Decrypts a globally-allowed ciphertext. Returns (ctHash, plaintext, signature).
decryptForTx_withACP(bytes32 ctHash, Permission permission)(bytes32, uint256, bytes)Decrypts using an ACP. Same return shape.
decryptForView(bytes32 ctHash, Permission permission)uint256Seals and unseals with the ACP's sealing key for off-chain reading.

ACPs

FunctionReturnsDescription
acp_createSelf()PermissionCreates a self-ACP signed by the connected account.
acp_createShared(address recipient)PermissionCreates the issuer half of a shared ACP.
acp_exportShared(Permission)SharedACPExportStrips private fields for safe transmission.
acp_importShared(SharedACPExport)PermissionReconstructs a Permission as the recipient, adding the sealing key and recipient signature.

Hardhat 3 Solidity tests

@cofhe/foundry-plugin ships a remappings.txt that Hardhat 3 automatically discovers. Add the package as a devDependency in your hardhat-3-plugin-test project and configure the Solidity test path:

hardhat.config.ts
export default defineConfig({
  plugins: [cofhePlugin, hardhatViem, hardhatNodeTestRunner],
  solidity: '0.8.29',
  paths: {
    tests: {
      nodejs: './test',
      solidity: './test/solidity',
    },
  },
});

Then import CofheTest and CofheClient exactly as in standard Foundry tests — the @cofhe/foundry-plugin/ prefix resolves via the bundled remappings:

import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol";
import { CofheClient } from "@cofhe/foundry-plugin/contracts/CofheClient.sol";

Run with:

hardhat test solidity