@cofhe/foundry-plugin 0.0.0-alpha-20260409113701

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @cofhe/foundry-plugin
2
+
3
+ Foundry test utilities for [CoFHE](https://github.com/FhenixProtocol/cofhesdk). Provides `CofheTest` (base test contract with mock deployment) and `CofheClient` (SDK-like client for encrypting inputs, decrypting outputs, and managing permits).
4
+
5
+ ## Installation
6
+
7
+ **npm / pnpm**
8
+
9
+ ```sh
10
+ npm install --save-dev @cofhe/foundry-plugin
11
+ ```
12
+
13
+ Add to `foundry.toml`:
14
+
15
+ ```toml
16
+ [profile.default]
17
+ libs = ["node_modules", "lib"]
18
+ remappings = [
19
+ "forge-std/=node_modules/forge-std/src/",
20
+ "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/",
21
+ "@fhenixprotocol/cofhe-contracts/=node_modules/@fhenixprotocol/cofhe-contracts/",
22
+ "@cofhe/mock-contracts/=node_modules/@cofhe/mock-contracts/",
23
+ "@cofhe/foundry-plugin/=node_modules/@cofhe/foundry-plugin/contracts/"
24
+ ]
25
+ ```
26
+
27
+ **Git submodule**
28
+
29
+ ```sh
30
+ git submodule add https://github.com/FhenixProtocol/cofhesdk lib/cofhe-foundry-plugin
31
+ # also add @cofhe/mock-contracts as a second submodule or via npm
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ Inherit `CofheTest` in your test contract and call `deployMocks()` in `setUp`. Use `createCofheClient()` to get a connected client.
37
+
38
+ ```solidity
39
+ import { CofheTest } from "@cofhe/foundry-plugin/CofheTest.sol";
40
+ import { CofheClient } from "@cofhe/foundry-plugin/CofheClient.sol";
41
+
42
+ contract MyTest is CofheTest {
43
+ CofheClient client;
44
+ MyContract target;
45
+
46
+ uint256 constant USER_PKEY = 0xac09...;
47
+
48
+ function setUp() public {
49
+ deployMocks();
50
+ client = createCofheClient();
51
+ client.connect(USER_PKEY);
52
+ target = new MyContract();
53
+ }
54
+
55
+ function testEncryptAndStore() public {
56
+ InEuint32 memory enc = client.createInEuint32(42);
57
+
58
+ vm.prank(client.account());
59
+ target.store(enc);
60
+
61
+ expectPlaintext(target.eValue(), uint32(42));
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## API
67
+
68
+ ### `CofheTest` (abstract base)
69
+
70
+ | Function | Description |
71
+ | -------------------------------- | ------------------------------------------------------------ |
72
+ | `deployMocks()` | Deploys all mock contracts and wires them together |
73
+ | `createCofheClient()` | Returns a new unconnected `CofheClient` |
74
+ | `enableLogs()` / `disableLogs()` | Toggle plaintext operation logging |
75
+ | `getPlaintext(ctHash)` | Returns the stored plaintext for a ciphertext handle |
76
+ | `expectPlaintext(handle, value)` | Asserts the plaintext of an encrypted handle matches `value` |
77
+
78
+ `getPlaintext` and `expectPlaintext` have typed overloads for `ebool`, `euint8`, `euint16`, `euint32`, `euint64`, `euint128`, and `eaddress`.
79
+
80
+ ### `CofheClient`
81
+
82
+ | Function | Description |
83
+ | --------------------------------------------- | -------------------------------------------------------------------------------- |
84
+ | `connect(pkey)` | Sets the active account from a private key |
85
+ | `account()` | Returns the connected account address |
86
+ | `createInEbool/8/16/32/64/128/address(value)` | Creates a signed encrypted input |
87
+ | `decryptForTx_withoutPermit(ctHash)` | Decrypts a globally-allowed ciphertext; returns `(ctHash, plaintext, signature)` |
88
+ | `decryptForTx_withPermit(ctHash, permit)` | Decrypts with a permission; returns `(ctHash, plaintext, signature)` |
89
+ | `decryptForView(ctHash, sealingKey, permit)` | Seals and unseals for off-chain reading |
90
+ | `permit_createSelf()` | Creates a self-permit for the connected account |
91
+ | `permit_createShared(recipient)` | Creates the issuer half of a shared permit |
92
+ | `permit_exportShared(permit)` | Strips recipient fields for safe transmission |
93
+ | `permit_importShared(export)` | Completes a shared permit as the recipient |
@@ -0,0 +1,291 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity ^0.8.13;
3
+
4
+ import { Test } from 'forge-std/Test.sol';
5
+ import { MessageHashUtils } from '@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol';
6
+ import '@fhenixprotocol/cofhe-contracts/FHE.sol';
7
+ import { MockTaskManager } from '@cofhe/mock-contracts/contracts/MockTaskManager.sol';
8
+ import { MockACL } from '@cofhe/mock-contracts/contracts/MockACL.sol';
9
+ import { MockZkVerifier } from '@cofhe/mock-contracts/contracts/MockZkVerifier.sol';
10
+ import { MockZkVerifierSigner } from './MockZkVerifierSigner.sol';
11
+ import { MockThresholdNetwork } from '@cofhe/mock-contracts/contracts/MockThresholdNetwork.sol';
12
+ import { MockThresholdNetworkSigner } from './MockThresholdNetworkSigner.sol';
13
+ import { Permission, PermissionUtils } from '@cofhe/mock-contracts/contracts/Permissioned.sol';
14
+ import { ZK_VERIFIER_SIGNER_ADDRESS, DECRYPT_RESULT_SIGNER_ADDRESS } from '@cofhe/mock-contracts/contracts/MockCoFHE.sol';
15
+
16
+ /// @notice Portable representation of the issuer's half of a shared permit, safe to transmit as cleartext.
17
+ struct SharedPermitExport {
18
+ address issuer;
19
+ uint64 expiration;
20
+ address recipient;
21
+ uint256 validatorId;
22
+ address validatorContract;
23
+ bytes issuerSignature;
24
+ }
25
+
26
+ /// @notice SDK-like client for Foundry tests. Mirrors the JS SDK's `createCofheClient` pattern.
27
+ /// @dev Must be deployed via `CofheTestUtils.createCofheClient()` after `deployMocks()`.
28
+ /// Call `connect(pkey)` before using any other function.
29
+ contract CofheClient is Test {
30
+ // Keep in sync with `packages/sdk/core/consts.ts`
31
+ address constant ZK_VERIFIER_ADDRESS = 0x0000000000000000000000000000000000005001;
32
+ address constant THRESHOLD_NETWORK_ADDRESS = 0x0000000000000000000000000000000000005002;
33
+
34
+ MockTaskManager public mockTaskManager;
35
+ MockACL public mockAcl;
36
+ MockZkVerifier public mockZkVerifier;
37
+ MockZkVerifierSigner public mockZkVerifierSigner;
38
+ MockThresholdNetwork public mockThresholdNetwork;
39
+ MockThresholdNetworkSigner public mockThresholdNetworkSigner;
40
+
41
+ uint256 private _pkey;
42
+ address private _account;
43
+ bool private _connected;
44
+
45
+ constructor() {
46
+ mockTaskManager = MockTaskManager(TASK_MANAGER_ADDRESS);
47
+ mockAcl = MockACL(address(mockTaskManager.acl()));
48
+ mockZkVerifier = MockZkVerifier(ZK_VERIFIER_ADDRESS);
49
+ mockZkVerifierSigner = MockZkVerifierSigner(ZK_VERIFIER_SIGNER_ADDRESS);
50
+ mockThresholdNetwork = MockThresholdNetwork(THRESHOLD_NETWORK_ADDRESS);
51
+ mockThresholdNetworkSigner = MockThresholdNetworkSigner(DECRYPT_RESULT_SIGNER_ADDRESS);
52
+ }
53
+
54
+ modifier onlyConnected() {
55
+ require(_connected, 'CofheClient: not connected');
56
+ _;
57
+ }
58
+
59
+ /// @notice Returns the address derived from the connected private key.
60
+ function account() public view onlyConnected returns (address) {
61
+ return _account;
62
+ }
63
+
64
+ /// @notice Stores the private key, derives the account address, and marks the client as connected.
65
+ function connect(uint256 pkey) public {
66
+ _pkey = pkey;
67
+ _account = vm.addr(pkey);
68
+ _connected = true;
69
+ }
70
+
71
+ // =====================
72
+ // ENCRYPT
73
+ // =====================
74
+
75
+ /// @notice Creates a signed encrypted input for the connected account (security zone 0).
76
+ function createEncryptedInput(
77
+ uint8 utype,
78
+ uint256 value
79
+ ) internal onlyConnected returns (EncryptedInput memory input) {
80
+ input = mockZkVerifier.zkVerify(value, utype, _account, 0, block.chainid);
81
+ input = mockZkVerifierSigner.zkVerifySign(input, _account);
82
+ }
83
+
84
+ /// @notice Creates an encrypted boolean input.
85
+ function createInEbool(bool value) public returns (InEbool memory) {
86
+ return abi.decode(abi.encode(createEncryptedInput(Utils.EBOOL_TFHE, value ? 1 : 0)), (InEbool));
87
+ }
88
+
89
+ /// @notice Creates an encrypted uint8 input.
90
+ function createInEuint8(uint8 value) public returns (InEuint8 memory) {
91
+ return abi.decode(abi.encode(createEncryptedInput(Utils.EUINT8_TFHE, value)), (InEuint8));
92
+ }
93
+
94
+ /// @notice Creates an encrypted uint16 input.
95
+ function createInEuint16(uint16 value) public returns (InEuint16 memory) {
96
+ return abi.decode(abi.encode(createEncryptedInput(Utils.EUINT16_TFHE, value)), (InEuint16));
97
+ }
98
+
99
+ /// @notice Creates an encrypted uint32 input.
100
+ function createInEuint32(uint32 value) public returns (InEuint32 memory) {
101
+ return abi.decode(abi.encode(createEncryptedInput(Utils.EUINT32_TFHE, value)), (InEuint32));
102
+ }
103
+
104
+ /// @notice Creates an encrypted uint64 input.
105
+ function createInEuint64(uint64 value) public returns (InEuint64 memory) {
106
+ return abi.decode(abi.encode(createEncryptedInput(Utils.EUINT64_TFHE, value)), (InEuint64));
107
+ }
108
+
109
+ /// @notice Creates an encrypted uint128 input.
110
+ function createInEuint128(uint128 value) public returns (InEuint128 memory) {
111
+ return abi.decode(abi.encode(createEncryptedInput(Utils.EUINT128_TFHE, value)), (InEuint128));
112
+ }
113
+
114
+ /// @notice Creates an encrypted address input.
115
+ function createInEaddress(address value) public returns (InEaddress memory) {
116
+ return abi.decode(abi.encode(createEncryptedInput(Utils.EADDRESS_TFHE, uint256(uint160(value)))), (InEaddress));
117
+ }
118
+
119
+ // =====================
120
+ // DECRYPT FOR TX
121
+ // =====================
122
+
123
+ /// @notice Decrypts a globally-allowed ciphertext and returns the plaintext with a publishable signature.
124
+ function decryptForTx_withoutPermit(
125
+ bytes32 ctHash
126
+ ) public view onlyConnected returns (bytes32, uint256, bytes memory) {
127
+ uint256 ct = uint256(ctHash);
128
+
129
+ (bool allowed, string memory error, uint256 decryptedValue) = mockThresholdNetwork.decryptForTxWithoutPermit(ct);
130
+ require(allowed, string.concat('CofheClient: decryptForTx failed: ', error));
131
+
132
+ bytes memory signature = mockThresholdNetworkSigner.signDecryptResult(ct, decryptedValue);
133
+ return (ctHash, decryptedValue, signature);
134
+ }
135
+
136
+ /// @notice Decrypts a ciphertext using a permit and returns the plaintext with a publishable signature.
137
+ function decryptForTx_withPermit(
138
+ bytes32 ctHash,
139
+ Permission memory permission
140
+ ) public view onlyConnected returns (bytes32, uint256, bytes memory) {
141
+ uint256 ct = uint256(ctHash);
142
+
143
+ (bool allowed, string memory error, uint256 decryptedValue) = mockThresholdNetwork.decryptForTxWithPermit(
144
+ ct,
145
+ permission
146
+ );
147
+ require(allowed, string.concat('CofheClient: decryptForTx failed: ', error));
148
+
149
+ bytes memory signature = mockThresholdNetworkSigner.signDecryptResult(ct, decryptedValue);
150
+ return (ctHash, decryptedValue, signature);
151
+ }
152
+
153
+ // =====================
154
+ // DECRYPT FOR VIEW
155
+ // =====================
156
+
157
+ /// @notice Decrypts a ciphertext for off-chain reading by sealing/unsealing with the permit's sealing key.
158
+ function decryptForView(
159
+ bytes32 ctHash,
160
+ Permission memory permission
161
+ ) public view onlyConnected returns (uint256) {
162
+ uint256 ct = uint256(ctHash);
163
+
164
+ (bool allowed, string memory error, bytes32 sealedOutput) = mockThresholdNetwork.querySealOutput(
165
+ ct,
166
+ block.chainid,
167
+ permission
168
+ );
169
+ require(allowed, string.concat('CofheClient: decryptForView failed: ', error));
170
+
171
+ return mockThresholdNetwork.unseal(sealedOutput, permission.sealingKey);
172
+ }
173
+
174
+ // =====================
175
+ // PERMITS
176
+ // =====================
177
+
178
+ bytes32 private constant PERMISSION_TYPE_HASH =
179
+ keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)');
180
+
181
+ function permissionDomainSeparator() internal view returns (bytes32) {
182
+ string memory name;
183
+ string memory version;
184
+ uint256 chainId;
185
+ address verifyingContract;
186
+
187
+ (, name, version, chainId, verifyingContract, , ) = mockAcl.eip712Domain();
188
+
189
+ return
190
+ keccak256(
191
+ abi.encode(PERMISSION_TYPE_HASH, keccak256(bytes(name)), keccak256(bytes(version)), chainId, verifyingContract)
192
+ );
193
+ }
194
+
195
+ /// @notice Wraps a struct hash into a full EIP-712 typed data hash using the ACL's domain separator.
196
+ function permissionHashTypedDataV4(bytes32 structHash) public view returns (bytes32) {
197
+ return MessageHashUtils.toTypedDataHash(permissionDomainSeparator(), structHash);
198
+ }
199
+
200
+ function _signPermission(bytes32 structHash, uint256 pkey) internal pure returns (bytes memory signature) {
201
+ (uint8 v, bytes32 r, bytes32 s) = vm.sign(pkey, structHash);
202
+ return abi.encodePacked(r, s, v);
203
+ }
204
+
205
+ function _signIssuerSelf(Permission memory permission, uint256 pkey) internal view returns (Permission memory) {
206
+ bytes32 permissionHash = PermissionUtils.issuerSelfHash(permission);
207
+ bytes32 structHash = permissionHashTypedDataV4(permissionHash);
208
+ permission.issuerSignature = _signPermission(structHash, pkey);
209
+ return permission;
210
+ }
211
+
212
+ function _signIssuerShared(Permission memory permission, uint256 pkey) internal view returns (Permission memory) {
213
+ bytes32 permissionHash = PermissionUtils.issuerSharedHash(permission);
214
+ bytes32 structHash = permissionHashTypedDataV4(permissionHash);
215
+ permission.issuerSignature = _signPermission(structHash, pkey);
216
+ return permission;
217
+ }
218
+
219
+ function _signRecipient(Permission memory permission, uint256 pkey) internal view returns (Permission memory) {
220
+ bytes32 permissionHash = PermissionUtils.recipientHash(permission);
221
+ bytes32 structHash = permissionHashTypedDataV4(permissionHash);
222
+ permission.recipientSignature = _signPermission(structHash, pkey);
223
+ return permission;
224
+ }
225
+
226
+ /// @notice Returns a blank Permission with default field values.
227
+ function createBasePermission() public pure returns (Permission memory permission) {
228
+ permission = Permission({
229
+ issuer: address(0),
230
+ expiration: 1000000000000,
231
+ recipient: address(0),
232
+ validatorId: 0,
233
+ validatorContract: address(0),
234
+ sealingKey: bytes32(0),
235
+ issuerSignature: new bytes(0),
236
+ recipientSignature: new bytes(0)
237
+ });
238
+ }
239
+
240
+ /// @notice Derives a deterministic sealing key from a seed.
241
+ function createSealingKey(uint256 seed) public pure returns (bytes32) {
242
+ return keccak256(abi.encodePacked(seed));
243
+ }
244
+
245
+ /// @notice Creates a self-permit for the connected account, signed with the stored private key.
246
+ function permit_createSelf() public view onlyConnected returns (Permission memory permission) {
247
+ permission = createBasePermission();
248
+ permission.issuer = _account;
249
+ permission.sealingKey = createSealingKey(uint256(uint160(_account)));
250
+ permission = _signIssuerSelf(permission, _pkey);
251
+ }
252
+
253
+ /// @notice Creates the issuer side of a shared permit. The result has no sealingKey (added by recipient on import).
254
+ function permit_createShared(address recipient) public view onlyConnected returns (Permission memory permission) {
255
+ permission = createBasePermission();
256
+ permission.issuer = _account;
257
+ permission.recipient = recipient;
258
+ permission = _signIssuerShared(permission, _pkey);
259
+ }
260
+
261
+ /// @notice Exports a shared permit, stripping sensitive/recipient-specific fields.
262
+ function permit_exportShared(Permission memory permission) public pure returns (SharedPermitExport memory exported) {
263
+ exported = SharedPermitExport({
264
+ issuer: permission.issuer,
265
+ expiration: permission.expiration,
266
+ recipient: permission.recipient,
267
+ validatorId: permission.validatorId,
268
+ validatorContract: permission.validatorContract,
269
+ issuerSignature: permission.issuerSignature
270
+ });
271
+ }
272
+
273
+ /// @notice Imports a shared permit export, adds the recipient's sealing key and signature.
274
+ function permit_importShared(
275
+ SharedPermitExport memory data
276
+ ) public view onlyConnected returns (Permission memory permission) {
277
+ require(data.recipient == _account, 'CofheClient: recipient mismatch');
278
+
279
+ permission = Permission({
280
+ issuer: data.issuer,
281
+ expiration: data.expiration,
282
+ recipient: data.recipient,
283
+ validatorId: data.validatorId,
284
+ validatorContract: data.validatorContract,
285
+ sealingKey: createSealingKey(uint256(uint160(_account))),
286
+ issuerSignature: data.issuerSignature,
287
+ recipientSignature: new bytes(0)
288
+ });
289
+ permission = _signRecipient(permission, _pkey);
290
+ }
291
+ }
@@ -0,0 +1,242 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity ^0.8.13;
3
+
4
+ import { Test } from 'forge-std/Test.sol';
5
+ import '@fhenixprotocol/cofhe-contracts/FHE.sol';
6
+ import { MockTaskManager } from '@cofhe/mock-contracts/contracts/MockTaskManager.sol';
7
+ import { MockACL } from '@cofhe/mock-contracts/contracts/MockACL.sol';
8
+ import { MockZkVerifier } from '@cofhe/mock-contracts/contracts/MockZkVerifier.sol';
9
+ import { MockZkVerifierSigner } from './MockZkVerifierSigner.sol';
10
+ import { MockThresholdNetwork } from '@cofhe/mock-contracts/contracts/MockThresholdNetwork.sol';
11
+ import { MockThresholdNetworkSigner } from './MockThresholdNetworkSigner.sol';
12
+ import { CofheClient } from './CofheClient.sol';
13
+ import {
14
+ ZK_VERIFIER_SIGNER_ADDRESS,
15
+ DECRYPT_RESULT_SIGNER_ADDRESS
16
+ } from '@cofhe/mock-contracts/contracts/MockCoFHE.sol';
17
+
18
+ /// @notice Base contract for Foundry tests that use the CoFHE mock environment.
19
+ /// @dev Inherit this contract and call `deployMocks()` in your `setUp()` function.
20
+ /// Use `createCofheClient()` to obtain a connected client instance.
21
+ abstract contract CofheTest is Test {
22
+ MockTaskManager public mockTaskManager;
23
+ MockACL public mockAcl;
24
+ MockZkVerifier public mockZkVerifier;
25
+ MockZkVerifierSigner public mockZkVerifierSigner;
26
+ MockThresholdNetwork public mockThresholdNetwork;
27
+ MockThresholdNetworkSigner public mockThresholdNetworkSigner;
28
+
29
+ // Keep in sync with `packages/sdk/core/consts.ts`
30
+ address constant ZK_VERIFIER_ADDRESS = 0x0000000000000000000000000000000000005001;
31
+ address constant THRESHOLD_NETWORK_ADDRESS = 0x0000000000000000000000000000000000005002;
32
+
33
+ address public constant TM_ADMIN = address(128);
34
+
35
+ // =====================
36
+ // DEPLOY MOCKS
37
+ // =====================
38
+
39
+ /// @notice Deploys all mock contracts and wires them together, mirroring the Hardhat plugin's deployment order.
40
+ function deployMocks() public {
41
+ // 1. Task Manager
42
+ deployCodeTo('MockTaskManager.sol:MockTaskManager', TASK_MANAGER_ADDRESS);
43
+ mockTaskManager = MockTaskManager(TASK_MANAGER_ADDRESS);
44
+ mockTaskManager.initialize(TM_ADMIN);
45
+ mockTaskManager.setLogOps(false);
46
+ vm.label(address(mockTaskManager), 'MockTaskManager');
47
+
48
+ // 2. ACL (non-fixed deploy so constructor runs and EIP712 domain is set)
49
+ mockAcl = new MockACL();
50
+ vm.label(address(mockAcl), 'MockACL');
51
+
52
+ // 3. Link Task Manager <-> ACL, configure signers
53
+ vm.startPrank(TM_ADMIN);
54
+ mockTaskManager.setACLContract(address(mockAcl));
55
+ mockTaskManager.setSecurityZoneMin(0);
56
+ mockTaskManager.setSecurityZoneMax(1);
57
+ mockTaskManager.setVerifierSigner(ZK_VERIFIER_SIGNER_ADDRESS);
58
+ mockTaskManager.setDecryptResultSigner(DECRYPT_RESULT_SIGNER_ADDRESS);
59
+ vm.stopPrank();
60
+
61
+ // 4. Fund ZK Verifier Signer
62
+ vm.deal(ZK_VERIFIER_SIGNER_ADDRESS, 10 ether);
63
+
64
+ // 5. ZK Verifier
65
+ deployCodeTo('MockZkVerifier.sol:MockZkVerifier', ZK_VERIFIER_ADDRESS);
66
+ mockZkVerifier = MockZkVerifier(ZK_VERIFIER_ADDRESS);
67
+ vm.label(address(mockZkVerifier), 'MockZkVerifier');
68
+
69
+ // 6. ZK Verifier Signer
70
+ deployCodeTo('MockZkVerifierSigner.sol:MockZkVerifierSigner', ZK_VERIFIER_SIGNER_ADDRESS);
71
+ mockZkVerifierSigner = MockZkVerifierSigner(ZK_VERIFIER_SIGNER_ADDRESS);
72
+ vm.label(address(mockZkVerifierSigner), 'MockZkVerifierSigner');
73
+
74
+ // 7. Threshold Network
75
+ deployCodeTo('MockThresholdNetwork.sol:MockThresholdNetwork', THRESHOLD_NETWORK_ADDRESS);
76
+ mockThresholdNetwork = MockThresholdNetwork(THRESHOLD_NETWORK_ADDRESS);
77
+ mockThresholdNetwork.initialize(TASK_MANAGER_ADDRESS, address(mockAcl));
78
+ vm.label(address(mockThresholdNetwork), 'MockThresholdNetwork');
79
+
80
+ // 8. Threshold Network Signer
81
+ deployCodeTo('MockThresholdNetworkSigner.sol:MockThresholdNetworkSigner', DECRYPT_RESULT_SIGNER_ADDRESS);
82
+ mockThresholdNetworkSigner = MockThresholdNetworkSigner(DECRYPT_RESULT_SIGNER_ADDRESS);
83
+ vm.label(address(mockThresholdNetworkSigner), 'MockThresholdNetworkSigner');
84
+ }
85
+
86
+ // =====================
87
+ // CREATE CLIENT
88
+ // =====================
89
+
90
+ /// @notice Deploys a new CofheClient instance. Call `connect(pkey)` on the result before use.
91
+ function createCofheClient() public returns (CofheClient) {
92
+ return new CofheClient();
93
+ }
94
+
95
+ // =====================
96
+ // LOGGING
97
+ // =====================
98
+
99
+ /// @notice Enables plaintext operation logging in the mock task manager.
100
+ function enableLogs() public {
101
+ mockTaskManager.setLogOps(true);
102
+ }
103
+
104
+ /// @notice Disables plaintext operation logging in the mock task manager.
105
+ function disableLogs() public {
106
+ mockTaskManager.setLogOps(false);
107
+ }
108
+
109
+ // =====================
110
+ // GET PLAINTEXT
111
+ // =====================
112
+
113
+ /// @notice Returns the stored plaintext for a ciphertext hash. Reverts if the hash is not in mock storage.
114
+ function getPlaintext(bytes32 ctHash) public view returns (uint256) {
115
+ uint256 ct = uint256(ctHash);
116
+ require(mockTaskManager.inMockStorage(ct), 'CofheTest: plaintext does not exist');
117
+ return mockTaskManager.mockStorage(ct);
118
+ }
119
+
120
+ /// @notice Returns the stored plaintext of an ebool. Reverts if the hash is not in mock storage.
121
+ function getPlaintext(ebool eValue) internal view returns (bool) {
122
+ return getPlaintext(ebool.unwrap(eValue)) != 0;
123
+ }
124
+
125
+ /// @notice Returns the stored plaintext of an euint8. Reverts if the hash is not in mock storage.
126
+ function getPlaintext(euint8 eValue) internal view returns (uint8) {
127
+ return uint8(getPlaintext(euint8.unwrap(eValue)));
128
+ }
129
+
130
+ /// @notice Returns the stored plaintext of an euint16. Reverts if the hash is not in mock storage.
131
+ function getPlaintext(euint16 eValue) internal view returns (uint16) {
132
+ return uint16(getPlaintext(euint16.unwrap(eValue)));
133
+ }
134
+
135
+ /// @notice Returns the stored plaintext of an euint32. Reverts if the hash is not in mock storage.
136
+ function getPlaintext(euint32 eValue) internal view returns (uint32) {
137
+ return uint32(getPlaintext(euint32.unwrap(eValue)));
138
+ }
139
+
140
+ /// @notice Returns the stored plaintext of an euint64. Reverts if the hash is not in mock storage.
141
+ function getPlaintext(euint64 eValue) internal view returns (uint64) {
142
+ return uint64(getPlaintext(euint64.unwrap(eValue)));
143
+ }
144
+
145
+ /// @notice Returns the stored plaintext of an euint128. Reverts if the hash is not in mock storage.
146
+ function getPlaintext(euint128 eValue) internal view returns (uint128) {
147
+ return uint128(getPlaintext(euint128.unwrap(eValue)));
148
+ }
149
+
150
+ /// @notice Returns the stored plaintext of an eaddress. Reverts if the hash is not in mock storage.
151
+ function getPlaintext(eaddress eValue) internal view returns (address) {
152
+ return address(uint160(getPlaintext(eaddress.unwrap(eValue))));
153
+ }
154
+
155
+ // =====================
156
+ // EXPECT PLAINTEXT
157
+ // =====================
158
+
159
+ /// @notice Asserts that a ctHash exists in mock storage and its plaintext matches the expected value.
160
+ function expectPlaintext(bytes32 ctHash, uint256 value) public view {
161
+ uint256 ct = uint256(ctHash);
162
+ assertEq(mockTaskManager.inMockStorage(ct), true);
163
+ assertEq(mockTaskManager.mockStorage(ct), value);
164
+ }
165
+
166
+ /// @notice Same as `expectPlaintext(bytes32, uint256)` but includes a failure message.
167
+ function expectPlaintext(bytes32 ctHash, uint256 value, string memory message) public view {
168
+ uint256 ct = uint256(ctHash);
169
+ assertEq(mockTaskManager.inMockStorage(ct), true, message);
170
+ assertEq(mockTaskManager.mockStorage(ct), value, message);
171
+ }
172
+
173
+ /// @notice Asserts that a ebool exists in mock storage and its plaintext matches the expected value.
174
+ function expectPlaintext(ebool eValue, bool value) public view {
175
+ expectPlaintext(ebool.unwrap(eValue), value ? 1 : 0);
176
+ }
177
+
178
+ /// @notice Asserts that a euint8 exists in mock storage and its plaintext matches the expected value.
179
+ function expectPlaintext(euint8 eValue, uint8 value) public view {
180
+ expectPlaintext(euint8.unwrap(eValue), uint256(value));
181
+ }
182
+
183
+ /// @notice Asserts that a euint16 exists in mock storage and its plaintext matches the expected value.
184
+ function expectPlaintext(euint16 eValue, uint16 value) public view {
185
+ expectPlaintext(euint16.unwrap(eValue), uint256(value));
186
+ }
187
+
188
+ /// @notice Asserts that a euint32 exists in mock storage and its plaintext matches the expected value.
189
+ function expectPlaintext(euint32 eValue, uint32 value) public view {
190
+ expectPlaintext(euint32.unwrap(eValue), uint256(value));
191
+ }
192
+
193
+ /// @notice Asserts that a euint64 exists in mock storage and its plaintext matches the expected value.
194
+ function expectPlaintext(euint64 eValue, uint64 value) public view {
195
+ expectPlaintext(euint64.unwrap(eValue), uint256(value));
196
+ }
197
+
198
+ /// @notice Asserts that a euint128 exists in mock storage and its plaintext matches the expected value.
199
+ function expectPlaintext(euint128 eValue, uint128 value) public view {
200
+ expectPlaintext(euint128.unwrap(eValue), uint256(value));
201
+ }
202
+
203
+ /// @notice Asserts that a eaddress exists in mock storage and its plaintext matches the expected value.
204
+ function expectPlaintext(eaddress eValue, address value) public view {
205
+ expectPlaintext(eaddress.unwrap(eValue), uint256(uint160(value)));
206
+ }
207
+
208
+ /// @notice Asserts that a ebool exists in mock storage and its plaintext matches the expected value, with a failure message.
209
+ function expectPlaintext(ebool eValue, bool value, string memory message) public view {
210
+ expectPlaintext(ebool.unwrap(eValue), value ? 1 : 0, message);
211
+ }
212
+
213
+ /// @notice Asserts that a euint8 exists in mock storage and its plaintext matches the expected value, with a failure message.
214
+ function expectPlaintext(euint8 eValue, uint8 value, string memory message) public view {
215
+ expectPlaintext(euint8.unwrap(eValue), uint256(value), message);
216
+ }
217
+
218
+ /// @notice Asserts that a euint16 exists in mock storage and its plaintext matches the expected value, with a failure message.
219
+ function expectPlaintext(euint16 eValue, uint16 value, string memory message) public view {
220
+ expectPlaintext(euint16.unwrap(eValue), uint256(value), message);
221
+ }
222
+
223
+ /// @notice Asserts that a euint32 exists in mock storage and its plaintext matches the expected value, with a failure message.
224
+ function expectPlaintext(euint32 eValue, uint32 value, string memory message) public view {
225
+ expectPlaintext(euint32.unwrap(eValue), uint256(value), message);
226
+ }
227
+
228
+ /// @notice Asserts that a euint64 exists in mock storage and its plaintext matches the expected value, with a failure message.
229
+ function expectPlaintext(euint64 eValue, uint64 value, string memory message) public view {
230
+ expectPlaintext(euint64.unwrap(eValue), uint256(value), message);
231
+ }
232
+
233
+ /// @notice Asserts that a euint128 exists in mock storage and its plaintext matches the expected value, with a failure message.
234
+ function expectPlaintext(euint128 eValue, uint128 value, string memory message) public view {
235
+ expectPlaintext(euint128.unwrap(eValue), uint256(value), message);
236
+ }
237
+
238
+ /// @notice Asserts that a eaddress exists in mock storage and its plaintext matches the expected value, with a failure message.
239
+ function expectPlaintext(eaddress eValue, address value, string memory message) public view {
240
+ expectPlaintext(eaddress.unwrap(eValue), uint256(uint160(value)), message);
241
+ }
242
+ }
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: BSD-3-Clause-Clear
2
+
3
+ pragma solidity >=0.8.19 <0.9.0;
4
+
5
+ import { Test } from 'forge-std/Test.sol';
6
+ import { DECRYPT_RESULT_SIGNER_PRIVATE_KEY } from '@cofhe/mock-contracts/contracts/MockCoFHE.sol';
7
+
8
+ /**
9
+ * @dev Generates valid signatures for decrypt results.
10
+ * Uses vm.sign to generate the signatures (only available in foundry tests).
11
+ * Mirrors the SDK's cofheMocksDecryptForTx.ts signing logic:
12
+ * keccak256(abi.encodePacked(ctHash, decryptedValue)) signed with DECRYPT_RESULT_SIGNER_PRIVATE_KEY
13
+ */
14
+ contract MockThresholdNetworkSigner is Test {
15
+ function signDecryptResult(
16
+ uint256 ctHash,
17
+ uint256 decryptedValue
18
+ ) public pure returns (bytes memory signature) {
19
+ bytes32 messageHash = keccak256(abi.encodePacked(ctHash, decryptedValue));
20
+
21
+ (uint8 v, bytes32 r, bytes32 s) = vm.sign(DECRYPT_RESULT_SIGNER_PRIVATE_KEY, messageHash);
22
+ signature = abi.encodePacked(r, s, v);
23
+ }
24
+ }
@@ -0,0 +1,40 @@
1
+ // SPDX-License-Identifier: BSD-3-Clause-Clear
2
+ // solhint-disable one-contract-per-file
3
+
4
+ pragma solidity >=0.8.19 <0.9.0;
5
+
6
+ import { console } from 'forge-std/console.sol';
7
+ import { Test } from 'forge-std/Test.sol';
8
+ import { MessageHashUtils } from '@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol';
9
+ import { EncryptedInput } from '@fhenixprotocol/cofhe-contracts/ICofhe.sol';
10
+ import { ZK_VERIFIER_SIGNER_PRIVATE_KEY } from '@cofhe/mock-contracts/contracts/MockCoFHE.sol';
11
+
12
+ /**
13
+ * @dev Generates valid signatures for encrypted inputs.
14
+ * Uses vm.sign to generate the signatures (only available in foundry tests)
15
+ * Should not need to be interacted with directly, is part of the `createEncryptedInput` function set
16
+ */
17
+ contract MockZkVerifierSigner is Test {
18
+ function zkVerifySignPacked(
19
+ EncryptedInput[] memory inputs,
20
+ address sender
21
+ ) public view returns (EncryptedInput[] memory) {
22
+ EncryptedInput[] memory signedInputs = new EncryptedInput[](inputs.length);
23
+ for (uint256 i = 0; i < inputs.length; i++) {
24
+ signedInputs[i] = zkVerifySign(inputs[i], sender);
25
+ }
26
+ return signedInputs;
27
+ }
28
+
29
+ function zkVerifySign(EncryptedInput memory input, address sender) public view returns (EncryptedInput memory) {
30
+ bytes memory combined = abi.encodePacked(input.ctHash, input.utype, input.securityZone, sender, block.chainid);
31
+
32
+ bytes32 expectedHash = keccak256(combined);
33
+
34
+ (uint8 v, bytes32 r, bytes32 s) = vm.sign(ZK_VERIFIER_SIGNER_PRIVATE_KEY, expectedHash);
35
+ bytes memory signature = abi.encodePacked(r, s, v); // note the order here is different from line above.
36
+
37
+ input.signature = signature;
38
+ return input;
39
+ }
40
+ }
package/foundry.toml ADDED
@@ -0,0 +1,17 @@
1
+ [profile.default]
2
+ src = "contracts"
3
+ out = "out"
4
+ libs = ["node_modules", "lib"]
5
+ auto_detect_remappings = false
6
+ remappings = [
7
+ "forge-std/=node_modules/forge-std/src/",
8
+ "hardhat/=node_modules/forge-std/src/",
9
+ "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/",
10
+ "@fhenixprotocol/cofhe-contracts/=node_modules/@fhenixprotocol/cofhe-contracts/",
11
+ "@cofhe/mock-contracts/=node_modules/@cofhe/mock-contracts/"
12
+ ]
13
+
14
+ [lint]
15
+ lint_on_build = false
16
+
17
+ # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@cofhe/foundry-plugin",
3
+ "version": "0.0.0-alpha-20260409113701",
4
+ "description": "Foundry plugin for CoFHE — Write and test FHE enabled contracts",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/FhenixProtocol/cofhesdk.git",
9
+ "directory": "packages/foundry-plugin"
10
+ },
11
+ "files": [
12
+ "contracts/**/*.sol",
13
+ "foundry.toml"
14
+ ],
15
+ "keywords": [
16
+ "blockchain",
17
+ "ethereum",
18
+ "smart-contracts",
19
+ "solidity",
20
+ "FHE",
21
+ "foundry",
22
+ "testing",
23
+ "cofhe",
24
+ "fhenix"
25
+ ],
26
+ "dependencies": {
27
+ "@fhenixprotocol/cofhe-contracts": "0.1.0",
28
+ "@openzeppelin/contracts": "^5.0.0",
29
+ "@cofhe/mock-contracts": "0.0.0-alpha-20260409113701"
30
+ },
31
+ "devDependencies": {
32
+ "forge-std": "github:foundry-rs/forge-std"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "registry": "https://registry.npmjs.org/"
37
+ },
38
+ "scripts": {
39
+ "build": "forge build -q",
40
+ "test": "forge build -q && forge test -vvv",
41
+ "clean": "rm -rf out && rm -rf cache"
42
+ }
43
+ }