Skip to main content
CofheClient is the Foundry plugin’s in-Solidity SDK shim. One client per “user” in your scenario. The client carries a private key and produces encrypted inputs and signed ACPs as if it were that user’s frontend SDK, with no JS bridge required.

Creating and connecting

Spin up a client from inside CofheTest with createCofheClient(), then bind it to an address with connect(pkey):
After connect, the client knows which address to sign as. All createExternalEuintN and ACP_* calls use that account automatically; there’s no account argument to pass. To act onchain as that user, prank with client.account():
A mismatch between the prank address and the client that produced the input will fail the ZK-verifier signature check. The input was signed for bob.account(), not whoever you pranked. Always match the client to the prank.

Encrypting inputs

The client mirrors the JS SDK’s encryptInputs API, one method per encrypted Solidity type. Each takes the plaintext plus the contract that will consume it, and returns the handle and its proof as a pair: The second argument is the consuming contract. The verifier binds it into the signature, so a proof made for one contract will not verify in another.
For several values under one signature, use createEncryptedInputsBatch.

Decrypting

The plugin exposes both decryption flows the SDK supports:

Public-decrypt 3-step flow with decryptForTx_withoutACP

Mirrors the production flow when a contract calls FHE.publishDecryptResult:
The same shape runs unmodified against real CoFHE on testnet. The mock signature is produced by the same MockThresholdNetworkSigner that FHE.verifyDecryptResult accepts.

ACP-based unseal with decryptForView

decryptForView reverts when the caller isn’t on the ACL. To assert the deny path (e.g. “Alice should NOT be able to decrypt Bob’s value”), drop down to the mock directly. See Testing: Deny path.

Access Control Permissions

The client signs EIP-712 ACPs against the ACL’s domain. Two flavors:

Self-ACP (most common)

ACP_createSelf builds the EIP-712 typed-data, derives a sealing key from the connected account, and signs, all in one call.

Shared ACPs (issuer to recipient)

ACP_importShared reverts unless the calling client’s account() matches export.recipient, preventing Alice from importing an ACP shared to someone else.

Common pitfalls

vm.prank(bob.account()) while the input came from alice.createExternalEuintN(...) fails ZK verification. The input was signed for Alice’s address, not Bob’s. Match the client to the prank.
euint32.unwrap(counter.count()) returns the current handle. Storing it in a local then asserting after a write reads the old handle.
Re-fetch after each state change.
The pkey passed to connect must derive the address used as acp.issuer. If you call bob.ACP_createSelf() after bob.connect(0xB0B), the issuer is vm.addr(0xB0B). Trying to forge an issuer mismatch will fail signature verification.
Useful default: most tests want a hard failure when the caller isn’t permitted. To assert “Alice cannot decrypt”, call the mock’s querySealOutput directly:

Next steps

  • Testing: full test-writing patterns.
  • CofheTest: the test base contract that creates clients.