@cofhe/mock-contracts 0.7.1 → 1.0.0-alpha.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 +12 -0
- package/README.md +10 -0
- package/contracts/ACPShareRegistry.sol +227 -114
- package/contracts/ACPTimestampRevoker.sol +36 -34
- package/contracts/MockACL.sol +35 -16
- package/contracts/MockCoFHE.sol +201 -86
- package/contracts/MockCoFHEAddressBook.sol +64 -0
- package/contracts/MockERC1967Proxy.sol +13 -0
- package/contracts/MockPermissioned.sol +16 -0
- package/contracts/MockTaskManager.sol +143 -28
- package/contracts/MockThresholdNetwork.sol +5 -5
- package/contracts/MockZkVerifier.sol +2 -2
- package/contracts/Permissioned.sol +240 -143
- package/contracts/cofhe-contracts.json +10 -0
- package/dist/index.d.mts +980 -101
- package/dist/index.d.ts +980 -101
- package/dist/index.js +817 -118
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +816 -119
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -6
- package/remappings.txt +1 -0
- package/src/ACPShareRegistry.ts +469 -56
- package/src/MockACL.ts +56 -25
- package/src/MockCoFHEAddressBook.ts +114 -0
- package/src/MockERC1967Proxy.ts +74 -0
- package/src/MockTaskManager.ts +73 -6
- package/src/index.ts +4 -0
- package/src/typechain-types/ACPShareRegistry.ts +309 -12
- package/src/typechain-types/MockACL.ts +65 -6
- package/src/typechain-types/MockCoFHEAddressBook.ts +124 -0
- package/src/typechain-types/MockERC1967Proxy.ts +85 -0
- package/src/typechain-types/MockTaskManager.ts +66 -1
- package/src/typechain-types/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @cofhe/mock-contracts Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6f7a4c5: Resolve the Task Manager through the `CoFHEAddressBook` (`@fhenixprotocol/cofhe-contracts@1.0.0`). **Breaking:** `TASK_MANAGER_ADDRESS` is removed from `@cofhe/sdk`. Use `getTaskManagerAddress(publicClient)`, which reads `getTm(TASK_MANAGER_ID)` from the book at `COFHE_ADDRESS_BOOK_ADDRESS` (both exported) and caches the result per chain. If the chain has no book, or the id is unset, it throws `TASK_MANAGER_UNRESOLVED`. `verifyDecryptResult`, the ACL/ACP lookups and `useCofheEnabled` / `useCofheReadDecryptionResults` now resolve the Task Manager this way. `@cofhe/react` adds `useCofheTaskManagerAddress()`.
|
|
8
|
+
|
|
9
|
+
Mocks: a new `MockCoFHEAddressBook` is deployed at FHE.sol's `COFHE_ADDRESS_BOOK`, and `MockTaskManager` moves to `MOCKS_TASK_MANAGER_ADDRESS` (`0x…5000`) and is registered in the book. `MockACL` now stores its Task Manager (`setTaskManager` / `getTaskManagerAddress`) instead of reading a constant, so its `TASK_MANAGER_ADDRESS_()` getter is gone. All three plugins deploy and wire the book. Hardhat 2 exposes `hre.cofhe.mocks.getMockCoFHEAddressBook()`, Hardhat 3 exposes `conn.cofhe.mocks.MockCoFHEAddressBook`, and Foundry exposes `CofheTest.mockAddressBook`. Contracts must be built against `@fhenixprotocol/cofhe-contracts@1.0.0` or later (the hardhat plugin's peer range is now `>=1.0.0`). Each FHE op now makes one extra `getTm` call on the book, as it does on real networks, so expect slightly higher gas numbers.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- ccb7b68: Realistic gas reporting for mocks. Under Foundry, mock-only work is now excluded from gas metering by default (opt out with `mockTaskManager.setMockGasExcluded(false)`), so expect `forge snapshot` numbers to drop. Under Hardhat, mock transactions emit `MockGasConsumed` events (extra receipt logs), and the plugins add `getAdjustedGasUsed(receipt)` / `getAdjustedGasBreakdown(receipt)` plus an opt-in `cofhe.gasSummary` report.
|
|
14
|
+
|
|
3
15
|
## 0.7.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A mock smart contract library for testing CoFHE (Confidential Computing Framewor
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
12
|
- Mock implementations of core CoFHE contracts:
|
|
13
|
+
- MockCoFHEAddressBook (deployed at FHE.sol's `COFHE_ADDRESS_BOOK`, resolves the TaskManager)
|
|
13
14
|
- MockTaskManager
|
|
14
15
|
- MockThresholdNetwork
|
|
15
16
|
- MockZkVerifier
|
|
@@ -137,6 +138,15 @@ When interacting with CoFHE this request is routed to the Threshold Network, whi
|
|
|
137
138
|
|
|
138
139
|
When working with the mocks, the `cofheClient` instead queries the `MockThresholdNetwork` contract, which verifies the request `acp` and returns the decrypted result.
|
|
139
140
|
|
|
141
|
+
### Gas
|
|
142
|
+
|
|
143
|
+
Because the mocks replicate the off-chain FHE computation on-chain (plaintext math, mock storage writes, logging), transactions consume more gas than they would on a real CoFHE network. The mocks account for this in two ways:
|
|
144
|
+
|
|
145
|
+
- **Foundry**: when gas exclusion is enabled (`setMockGasExcluded(true)` — the foundry plugin's `deployMocks()` does this automatically), the mock-only work runs between `pauseGasMetering` / `resumeGasMetering` cheatcodes, so forge's reported gas approximates real-network cost. Requires cheatcode access (`vm.allowCheatcodes(taskManagerAddress)`) and forge ≥ 1.0; without either, execution silently stays metered.
|
|
146
|
+
- **Everywhere else** (Hardhat, anvil): `MockTaskManager` measures each block of mock-only work with `gasleft()` and emits a `MockGasConsumed(uint256)` event. Summing these events from a receipt and subtracting from `gasUsed` yields the adjusted cost — the hardhat plugins expose this as `getAdjustedGasUsed(receipt)` / `getAdjustedGasBreakdown(receipt)` and an opt-in `gasSummary` table.
|
|
147
|
+
|
|
148
|
+
`eth_estimateGas` is never adjusted: the mock work really executes, so transactions still need the raw gas limit.
|
|
149
|
+
|
|
140
150
|
### Using Foundry
|
|
141
151
|
|
|
142
152
|
Use [`@cofhe/foundry-plugin`](../foundry-plugin/README.md), which builds on these mocks. Inherit its
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity >=0.8.
|
|
2
|
+
pragma solidity >=0.8.25 <0.9.0;
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
|
5
|
+
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
|
6
|
+
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
|
|
7
|
+
import {ACP, IPermissionCustomIdValidator} from "./Permissioned.sol";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* @notice On-chain hand-off for sharing ACPs — replaces the JSON copy-paste flow.
|
|
@@ -12,7 +14,15 @@ import { ACP, IPermissionCustomIdValidator } from './Permissioned.sol';
|
|
|
12
14
|
* are indexed globally per recipient — a share is addressed to a person, and any
|
|
13
15
|
* cofhesdk-enabled app may surface it.
|
|
14
16
|
*
|
|
15
|
-
*
|
|
17
|
+
* Pointer-based: the full ACP and an optional metadata blob travel in the `Shared`
|
|
18
|
+
* event; storage keeps only the share header — the fields this contract checks
|
|
19
|
+
* (issuer, recipient, expiration, revoker) and the block of that event — plus the
|
|
20
|
+
* recipient's set of share ids. A reader takes the header from `sharesFor` / `getShare`
|
|
21
|
+
* and fetches the event with a `getLogs` over that one block, filtered by the
|
|
22
|
+
* share id topic. If that block holds several `Shared` events for the share id (it
|
|
23
|
+
* was removed and posted again within the block), the last one is the share.
|
|
24
|
+
*
|
|
25
|
+
* The registry stays deliberately dumb:
|
|
16
26
|
*
|
|
17
27
|
* - The posted ACP carries `sealingKey = 0` and `recipientSignature = ""` — the
|
|
18
28
|
* recipient supplies both at import, exactly as in the off-chain flow.
|
|
@@ -20,126 +30,229 @@ import { ACP, IPermissionCustomIdValidator } from './Permissioned.sol';
|
|
|
20
30
|
* before the recipient signs, and the SDK validates everything at import. The
|
|
21
31
|
* trust the registry adds is `msg.sender == acp.issuer` — a share listed under
|
|
22
32
|
* a recipient was genuinely posted by its claimed issuer.
|
|
33
|
+
* - The metadata is opaque bytes, never interpreted here (the SDK defines the
|
|
34
|
+
* format: labels saying where each ctHash of a SNAPSHOT share came from). It is
|
|
35
|
+
* written once, with the share, and cannot be replaced.
|
|
23
36
|
* - `isShareValid` is the verification hook for other contracts: share exists,
|
|
24
37
|
* is unexpired, and is not revoked (per the share's own revoker contract).
|
|
25
38
|
*
|
|
26
|
-
* Nothing
|
|
39
|
+
* Nothing posted here is sensitive: every field is part of the cleartext share
|
|
27
40
|
* payload by design. Posting on-chain does make the issuer→recipient sharing
|
|
28
41
|
* relationship public. (A future variant may accept an encrypted payload as a
|
|
29
42
|
* 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
43
|
*/
|
|
34
|
-
contract ACPShareRegistry {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
}
|
|
44
|
+
contract ACPShareRegistry is UUPSUpgradeable, AccessControlUpgradeable {
|
|
45
|
+
using EnumerableSet for EnumerableSet.Bytes32Set;
|
|
46
|
+
|
|
47
|
+
/// @notice Role allowed to upgrade the implementation.
|
|
48
|
+
bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
|
|
49
|
+
|
|
50
|
+
/// @notice What storage keeps of a share: the fields `removeShare` and the validity
|
|
51
|
+
/// check read, and where to find the rest.
|
|
52
|
+
/// @dev Packed into four slots; the two revoker slots stay zero for a share without
|
|
53
|
+
/// a revoker.
|
|
54
|
+
struct ShareHeader {
|
|
55
|
+
address issuer;
|
|
56
|
+
uint64 expiration;
|
|
57
|
+
address recipient;
|
|
58
|
+
/// @dev Block of the `Shared` event that carries the full ACP and the metadata
|
|
59
|
+
/// (the L2 block on Arbitrum). The last `Shared` event for the share id in that
|
|
60
|
+
/// block, should it hold more than one.
|
|
61
|
+
uint64 blockNumber;
|
|
62
|
+
address revokerContract;
|
|
63
|
+
uint256 revokerData;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// @custom:storage-location erc7201:cofhe.storage.ACPShareRegistry.v2
|
|
67
|
+
struct ACPShareRegistryStorage {
|
|
68
|
+
/// @dev recipient => ids of shares addressed to them
|
|
69
|
+
mapping(address => EnumerableSet.Bytes32Set) shareIdsFor;
|
|
70
|
+
/// @dev share id => stored header
|
|
71
|
+
mapping(bytes32 => ShareHeader) headers;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// @dev keccak256(abi.encode(uint256(keccak256("cofhe.storage.ACPShareRegistry.v2")) - 1)) & ~bytes32(uint256(0xff))
|
|
75
|
+
bytes32 private constant ACP_SHARE_REGISTRY_SLOT =
|
|
76
|
+
keccak256(abi.encode(uint256(keccak256("cofhe.storage.ACPShareRegistry.v2")) - 1)) & ~bytes32(uint256(0xff));
|
|
77
|
+
|
|
78
|
+
/// @notice The first version's storage, which kept every share whole. Abandoned by the
|
|
79
|
+
/// in-place upgrade: nothing reads it, and its shares are not carried over. Declared
|
|
80
|
+
/// only so the namespace stays reserved (the OpenZeppelin layout check requires it).
|
|
81
|
+
/// @custom:storage-location erc7201:cofhe.storage.ACPShareRegistry
|
|
82
|
+
struct ACPShareRegistryStorageV1 {
|
|
83
|
+
/// @dev recipient => ids of shares addressed to them
|
|
84
|
+
mapping(address => EnumerableSet.Bytes32Set) shareIdsFor;
|
|
85
|
+
/// @dev share id => stored payload
|
|
86
|
+
mapping(bytes32 => ACP) shares;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// @notice A share was posted. `acp` is the payload as posted; `metadata` is the opaque
|
|
90
|
+
/// blob that came with it (empty when none). The share id is
|
|
91
|
+
/// `keccak256(abi.encode(acp))`. A share removed and posted again in the same block
|
|
92
|
+
/// emits this twice there with the same id; the later one (higher log index) is
|
|
93
|
+
/// the share, and only its `metadata` can differ.
|
|
94
|
+
event Shared(address indexed recipient, address indexed issuer, bytes32 indexed shareId, ACP acp, bytes metadata);
|
|
95
|
+
event ShareRemoved(address indexed recipient, address indexed issuer, bytes32 indexed shareId);
|
|
96
|
+
|
|
97
|
+
error NotIssuer();
|
|
98
|
+
error NotIssuerOrRecipient();
|
|
99
|
+
error RecipientMissing();
|
|
100
|
+
error SealingKeyMustBeEmpty();
|
|
101
|
+
error IssuerSignatureMissing();
|
|
102
|
+
error ShareExpired();
|
|
103
|
+
error AlreadyShared();
|
|
104
|
+
error UnknownShare();
|
|
105
|
+
|
|
106
|
+
/// @custom:oz-upgrades-unsafe-allow constructor
|
|
107
|
+
constructor() {
|
|
108
|
+
_disableInitializers();
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
/**
|
|
112
|
+
* @notice Initializes the contract.
|
|
113
|
+
* @param initialAdmin Receives DEFAULT_ADMIN_ROLE and UPGRADER_ROLE.
|
|
114
|
+
*/
|
|
115
|
+
function initialize(address initialAdmin) public initializer {
|
|
116
|
+
__AccessControl_init();
|
|
117
|
+
_grantRole(DEFAULT_ADMIN_ROLE, initialAdmin);
|
|
118
|
+
_grantRole(UPGRADER_ROLE, initialAdmin);
|
|
112
119
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
120
|
+
|
|
121
|
+
/// @notice Post a sharing ACP for its recipient to pick up, with an optional metadata
|
|
122
|
+
/// blob (`""` for none).
|
|
123
|
+
/// @dev The share id is the hash of the payload — reposting an identical share reverts,
|
|
124
|
+
/// whatever its metadata.
|
|
125
|
+
function share(ACP calldata acp, bytes calldata metadata) external returns (bytes32 shareId) {
|
|
126
|
+
if (msg.sender != acp.issuer) revert NotIssuer();
|
|
127
|
+
if (acp.recipient == address(0)) revert RecipientMissing();
|
|
128
|
+
if (acp.sealingKey != bytes32(0)) revert SealingKeyMustBeEmpty();
|
|
129
|
+
if (acp.issuerSignature.length == 0) revert IssuerSignatureMissing();
|
|
130
|
+
if (acp.expiration < block.timestamp) revert ShareExpired();
|
|
131
|
+
|
|
132
|
+
ACPShareRegistryStorage storage $ = _getStorage();
|
|
133
|
+
|
|
134
|
+
shareId = keccak256(abi.encode(acp));
|
|
135
|
+
// the id commits to the recipient, so a duplicate can only be in this set
|
|
136
|
+
if (!$.shareIdsFor[acp.recipient].add(shareId)) revert AlreadyShared();
|
|
137
|
+
$.headers[shareId] = ShareHeader({
|
|
138
|
+
issuer: acp.issuer,
|
|
139
|
+
expiration: acp.expiration,
|
|
140
|
+
recipient: acp.recipient,
|
|
141
|
+
blockNumber: uint64(_blockNumber()),
|
|
142
|
+
revokerContract: acp.revokerContract,
|
|
143
|
+
revokerData: acp.revokerData
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
emit Shared(acp.recipient, acp.issuer, shareId, acp, metadata);
|
|
141
147
|
}
|
|
142
148
|
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
/// @notice Remove a share. The issuer may retract it; the recipient may dismiss it
|
|
150
|
+
/// (e.g. after importing, or to decline). The `Shared` event stays in its block;
|
|
151
|
+
/// readers go by the header, which is gone.
|
|
152
|
+
function removeShare(bytes32 shareId) external {
|
|
153
|
+
ACPShareRegistryStorage storage $ = _getStorage();
|
|
154
|
+
|
|
155
|
+
ShareHeader storage header = $.headers[shareId];
|
|
156
|
+
if (header.issuer == address(0)) revert UnknownShare();
|
|
157
|
+
if (msg.sender != header.issuer && msg.sender != header.recipient) revert NotIssuerOrRecipient();
|
|
158
|
+
|
|
159
|
+
address recipient = header.recipient;
|
|
160
|
+
address issuer = header.issuer;
|
|
161
|
+
|
|
162
|
+
// the id set and the header map stay in sync — mirror share()'s add() handling
|
|
163
|
+
if (!$.shareIdsFor[recipient].remove(shareId)) revert UnknownShare();
|
|
164
|
+
delete $.headers[shareId];
|
|
165
|
+
|
|
166
|
+
emit ShareRemoved(recipient, issuer, shareId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// @notice The importable shares addressed to `recipient` — unexpired and not revoked —
|
|
170
|
+
/// as their ids and headers, index for index. Dead entries stay in storage until
|
|
171
|
+
/// removed but are filtered here.
|
|
172
|
+
function sharesFor(
|
|
173
|
+
address recipient
|
|
174
|
+
) external view returns (bytes32[] memory shareIds, ShareHeader[] memory headers) {
|
|
175
|
+
ACPShareRegistryStorage storage $ = _getStorage();
|
|
176
|
+
EnumerableSet.Bytes32Set storage ids = $.shareIdsFor[recipient];
|
|
177
|
+
uint256 len = ids.length();
|
|
178
|
+
|
|
179
|
+
// single pass: allocate for the maximum, fill with valid shares only
|
|
180
|
+
shareIds = new bytes32[](len);
|
|
181
|
+
headers = new ShareHeader[](len);
|
|
182
|
+
uint256 live = 0;
|
|
183
|
+
for (uint256 i = 0; i < len; i++) {
|
|
184
|
+
bytes32 shareId = ids.at(i);
|
|
185
|
+
ShareHeader storage header = $.headers[shareId];
|
|
186
|
+
if (_isValid(header)) {
|
|
187
|
+
shareIds[live] = shareId;
|
|
188
|
+
headers[live] = header;
|
|
189
|
+
live++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// truncate the memory arrays' length to the live count (shrink-only)
|
|
194
|
+
if (live < len) {
|
|
195
|
+
assembly {
|
|
196
|
+
mstore(shareIds, live)
|
|
197
|
+
mstore(headers, live)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// @notice The header of a single share (zeroed struct if unknown/removed).
|
|
203
|
+
function getShare(bytes32 shareId) external view returns (ShareHeader memory) {
|
|
204
|
+
return _getStorage().headers[shareId];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// @notice Verification hook for contracts: the share exists, was posted by its
|
|
208
|
+
/// claimed issuer (guaranteed at posting), is unexpired, and is not
|
|
209
|
+
/// revoked per its own revoker contract.
|
|
210
|
+
function isShareValid(bytes32 shareId) external view returns (bool) {
|
|
211
|
+
ShareHeader storage header = _getStorage().headers[shareId];
|
|
212
|
+
if (header.issuer == address(0)) return false;
|
|
213
|
+
return _isValid(header);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// @dev Unexpired and not revoked. The revoker call mirrors `withPermission`'s
|
|
217
|
+
/// revocation clause; a reverting revoker fails closed (share invalid).
|
|
218
|
+
function _isValid(ShareHeader storage header) private view returns (bool) {
|
|
219
|
+
if (header.expiration < block.timestamp) return false;
|
|
220
|
+
|
|
221
|
+
if (header.revokerData != 0 && header.revokerContract != address(0)) {
|
|
222
|
+
try IPermissionCustomIdValidator(header.revokerContract).disabled(header.issuer, header.revokerData) returns (
|
|
223
|
+
bool disabled
|
|
224
|
+
) {
|
|
225
|
+
if (disabled) return false;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// @dev The number a `getLogs` query takes for the current block. On Arbitrum `block.number`
|
|
235
|
+
/// is an L1 block estimate and the L2 block comes from the ArbSys precompile; elsewhere
|
|
236
|
+
/// nothing answers at that address and `block.number` is the block.
|
|
237
|
+
function _blockNumber() private view returns (uint256) {
|
|
238
|
+
(bool ok, bytes memory result) = address(100).staticcall(abi.encodeWithSignature("arbBlockNumber()"));
|
|
239
|
+
return ok && result.length == 32 ? abi.decode(result, (uint256)) : block.number;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @dev Should revert when `msg.sender` is not authorized to upgrade the contract.
|
|
244
|
+
* Empty implementation since authorization is handled by the role check.
|
|
245
|
+
*/
|
|
246
|
+
/* solhint-disable-next-line no-empty-blocks */
|
|
247
|
+
function _authorizeUpgrade(address _newImplementation) internal virtual override onlyRole(UPGRADER_ROLE) {}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @dev Returns the registry storage location.
|
|
251
|
+
*/
|
|
252
|
+
function _getStorage() internal pure returns (ACPShareRegistryStorage storage $) {
|
|
253
|
+
bytes32 slot = ACP_SHARE_REGISTRY_SLOT;
|
|
254
|
+
assembly {
|
|
255
|
+
$.slot := slot
|
|
256
|
+
}
|
|
257
|
+
}
|
|
145
258
|
}
|
|
@@ -1,62 +1,64 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity >=0.8.19 <0.9.0;
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {IPermissionCustomIdValidator} from "./Permissioned.sol";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @notice Default ACP revoker — timestamp-based revocation.
|
|
8
8
|
*
|
|
9
9
|
* Implements the unchanged revoker interface. Interprets a permission's
|
|
10
|
-
* `revokerData` as its creation timestamp (populated by the SDK at
|
|
11
|
-
* creation). Every SDK-created
|
|
12
|
-
* contract call is needed —
|
|
10
|
+
* `revokerData` as its creation timestamp (populated by the SDK at permission
|
|
11
|
+
* creation, backdated 60s for clock skew). Every SDK-created permission points
|
|
12
|
+
* here by default: no upfront contract call is needed — a permission is
|
|
13
|
+
* revocable from birth.
|
|
13
14
|
*
|
|
14
15
|
* `disabled(issuer, id)` truth table:
|
|
15
16
|
*
|
|
16
17
|
* | condition | result | meaning |
|
|
17
18
|
* |-----------------------------|--------|----------------------------------|
|
|
18
|
-
* | id > block.timestamp | true | future-dated
|
|
19
|
+
* | id > block.timestamp | true | future-dated permissions never |
|
|
19
20
|
* | | | validate (closes the revoke-all |
|
|
20
|
-
* | | | dodge:
|
|
21
|
-
* | | | mass revocation would
|
|
21
|
+
* | | | dodge: a permission dated ahead |
|
|
22
|
+
* | | | of a mass revocation would |
|
|
23
|
+
* | | | otherwise survive it) |
|
|
22
24
|
* | id <= revokeAllAt[issuer] | true | mass revocation (inclusive) |
|
|
23
25
|
* | revokedSingle[issuer][id] | true | targeted revocation |
|
|
24
|
-
* | otherwise | false |
|
|
26
|
+
* | otherwise | false | permission valid |
|
|
25
27
|
*
|
|
26
|
-
* Accepted trade-offs (by design, see
|
|
27
|
-
* - Two
|
|
28
|
+
* Accepted trade-offs (by design, see ACP spec discussion):
|
|
29
|
+
* - Two permissions minted by the same issuer in the same second share an id;
|
|
28
30
|
* revoking one revokes both. Over-revocation is the fail-safe direction.
|
|
29
31
|
* - Revocation is permanent — there is no un-revoke.
|
|
30
32
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
+
* @dev TODO(review): production wants this upgradeable (OZ AccessControl for the
|
|
34
|
+
* upgrader role) and deployed to a fixed address as core infrastructure.
|
|
33
35
|
*/
|
|
34
36
|
contract ACPTimestampRevoker is IPermissionCustomIdValidator {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/// @notice issuer => threshold; permissions with id (creation ts) at or before this are revoked
|
|
38
|
+
mapping(address => uint256) public revokeAllAt;
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
/// @notice issuer => id => revoked
|
|
41
|
+
mapping(address => mapping(uint256 => bool)) public revokedSingle;
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
event RevokedSingle(address indexed issuer, uint256 indexed id);
|
|
44
|
+
event RevokedAll(address indexed issuer, uint256 at);
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
/// @notice Revoke a single permission by its id (creation timestamp).
|
|
47
|
+
/// Only affects permissions issued by the caller.
|
|
48
|
+
function revokeSingle(uint256 id) external {
|
|
49
|
+
revokedSingle[msg.sender][id] = true;
|
|
50
|
+
emit RevokedSingle(msg.sender, id);
|
|
51
|
+
}
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
/// @notice Revoke every permission the caller created up to now.
|
|
54
|
+
/// O(1): a single threshold write, regardless of permission count.
|
|
55
|
+
function revokeAllExisting() external {
|
|
56
|
+
revokeAllAt[msg.sender] = block.timestamp;
|
|
57
|
+
emit RevokedAll(msg.sender, block.timestamp);
|
|
58
|
+
}
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
/// @dev Called by the ACL during permission validation.
|
|
61
|
+
function disabled(address issuer, uint256 id) external view returns (bool) {
|
|
62
|
+
return id > block.timestamp || id <= revokeAllAt[issuer] || revokedSingle[issuer][id];
|
|
63
|
+
}
|
|
62
64
|
}
|