Foundry Plugin
@cofhe/foundry-plugin provides two contracts for writing CoFHE tests with Foundry:
CofheTest— abstract base contract. Inheritsforge-std/Test. ExposesdeployMocks(),createCofheClient(),getPlaintext(), andexpectPlaintext().CofheClient— SDK-like client. Manages a private key / account pair and exposes encrypt, decrypt, and ACP helpers.
Installation
npm / pnpm
npm install --save-dev @cofhe/foundry-pluginThen add the remappings to your 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-contractsForge 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.
| Function | Description |
|---|---|
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, uint8…uint128, 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
| Function | Description |
|---|---|
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.
| Function | Returns |
|---|---|
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.
| Function | Returns |
|---|---|
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
| Function | Returns | Description |
|---|---|---|
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) | uint256 | Seals and unseals with the ACP's sealing key for off-chain reading. |
ACPs
| Function | Returns | Description |
|---|---|---|
acp_createSelf() | Permission | Creates a self-ACP signed by the connected account. |
acp_createShared(address recipient) | Permission | Creates the issuer half of a shared ACP. |
acp_exportShared(Permission) | SharedACPExport | Strips private fields for safe transmission. |
acp_importShared(SharedACPExport) | Permission | Reconstructs 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:
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