@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 +38 -0
- package/README.md +46 -30
- package/contracts/ABITest.sol +26 -13
- package/contracts/ACPShareRegistry.sol +145 -0
- package/contracts/ACPTimestampRevoker.sol +62 -0
- package/contracts/MockACL.sol +199 -20
- package/contracts/MockTaskManager.sol +90 -17
- package/contracts/MockThresholdNetwork.sol +19 -19
- package/contracts/Permissioned.sol +85 -124
- package/dist/index.d.mts +1023 -128
- package/dist/index.d.ts +1023 -128
- package/dist/index.js +868 -81
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +867 -82
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/ACPShareRegistry.ts +359 -0
- package/src/ACPTimestampRevoker.ts +134 -0
- package/src/MockACL.ts +207 -9
- package/src/MockTaskManager.ts +101 -50
- package/src/MockThresholdNetwork.ts +59 -14
- package/src/index.ts +10 -1
- package/src/typechain-types/ACPShareRegistry.ts +189 -0
- package/src/typechain-types/ACPTimestampRevoker.ts +157 -0
- package/src/typechain-types/MockACL.ts +151 -19
- package/src/typechain-types/MockTaskManager.ts +67 -33
- package/src/typechain-types/MockThresholdNetwork.ts +35 -29
- package/src/typechain-types/index.ts +2 -0
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/@
|
|
4
|
-
[npm-badge]: https://img.shields.io/npm/v/@
|
|
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 @
|
|
26
|
+
npm install @cofhe/mock-contracts
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
foundry
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
forge install
|
|
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
|
|
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
|
|
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
|
|
89
|
-
- You use
|
|
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.
|
|
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`,
|
|
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
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
-
|
|
147
|
+
`CofheTest` exposes useful test helpers such as
|
|
145
148
|
|
|
146
|
-
- `
|
|
147
|
-
- `
|
|
148
|
-
|
|
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 {
|
|
154
|
-
import {
|
|
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
|
|
165
|
+
|
|
166
|
+
contract MyFheContractExample is CofheTest {
|
|
158
167
|
|
|
159
168
|
MyFheContract private target;
|
|
169
|
+
CofheClient private client;
|
|
160
170
|
|
|
161
|
-
|
|
171
|
+
uint256 private constant USER_PKEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
|
|
162
172
|
|
|
163
173
|
function setUp() public {
|
|
164
174
|
// optional ... enable verbose logging for fhe mocks
|
|
165
|
-
//
|
|
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
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
195
|
+
expectPlaintext(target.getValue(), n);
|
|
180
196
|
}
|
|
181
197
|
}
|
|
182
198
|
```
|
package/contracts/ABITest.sol
CHANGED
|
@@ -27,7 +27,7 @@ contract ABITest {
|
|
|
27
27
|
|
|
28
28
|
struct ContainsEncryptedInput {
|
|
29
29
|
uint256 value;
|
|
30
|
-
|
|
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(
|
|
47
|
+
function fnEncryptedInput(externalEuint32 inNumber, bytes memory inputProof) public {}
|
|
43
48
|
|
|
44
|
-
function fnBlendedInputsIncludingEncryptedInput(
|
|
49
|
+
function fnBlendedInputsIncludingEncryptedInput(
|
|
50
|
+
uint256 value,
|
|
51
|
+
externalEuint32 inNumber,
|
|
52
|
+
bytes memory inputProof
|
|
53
|
+
) public {}
|
|
45
54
|
|
|
46
55
|
function fnAllEncryptedInputs(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
66
|
+
function fnStructContainsEncryptedInput(
|
|
67
|
+
ContainsEncryptedInput memory containsEncryptedInput,
|
|
68
|
+
bytes memory inputProof
|
|
69
|
+
) public {}
|
|
57
70
|
|
|
58
|
-
function fnArrayContainsEncryptedInput(
|
|
71
|
+
function fnArrayContainsEncryptedInput(externalEuint32[] memory inEuint32Array, bytes memory inputProof) public {}
|
|
59
72
|
|
|
60
|
-
function fnTupleContainsEncryptedInput(
|
|
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
|
+
}
|