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 insideCofheTest with createCofheClient(), then bind it to an address with connect(pkey):
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():
Encrypting inputs
The client mirrors the JS SDK’sencryptInputs 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.
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:
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
Wrong client for the prank
Wrong client for the prank
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.Stale handle reads
Stale handle reads
euint32.unwrap(counter.count()) returns the current handle. Storing it in a local then asserting after a write reads the old handle.ACP issuer must derive from the connected key
ACP issuer must derive from the connected key
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.`decryptForView` reverts on deny
`decryptForView` reverts on deny
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: