@cofhe/mock-contracts 0.6.0 → 0.7.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @cofhe/mock-contracts Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fb87d91: **Breaking: Permit (V2) → ACP (Access Control Permission).** Permits become scoped, revocable ACPs; old names are removed rather than deprecated. Highlights (full list in the [0.7.0 migration guide](https://cofhesdk.fhenix.io/migrating-to-0-7-0)):
8
+
9
+ - `Permit`/`Permission`/`PermitUtils`/`client.permits` → `ACP`/`ACPPublic`/`ACPUtils`/`client.acp`; `getPermission()` → `getPublic()`
10
+ - `ACPPrivate` & `ACPPublic` are top-level types, `ACP` is the union; the sealing keypair is flattened to `sealingPrivateKey`/`sealingKey` (the `SealingKey` class is removed)
11
+ - New scope fields (`scope`, `contracts`, `handles: bytes32[]`) and revocation fields renamed `validatorId`/`validatorContract` → `revokerData`/`revokerContract`; default revoker `ACPTimestampRevoker` with `revokeACP`/`revokeAllACPs`/`isACPRevoked` on the client
12
+ - EIP-712 domain bumped to `("ACL", "2")` with new `ACPIssuerSelf`/`ACPIssuerShared`/`ACPRecipient` types — previously signed permits no longer verify; the permit store migrates by wiping retired-format permits
13
+ - `ACPUtils.export()` produces a fixed `SharedACP` shape and only accepts signed sharing ACPs
14
+
15
+ - fb87d91: **On-chain ACP sharing.** New `ACPShareRegistry` contract (deployed with the mocks) lets an issuer post a sharing ACP on-chain for its recipient to discover and import — replacing the JSON copy-paste hand-off.
16
+
17
+ - `client.acp.shareOnChain(acp)` posts a signed sharing ACP (issuer-only, same guards as `export()`); `cancelShare(shareId)` retracts it
18
+ - `client.acp.getIncomingShares()` lists importable shares addressed to the connected account (unexpired, not revoked — the registry checks the share's own revoker)
19
+ - `client.acp.importFromChain(share)` imports like the JSON flow (recipient sealing key + signature); `dismissShare(shareId)` cleans up the entry
20
+ - config: `acp.sharingRegistry: Record<chainId, address>`
21
+ - registry exposes `isShareValid(shareId)` as an on-chain verification hook for contracts
22
+
23
+ - fb87d91: Migrate `cofheClient.encryptInputs` from one-signature-per-ciphertext to the new batch verification scheme (one signature per batch, per `FhenixProtocol/cofhe-contracts#78`).
24
+
25
+ **Breaking:** `EncryptInputsBuilder.execute()` now always returns `[...hashes, signature]` (`HashPlusProofResult<T>`) instead of an array of per-item `EncryptedItemInput` structs. `EncryptedItemInput` and its per-type aliases (`EncryptedBoolInput`, `EncryptedUint8Input`, etc.) are removed, along with `EncryptInputsBuilder.asHashPlusProof()` (no longer needed - it's the only shape now). `@cofhe/abi`'s `extractEncryptableValues`/`insertEncryptedValues` now detect `external*` ABI types instead of `struct InEuintXX`, with a new calling convention: any function with encrypted inputs must end with a plain `bytes` parameter to receive the shared batch signature. `@cofhe/foundry-plugin`'s `CofheClient.createIn*` helpers are renamed to `createExternal*` (`createInEuint32` → `createExternalEuint32`, etc.) and now return an `external*` handle plus a batch signature rather than an `InEuintXX` struct; the `createIn*_asHashPlusProof` variants are removed as redundant. See the [0.7.0 migration guide](https://cofhesdk.fhenix.io/migrating-to-0-7-0) for the full list of changes and what contract authors need to update.
26
+
27
+ - fb87d91: Bind encrypted inputs to a consuming contract (`FhenixProtocol/cofhe-contracts#77`). The verifier-signed digest now includes the contract that will consume the ciphertext, closing a replay path where a signed input packet observed on-chain could be reused against a different contract than the one it was signed for.
28
+
29
+ **Breaking:** `EncryptInputsBuilder.setConsumingContract(address)` must be called before `.execute()` - it throws `ConsumingContractUninitialized` otherwise. `@cofhe/mock-contracts`'s `MockTaskManager` signature digest changed to include the consuming contract (external ABI unchanged). `@cofhe/foundry-plugin`'s `CofheClient.createExternal*`/`createEuint32sBatch` helpers now take a required `address consumingContract` as their last parameter (no global setter). `@cofhe/react`'s `useCofheEncryptAndWriteContract` defaults `consumingContract` to the write's target address automatically; `useCofheEncrypt` accepts it as an explicit option. See the [0.7.0 migration guide](https://cofhesdk.fhenix.io/migrating-to-0-7-0) for full details.
30
+
31
+ - fb87d91: Add mock support for `sharedEuintXX` — contract-to-contract encrypted value movement, shipped in `@fhenixprotocol/cofhe-contracts@0.2.0-beta.3`.
32
+
33
+ `MockACL` gains `shareCtHash(handle, sharer, receiver)` and `receiveCtHash(handle, expectedSharer, receiver)`, backed by a directed, single-use, transaction-scoped share slot (domain-separated from transient allowance keys), plus the `NotShared` and `UnexpectedSharer` errors. `MockTaskManager` gains the matching `shareCtHash`/`receiveCtHash` passthroughs with `MOCK_logAllow` hooks.
34
+
35
+ `MockACL`'s transient allowances now use real EIP-1153 transient storage instead of approximating it with `block.number`, and `cleanTransientStorage()` is implemented rather than a no-op. This makes the mock faithful to the production ACL: **a transient allowance now expires at the end of its own transaction, not at the end of the block.** Tests that granted a transient allowance in one transaction and relied on it in a later transaction of the same block must be updated.
36
+
37
+ The repo also drops the temporary `pnpm patch` that carried these `FHE.sol` / `ICofhe.sol` additions, and pins `@fhenixprotocol/cofhe-contracts@0.2.0-beta.3`.
38
+
39
+ ## 0.6.1
40
+
3
41
  ## 0.6.0
4
42
 
5
43
  ### Minor Changes
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # cofhe/mock-contracts [![NPM Package][npm-badge]][npm] [![License: MIT][license-badge]][license]
2
2
 
3
- [npm]: https://www.npmjs.com/package/@fhenixprotocol/cofhe-mock-contracts
4
- [npm-badge]: https://img.shields.io/npm/v/@fhenixprotocol/cofhe-mock-contracts.svg
3
+ [npm]: https://www.npmjs.com/package/@cofhe/mock-contracts
4
+ [npm-badge]: https://img.shields.io/npm/v/@cofhe/mock-contracts.svg
5
5
  [license]: https://opensource.org/licenses/MIT
6
6
  [license-badge]: https://img.shields.io/badge/License-MIT-blue.svg
7
7
 
@@ -23,13 +23,13 @@ A mock smart contract library for testing CoFHE (Confidential Computing Framewor
23
23
  npm
24
24
 
25
25
  ```bash
26
- npm install @fhenixprotocol/cofhe-mock-contracts
26
+ npm install @cofhe/mock-contracts
27
27
  ```
28
28
 
29
29
  foundry
30
30
 
31
31
  ```bash
32
- forge install fhenixprotocol/cofhe-mock-contracts
32
+ forge install FhenixProtocol/cofhesdk
33
33
  ```
34
34
 
35
35
  ## Usages and Integrations
@@ -54,7 +54,7 @@ Both integrations use the same underlying mock contracts, but they differ in **h
54
54
 
55
55
  Use this when you are already using **Hardhat** and/or want to run the **TypeScript SDK (`@cofhe/sdk`)** against a local chain.
56
56
 
57
- - The `cofhesdk/hardhat-plugin` watches Hardhat `node` and `test` tasks.
57
+ - The `@cofhe/hardhat-plugin` watches Hardhat `node` and `test` tasks.
58
58
  - It automatically deploys the mocks to the Hardhat network at fixed addresses.
59
59
  - The `cofheClient` (created with `createCofheClient(...)`) detects the mocks and routes CoFHE actions to them.
60
60
 
@@ -62,7 +62,7 @@ Minimal setup:
62
62
 
63
63
  ```ts
64
64
  // hardhat.config.ts
65
- import 'cofhe-hardhat-plugin';
65
+ import '@cofhe/hardhat-plugin';
66
66
 
67
67
  export default {
68
68
  cofhe: {
@@ -85,14 +85,14 @@ If you want to assert on plaintext values in Hardhat tests, the plugin exposes h
85
85
 
86
86
  Use this when you are writing tests in **Solidity** and running them with `forge test`.
87
87
 
88
- - You typically inherit from the abstract `CoFheTest` helper to deploy/setup the necessary FHE mock environment.
89
- - You use helper methods to create encrypted inputs and assert their underlying values.
88
+ - You inherit from `@cofhe/foundry-plugin`'s abstract `CofheTest` helper to deploy/setup the necessary FHE mock environment.
89
+ - You use `createCofheClient()` and its `createExternal*` helpers to create encrypted inputs, and `expectPlaintext`/`getPlaintext` to assert their underlying values.
90
90
 
91
91
  > **Important**: You must set `isolate = true` in your `foundry.toml`. Without this setting, some variables may be used without proper permission checks, which will cause failures on production chains.
92
92
 
93
- `@cofhe/sdk` is designed to work with mock contracts in a testing / hardhat environment. `cofhesdk/hardhat-plugin` deploys the mock contracts in this repo, and the `cofheClient` detects a testnet chain and interacts correctly using the mocks rather than the true CoFHE coprocessor.
93
+ `@cofhe/sdk` is designed to work with mock contracts in a testing / hardhat environment. `@cofhe/hardhat-plugin` deploys the mock contracts in this repo, and the `cofheClient` detects a testnet chain and interacts correctly using the mocks rather than the true CoFHE coprocessor.
94
94
 
95
- When installed and imported in the `hardhat.config.ts`, `cofhesdk/hardhat-plugin` will watch for Hardhat `node` and `test` tasks, and will deploy the mocks to the hardhat testnet chain at fixed addresses.
95
+ When installed and imported in the `hardhat.config.ts`, `@cofhe/hardhat-plugin` will watch for Hardhat `node` and `test` tasks, and will deploy the mocks to the hardhat testnet chain at fixed addresses.
96
96
 
97
97
  Once deployed, interaction with the mock contracts is handled by the `cofheClient` (created with `createCofheClient(...)`). The client checks for the existence of mock contracts at known addresses, and if they exist, marks the current connection as a testnet.
98
98
 
@@ -108,7 +108,7 @@ The CoFHE coprocessor uses symbolic execution when performing operations on chai
108
108
 
109
109
  FHE operations between one or more `ctHash`es returns a resultant `ctHash`, which is symbolically linked to the true `ciphertext` which includes the encrypted values.
110
110
 
111
- In `cofhe-mock-contracts` the symbolic execution is preserved. In the case of the mocks, the `ciphertext` is not encrypted to be used in the FHE scheme, but is stored as a plaintext value. In this case, the `ctHash` associated with the `ciphertext` is pointing directly at the plaintext value instead.
111
+ In `@cofhe/mock-contracts` the symbolic execution is preserved. In the case of the mocks, the `ciphertext` is not encrypted to be used in the FHE scheme, but is stored as a plaintext value. In this case, the `ctHash` associated with the `ciphertext` is pointing directly at the plaintext value instead.
112
112
 
113
113
  During the execution of a mock FHE operation, say `FHE.add(euint8 ctHashA, euint8 ctHashB) -> euint8 ctHashC`, rather than being performed off-chain by the FHE computation engine, the input `ctHashes` are mapped to their plaintext value, and the operation performed as plaintext math on-chain. The result is inserted into the symbolic value position of `ctHashC`.
114
114
 
@@ -131,52 +131,68 @@ The `MockZkVerifier` contract handles the on-chain storage of encrypted inputs.
131
131
 
132
132
  ### Off-chain Decryption / Sealing
133
133
 
134
- For view-oriented decryption, use the SDK flow `cofheClient.decryptForView(...)` (also exposed as `decryptHandle(...)`) with a valid `ctHash`, the matching FHE type, and a valid `permit` when required.
134
+ For view-oriented decryption, use the SDK flow `cofheClient.decryptForView(...)` (also exposed as `decryptHandle(...)`) with a valid `ctHash`, the matching FHE type, and a valid `acp` when required.
135
135
 
136
136
  When interacting with CoFHE this request is routed to the Threshold Network, which returns the plaintext to the consumer for local use.
137
137
 
138
- When working with the mocks, the `cofheClient` instead queries the `MockThresholdNetwork` contract, which verifies the request `permit` and returns the decrypted result.
138
+ When working with the mocks, the `cofheClient` instead queries the `MockThresholdNetwork` contract, which verifies the request `acp` and returns the decrypted result.
139
139
 
140
140
  ### Using Foundry
141
141
 
142
- Use abstract CoFheTest contract to automatically deploy all necessary FHE contracts for testing.
142
+ Use [`@cofhe/foundry-plugin`](../foundry-plugin/README.md), which builds on these mocks. Inherit its
143
+ abstract `CofheTest` contract and call `deployMocks()` to deploy the full mock stack, then
144
+ `createCofheClient()` for an SDK-like client that encrypts inputs, decrypts outputs, and manages
145
+ acps.
143
146
 
144
- CoFheTest also exposes useful test methods such as
147
+ `CofheTest` exposes useful test helpers such as
145
148
 
146
- - `assertHashValue(euint, uint)` - asserting an encrypted value is equal to an expected plaintext value
147
- - `createInEuint..(number, user)` - for creating encrypted inputs (8-256bits) for a given user
148
- - `createInEuint*_asHashPlusProof(number, user)` - for creating encrypted inputs in the hash plus proof format
149
+ - `expectPlaintext(euint32, uint32)` - asserts an encrypted value equals an expected plaintext value
150
+ - `getPlaintext(euint32)` - reads the plaintext behind an encrypted handle
151
+
152
+ and `CofheClient` exposes
153
+
154
+ - `createExternalEuint32(value, consumingContract)` (and variants for `bool`, `uint8`…`uint128`,
155
+ `address`) - creates an encrypted input bound to the contract that will consume it, returning
156
+ `(externalEuint32 hash, bytes signature)`
157
+ - `createEuint32sBatch(values, consumingContract)` - a whole batch sharing one signature
149
158
 
150
159
  Example:
151
160
 
152
161
  ```solidity
153
- import {Test} from "forge-std/Test.sol";
154
- import {CoFheTest} from "@fhenixprotocol/cofhe-contracts/FHE.sol";
162
+ import {CofheTest} from "@cofhe/foundry-plugin/contracts/CofheTest.sol";
163
+ import {CofheClient} from "@cofhe/foundry-plugin/contracts/CofheClient.sol";
155
164
  import {MyFheContract} from "./MyFheContract.sol";
156
- ...
157
- contract MyFheContractExample is Test, CoFheTest {
165
+
166
+ contract MyFheContractExample is CofheTest {
158
167
 
159
168
  MyFheContract private target;
169
+ CofheClient private client;
160
170
 
161
- address private user = makeAddr("user");
171
+ uint256 private constant USER_PKEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
162
172
 
163
173
  function setUp() public {
164
174
  // optional ... enable verbose logging for fhe mocks
165
- // setLog(true);
175
+ // enableLogs();
166
176
 
177
+ deployMocks();
178
+ client = createCofheClient();
179
+ client.connect(USER_PKEY);
167
180
  target = new MyFheContract();
168
181
  }
169
182
 
170
183
  function testSetValue() public {
171
184
  uint32 n = 10;
172
- InEuint32 memory number = createInEuint32(n, user);
173
185
 
174
- //must be the user who sends transaction
175
- //or else invalid permissions from fhe allow
176
- vm.prank(user);
177
- target.setValue(number);
186
+ // The last argument binds the input to the contract that will consume it.
187
+ (externalEuint32 hash, bytes memory signature) = client.createExternalEuint32(n, address(target));
188
+ externalEuint32[] memory hashes = new externalEuint32[](1);
189
+ hashes[0] = hash;
190
+
191
+ // Must be the account that encrypted the input, or FHE.allow permissions will be invalid.
192
+ vm.prank(client.account());
193
+ target.setValueBatch(hashes, signature);
178
194
 
179
- assertHashValue(target.getValue(), n);
195
+ expectPlaintext(target.getValue(), n);
180
196
  }
181
197
  }
182
198
  ```
@@ -27,7 +27,7 @@ contract ABITest {
27
27
 
28
28
  struct ContainsEncryptedInput {
29
29
  uint256 value;
30
- InEuint32 encryptedInput;
30
+ externalEuint32 encryptedInput;
31
31
  }
32
32
 
33
33
  struct ContainsEncryptedResult {
@@ -36,28 +36,41 @@ contract ABITest {
36
36
  }
37
37
 
38
38
  // INPUTS
39
+ //
40
+ // Encrypted inputs are `external*` ciphertext handles (plain bytes32-based value types).
41
+ // Every function that takes one or more `external*` params must end with a trailing
42
+ // `bytes` parameter - the single signature authenticating the whole batch of handles
43
+ // passed to that call.
39
44
 
40
45
  function fnNoEncryptedInputs(uint8 value) public {}
41
46
 
42
- function fnEncryptedInput(InEuint32 memory inNumber) public {}
47
+ function fnEncryptedInput(externalEuint32 inNumber, bytes memory inputProof) public {}
43
48
 
44
- function fnBlendedInputsIncludingEncryptedInput(uint256 value, InEuint32 memory inNumber) public {}
49
+ function fnBlendedInputsIncludingEncryptedInput(
50
+ uint256 value,
51
+ externalEuint32 inNumber,
52
+ bytes memory inputProof
53
+ ) public {}
45
54
 
46
55
  function fnAllEncryptedInputs(
47
- InEuint8 memory inEuint8,
48
- InEuint16 memory inEuint16,
49
- InEuint32 memory inEuint32,
50
- InEuint64 memory inEuint64,
51
- InEuint128 memory inEuint128,
52
- InEbool memory inEbool,
53
- InEaddress memory inEaddress
56
+ externalEuint8 inEuint8,
57
+ externalEuint16 inEuint16,
58
+ externalEuint32 inEuint32,
59
+ externalEuint64 inEuint64,
60
+ externalEuint128 inEuint128,
61
+ externalEbool inEbool,
62
+ externalEaddress inEaddress,
63
+ bytes memory inputProof
54
64
  ) public {}
55
65
 
56
- function fnStructContainsEncryptedInput(ContainsEncryptedInput memory containsEncryptedInput) public {}
66
+ function fnStructContainsEncryptedInput(
67
+ ContainsEncryptedInput memory containsEncryptedInput,
68
+ bytes memory inputProof
69
+ ) public {}
57
70
 
58
- function fnArrayContainsEncryptedInput(InEuint32[] memory inEuint32Array) public {}
71
+ function fnArrayContainsEncryptedInput(externalEuint32[] memory inEuint32Array, bytes memory inputProof) public {}
59
72
 
60
- function fnTupleContainsEncryptedInput(InEuint32[2] memory inEuint32Array) public {}
73
+ function fnTupleContainsEncryptedInput(externalEuint32[2] memory inEuint32Array, bytes memory inputProof) public {}
61
74
 
62
75
  // OUTPUTS
63
76
 
@@ -0,0 +1,145 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity >=0.8.19 <0.9.0;
3
+
4
+ import { EnumerableSet } from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol';
5
+ import { ACP, IPermissionCustomIdValidator } from './Permissioned.sol';
6
+
7
+ /**
8
+ * @notice On-chain hand-off for sharing ACPs — replaces the JSON copy-paste flow.
9
+ *
10
+ * An issuer posts a sharing ACP addressed to a recipient; the recipient reads it
11
+ * from here, fills in their sealing key, signs, and imports it client-side. Shares
12
+ * are indexed globally per recipient — a share is addressed to a person, and any
13
+ * cofhesdk-enabled app may surface it.
14
+ *
15
+ * The registry stores the payload verbatim and stays deliberately dumb:
16
+ *
17
+ * - The posted ACP carries `sealingKey = 0` and `recipientSignature = ""` — the
18
+ * recipient supplies both at import, exactly as in the off-chain flow.
19
+ * - No signature verification on-chain: a share's full validity cannot be checked
20
+ * before the recipient signs, and the SDK validates everything at import. The
21
+ * trust the registry adds is `msg.sender == acp.issuer` — a share listed under
22
+ * a recipient was genuinely posted by its claimed issuer.
23
+ * - `isShareValid` is the verification hook for other contracts: share exists,
24
+ * is unexpired, and is not revoked (per the share's own revoker contract).
25
+ *
26
+ * Nothing stored here is sensitive: every field is part of the cleartext share
27
+ * payload by design. Posting on-chain does make the issuer→recipient sharing
28
+ * relationship public. (A future variant may accept an encrypted payload as a
29
+ * parallel entry type; this registry's cleartext entries would be unaffected.)
30
+ *
31
+ * The production counterpart is upgradeable (OZ AccessControl for the upgrader
32
+ * role) and deployed to a fixed address as core infrastructure.
33
+ */
34
+ contract ACPShareRegistry {
35
+ using EnumerableSet for EnumerableSet.Bytes32Set;
36
+
37
+ /// @notice recipient => ids of shares addressed to them
38
+ mapping(address => EnumerableSet.Bytes32Set) private _shareIdsFor;
39
+
40
+ /// @notice share id => stored payload
41
+ mapping(bytes32 => ACP) private _shares;
42
+
43
+ event Shared(address indexed recipient, address indexed issuer, bytes32 shareId);
44
+ event ShareRemoved(address indexed recipient, address indexed issuer, bytes32 shareId);
45
+
46
+ error NotIssuer();
47
+ error NotIssuerOrRecipient();
48
+ error RecipientMissing();
49
+ error SealingKeyMustBeEmpty();
50
+ error IssuerSignatureMissing();
51
+ error ShareExpired();
52
+ error AlreadyShared();
53
+ error UnknownShare();
54
+
55
+ /// @notice Post a sharing ACP for its recipient to pick up.
56
+ /// @dev The share id is the hash of the payload — reposting an identical share reverts.
57
+ function share(ACP calldata acp) external returns (bytes32 shareId) {
58
+ if (msg.sender != acp.issuer) revert NotIssuer();
59
+ if (acp.recipient == address(0)) revert RecipientMissing();
60
+ if (acp.sealingKey != bytes32(0)) revert SealingKeyMustBeEmpty();
61
+ if (acp.issuerSignature.length == 0) revert IssuerSignatureMissing();
62
+ if (acp.expiration < block.timestamp) revert ShareExpired();
63
+
64
+ shareId = keccak256(abi.encode(acp));
65
+ // the id commits to the recipient, so a duplicate can only be in this set
66
+ if (!_shareIdsFor[acp.recipient].add(shareId)) revert AlreadyShared();
67
+ _shares[shareId] = acp;
68
+
69
+ emit Shared(acp.recipient, acp.issuer, shareId);
70
+ }
71
+
72
+ /// @notice Remove a share. The issuer may retract it; the recipient may dismiss it
73
+ /// (e.g. after importing, or to decline).
74
+ function removeShare(bytes32 shareId) external {
75
+ ACP storage acp = _shares[shareId];
76
+ if (acp.issuer == address(0)) revert UnknownShare();
77
+ if (msg.sender != acp.issuer && msg.sender != acp.recipient) revert NotIssuerOrRecipient();
78
+
79
+ address recipient = acp.recipient;
80
+ address issuer = acp.issuer;
81
+
82
+ // the id set and the payload map stay in sync — mirror share()'s add() handling
83
+ if (!_shareIdsFor[recipient].remove(shareId)) revert UnknownShare();
84
+ delete _shares[shareId];
85
+
86
+ emit ShareRemoved(recipient, issuer, shareId);
87
+ }
88
+
89
+ /// @notice All importable shares addressed to `recipient`: unexpired and not revoked.
90
+ /// Dead entries stay in storage until removed but are filtered here.
91
+ function sharesFor(address recipient) external view returns (ACP[] memory acps) {
92
+ EnumerableSet.Bytes32Set storage ids = _shareIdsFor[recipient];
93
+ uint256 len = ids.length();
94
+ if (len == 0) return acps;
95
+
96
+ // single pass: allocate for the maximum, fill with valid shares only
97
+ acps = new ACP[](len);
98
+ uint256 live = 0;
99
+ for (uint256 i = 0; i < len; i++) {
100
+ ACP storage acp = _shares[ids.at(i)];
101
+ if (_isValid(acp)) {
102
+ acps[live] = acp;
103
+ live++;
104
+ }
105
+ }
106
+
107
+ // truncate the memory array's length to the live count (shrink-only)
108
+ if (live < len) {
109
+ assembly {
110
+ mstore(acps, live)
111
+ }
112
+ }
113
+ }
114
+
115
+ /// @notice A single share by id (zeroed struct if unknown/removed).
116
+ function getShare(bytes32 shareId) external view returns (ACP memory) {
117
+ return _shares[shareId];
118
+ }
119
+
120
+ /// @notice Verification hook for contracts: the share exists, was posted by its
121
+ /// claimed issuer (guaranteed at posting), is unexpired, and is not
122
+ /// revoked per its own revoker contract.
123
+ function isShareValid(bytes32 shareId) external view returns (bool) {
124
+ if (_shares[shareId].issuer == address(0)) return false;
125
+ return _isValid(_shares[shareId]);
126
+ }
127
+
128
+ /// @dev Unexpired and not revoked. The revoker call mirrors `withPermission`'s
129
+ /// revocation clause; a reverting revoker fails closed (share invalid).
130
+ function _isValid(ACP storage acp) private view returns (bool) {
131
+ if (acp.expiration < block.timestamp) return false;
132
+
133
+ if (acp.revokerData != 0 && acp.revokerContract != address(0)) {
134
+ try IPermissionCustomIdValidator(acp.revokerContract).disabled(acp.issuer, acp.revokerData) returns (
135
+ bool disabled
136
+ ) {
137
+ if (disabled) return false;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+
143
+ return true;
144
+ }
145
+ }
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity >=0.8.19 <0.9.0;
3
+
4
+ import { IPermissionCustomIdValidator } from './Permissioned.sol';
5
+
6
+ /**
7
+ * @notice Default ACP revoker — timestamp-based revocation.
8
+ *
9
+ * Implements the unchanged revoker interface. Interprets a permission's
10
+ * `revokerData` as its creation timestamp (populated by the SDK at acp
11
+ * creation). Every SDK-created acp points here by default: no upfront
12
+ * contract call is needed — an ACP is revocable from birth.
13
+ *
14
+ * `disabled(issuer, id)` truth table:
15
+ *
16
+ * | condition | result | meaning |
17
+ * |-----------------------------|--------|----------------------------------|
18
+ * | id > block.timestamp | true | future-dated acps never |
19
+ * | | | validate (closes the revoke-all |
20
+ * | | | dodge: an ACP dated ahead of a|
21
+ * | | | mass revocation would survive it)|
22
+ * | id <= revokeAllAt[issuer] | true | mass revocation (inclusive) |
23
+ * | revokedSingle[issuer][id] | true | targeted revocation |
24
+ * | otherwise | false | acp valid |
25
+ *
26
+ * Accepted trade-offs (by design, see ACPV3 spec discussion):
27
+ * - Two acps minted by the same issuer in the same second share an id;
28
+ * revoking one revokes both. Over-revocation is the fail-safe direction.
29
+ * - Revocation is permanent — there is no un-revoke.
30
+ *
31
+ * The production counterpart is upgradeable (OZ AccessControl for the
32
+ * upgrader role) and deployed to a fixed address as core infrastructure.
33
+ */
34
+ contract ACPTimestampRevoker is IPermissionCustomIdValidator {
35
+ /// @notice issuer => threshold; acps with id (creation ts) at or before this are revoked
36
+ mapping(address => uint256) public revokeAllAt;
37
+
38
+ /// @notice issuer => id => revoked
39
+ mapping(address => mapping(uint256 => bool)) public revokedSingle;
40
+
41
+ event RevokedSingle(address indexed issuer, uint256 indexed id);
42
+ event RevokedAll(address indexed issuer, uint256 at);
43
+
44
+ /// @notice Revoke a single acp by its id (creation timestamp).
45
+ /// Only affects acps issued by the caller.
46
+ function revokeSingle(uint256 id) external {
47
+ revokedSingle[msg.sender][id] = true;
48
+ emit RevokedSingle(msg.sender, id);
49
+ }
50
+
51
+ /// @notice Revoke every acp the caller created up to now.
52
+ /// O(1): a single threshold write, regardless of acp count.
53
+ function revokeAllExisting() external {
54
+ revokeAllAt[msg.sender] = block.timestamp;
55
+ emit RevokedAll(msg.sender, block.timestamp);
56
+ }
57
+
58
+ /// @dev Called by the ACL during permission validation.
59
+ function disabled(address issuer, uint256 id) external view returns (bool) {
60
+ return id > block.timestamp || id <= revokeAllAt[issuer] || revokedSingle[issuer][id];
61
+ }
62
+ }