@cofhe/foundry-plugin. Hardhat counterpart: Hardhat Plugin → Testing.
Skeleton
test/Counter.t.sol
Rules
1. Inherit CofheTest, not Test
CofheTest already inherits forge-std/Test and exposes the mock state vars (mockTaskManager, mockAcl, mockThresholdNetwork, …) you’ll occasionally reach into. Inheriting both is a redeclaration error.
2. One CofheClient per scenario address
Each user with their own permit/encrypted inputs gets their own client. Connect with a deterministic plaintext private key — don’t recycle real keys; these are visible in test output.
account to createInEuintN — the client is bound at connect.
3. vm.prank(client.account()) to act on-chain as that user
4. Assert with expectPlaintext whenever possible
decryptForView and needs no permit. Reserve the SDK path for tests where the SDK behavior itself is under test.
5. Test the public-decrypt 3-step flow with decryptForTx_withoutPermit
When the contract calls FHE.publishDecryptResult:
MockThresholdNetworkSigner that FHE.verifyDecryptResult accepts.
6. Permit-based unseal: decryptForView for the success path
permit_createSelf builds the EIP-712 typed-data, derives a sealing key from the connected account, and signs — no manual signPermissionSelf boilerplate.
7. Deny path: asserting “Alice cannot decrypt Bob’s value”
decryptForView reverts when the caller isn’t on the ACL. To assert the deny path, drop down to the mock directly:
mockThresholdNetwork is a public field on CofheTest.
8. Fuzz tests inherit normally
createInEuintN accepts the full uintN range — no shaping needed.
Migration from the old mock-contracts API
If you’re upgrading from @cofhe/mock-contracts@0.4.x (where CoFheTest lived inside the mocks package), here’s the rename table:
| Old API (mock-contracts ≤ 0.4) | New API (@cofhe/foundry-plugin) |
|---|---|
import { CoFheTest } from "@cofhe/mock-contracts/foundry/CoFheTest.sol" | import { CofheTest } from "@cofhe/foundry-plugin/contracts/CofheTest.sol" |
is Test, CoFheTest | is CofheTest (Test already inherited) |
assertHashValue(handle, value) | expectPlaintext(handle, value) |
mockStorage(ctHash) | getPlaintext(ctHash) |
createInEuint32(v, bob) | bob.createInEuint32(v) |
createPermissionSelf(bob) + signPermissionSelf(perm, bobKey) | bob.permit_createSelf() (auto-signs) |
createSealingKey(seed) | bob.createSealingKey(seed) (rarely needed — permit_createSelf derives one) |
queryDecrypt(hash, chainId, permit) | bob.decryptForView(hash, permit) (reverts on deny) |
| Same, asserting deny | mockThresholdNetwork.querySealOutput(hash, block.chainid, permit) |
querySealOutput + unseal | bob.decryptForView (does both) |
decryptForTxWithoutPermit(ct) returns (allowed, error, plaintext) | bob.decryptForTx_withoutPermit(ct) returns (ctHash, plaintext, signature) |
Common pitfalls
IDE Solidity errors but `forge test` passes
IDE Solidity errors but `forge test` passes
LSPs like Wake/Cursor often resolve from the monorepo root; the plugin’s remappings are package-local.
forge is the truth — if forge build and forge test succeed, the test is correct.Forgetting `FHE.allowThis` in the contract
Forgetting `FHE.allowThis` in the contract
Tests pass on the first op, then a second op reverts with
ACLNotAllowed because the contract itself isn’t on the ACL. Toggle enableLogs() to see the missing grant — every op prints a line showing whether allowThis/allow was called.Wrong client for the prank
Wrong client for the prank
vm.prank(bob.account()) while the input came from alice.createInEuintN(...) fails ZK verification. Always 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:Permit issuer must derive from the connected key
Permit issuer must derive from the connected key
The
pkey passed to connect must derive the address used as permit.issuer. bob.permit_createSelf() after bob.connect(0xB0B) produces issuer == vm.addr(0xB0B). Trying to forge a mismatch fails signature verification.Related
- CofheTest — the test base contract API.
- CofheClient — the per-user shim API.
- Hardhat → Testing — the same patterns under Hardhat.