-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathIPolicyRegistry.sol
More file actions
303 lines (271 loc) · 16.6 KB
/
IPolicyRegistry.sol
File metadata and controls
303 lines (271 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
100
101
102
103
104
105
106
107
108
109
110
111
112
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.20 <0.9.0;
/// @title IPolicyRegistry
/// @notice Singleton registry of address-membership policies. B-20 tokens
/// reference policies in this registry by `uint64 policyId` to
/// enforce authorization at the protocol level: every transfer,
/// mint, and redeem on a B-20 token resolves to one or more
/// `isAuthorized(policyId, account)` calls into this registry.
///
/// Two policy types are supported in v1:
/// - **ALLOWLIST**: an account is authorized only if it is on the
/// policy's member set.
/// - **BLOCKLIST**: an account is authorized unless it is on the
/// policy's member set.
///
/// The registry deliberately stops at flat membership checks.
/// There is no on-registry composition (no AND/OR/COMPOUND),
/// no callback or richer guard policies, no amount conditioning.
/// Asymmetric per-role rules on a token are expressed by storing
/// multiple policy IDs on the token itself (one per role slot),
/// not by composing inside the registry. See `IB20`'s policy
/// model for how this is wired on the token side.
///
/// @dev The registry is a singleton precompile at a fixed address.
/// All B-20 tokens on the chain share the same `policyId`
/// namespace. Anyone may create a policy; the creator nominates
/// the policy admin (typically themselves or a multisig).
///
/// **Built-in policy IDs** (always present, never need to be
/// created):
/// - `0` — always-allow. `isAuthorized(0, any)` returns true.
/// Semantic: "there is no policy on this slot." This is
/// the default state of every unassigned policy slot on
/// a newly created token, matching the principle that
/// absence of a configured policy means no restriction.
/// - `type(uint64).max` — always-reject. `isAuthorized(max, any)`
/// returns false. Useful as an explicit hard-deny on a
/// policy slot (e.g. disabling redemption by pointing
/// `REDEEMER_SENDER` at this sentinel), or as a "kill
/// switch" independent of token-level pause.
///
/// **Policy ID encoding.** Custom policy IDs encode the policy's
/// type directly into the top byte of the ID, so `policyType(id)`
/// resolves via pure bit extraction with no SLOAD. Since every
/// B-20 transfer / mint / redeem consults the registry, removing
/// a per-call storage read from the type lookup is a material
/// protocol-wide saving.
///
/// Encoding layout for custom IDs:
/// ```
/// [63:56] uint8 type discriminator = uint8(PolicyType)
/// [55:0] uint56 local ID within that type's space (max ~7.2e16)
/// ```
/// Discriminators currently in use:
/// - `0x00` — ALLOWLIST
/// - `0x01` — BLOCKLIST
/// - `0x02` .. `0xFE` — reserved for future PolicyType enum values
/// - `0xFF` — reserved (`type(uint64).max` is the always-reject
/// built-in; the entire 0xFF discriminator slot is
/// reserved to keep that sentinel unambiguous)
///
/// The two built-in IDs (`0` and `type(uint64).max`) are
/// special-cased BEFORE the encoding is consulted: implementations
/// short-circuit on those exact ID values, so the fact that ID `0`
/// numerically appears in the ALLOWLIST discriminator's local-ID
/// space (and `type(uint64).max` appears in the reserved 0xFF
/// space) does not create ambiguity at evaluation time. The
/// per-type local-ID counter for ALLOWLIST skips local-ID `0` so
/// no custom ALLOWLIST policy is ever assigned the encoded ID `0`.
///
/// This encoding gives `isAuthorized(policyId, account)` a
/// best-case hot path of 1 SLOAD (the member set), compared to 2
/// SLOADs if the type required a separate storage lookup. The
/// built-in IDs cost 0 SLOADs (short-circuited).
///
/// Custom policy IDs are assigned by per-type monotonic counters;
/// see `nextPolicyId(PolicyType)` to predict the next ID for a
/// given type.
///
/// **Future extensions** (not in v1 scope, intended path):
/// - Union / intersect policies: compose two same-typed policies
/// into a derived membership check. Would be added as new enum
/// values (`UNION_ALLOWLIST`, `INTERSECT_ALLOWLIST`, and
/// blocklist counterparts) with sibling `createUnionPolicy` /
/// `createIntersectPolicy` creators. New types get new top-byte
/// discriminators automatically (the encoding already reserves
/// 253 future type slots), so the storage shape does not need
/// to change. Enum extension is backward-compatible; existing
/// policies and consumers stay valid. Defer to a future hardfork.
interface IPolicyRegistry {
/*//////////////////////////////////////////////////////////////
TYPES
//////////////////////////////////////////////////////////////*/
/// @notice Policy type discriminator.
/// @param ALLOWLIST An account is authorized only if it is in the policy's set.
/// @param BLOCKLIST An account is authorized unless it is in the policy's set.
enum PolicyType {
ALLOWLIST,
BLOCKLIST
}
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
/// @notice Caller is not the policy admin (where current admin is
/// required) or is not the pending admin (where pending admin
/// is required by `finalizeUpdateAdmin`).
error Unauthorized();
/// @notice The referenced policy ID does not exist (and is not built-in).
error PolicyNotFound();
/// @notice The operation is incompatible with the policy's type. For
/// example, calling `updateAllowlist` on a BLOCKLIST policy.
error IncompatiblePolicyType();
/// @notice The provided policy type value is not in the `PolicyType` enum.
error InvalidPolicyType();
/// @notice A required address argument was the zero address.
error ZeroAddress();
/// @notice `finalizeUpdateAdmin` was called for a policy with no
/// currently-staged pending admin.
error NoPendingAdmin();
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
/// @notice A new policy was created. The creator may or may not be the
/// policy admin (the admin is set explicitly at creation).
event PolicyCreated(uint64 indexed policyId, address indexed creator, PolicyType policyType);
/// @notice A new admin was staged via `stageUpdateAdmin`. The active
/// admin does not change until `finalizeUpdateAdmin` is called
/// by `pendingAdmin`. `pendingAdmin == address(0)` indicates
/// a previously-staged transfer was cleared.
event PolicyAdminStaged(uint64 indexed policyId, address indexed currentAdmin, address indexed pendingAdmin);
/// @notice The active admin actually changed: either via
/// `finalizeUpdateAdmin` (where `newAdmin` is the previously
/// pending admin) or via `renounceAdmin` (where
/// `newAdmin == address(0)`). Initial admin assignment at
/// policy creation is also emitted as a `PolicyAdminUpdated`
/// with `previousAdmin == address(0)`.
event PolicyAdminUpdated(uint64 indexed policyId, address indexed previousAdmin, address indexed newAdmin);
/// @notice One or more accounts had their ALLOWLIST membership set to
/// `allowed`. Emitted once per `updateAllowlist` call, carrying
/// the full batch.
event AllowlistUpdated(uint64 indexed policyId, address indexed updater, bool allowed, address[] accounts);
/// @notice One or more accounts had their BLOCKLIST membership set to
/// `blocked`. Emitted once per `updateBlocklist` call, carrying
/// the full batch.
event BlocklistUpdated(uint64 indexed policyId, address indexed updater, bool blocked, address[] accounts);
/*//////////////////////////////////////////////////////////////
POLICY CREATION
//////////////////////////////////////////////////////////////*/
/// @notice Creates a new policy with no initial members.
/// @dev Permissionless. Reverts with `ZeroAddress` if `admin` is
/// `address(0)`, and with `InvalidPolicyType` if `policyType`
/// is not a valid `PolicyType` enum value.
/// @param admin The address authorized to modify membership on
/// this policy and to transfer or renounce
/// administration.
/// @param policyType ALLOWLIST or BLOCKLIST.
/// @return newPolicyId The newly assigned policy ID.
function createPolicy(address admin, PolicyType policyType) external returns (uint64 newPolicyId);
/// @notice Same as `createPolicy`, but seeds the policy's member set
/// with `accounts` in a single call. Useful for one-shot
/// creation flows that ship with a non-empty initial state.
function createPolicyWithAccounts(address admin, PolicyType policyType, address[] calldata accounts)
external
returns (uint64 newPolicyId);
/*//////////////////////////////////////////////////////////////
POLICY ADMINISTRATION
//////////////////////////////////////////////////////////////*/
/// @notice Stages a proposed new admin for `policyId`. Caller MUST be
/// the current admin. The active admin does NOT change until
/// `pendingAdmin` calls `finalizeUpdateAdmin(policyId)`.
/// @dev Calling `stageUpdateAdmin` while a pending admin already
/// exists overwrites the prior nomination (the previously
/// pending admin loses their ability to finalize). Pass
/// `address(0)` to clear a previously-staged transfer
/// without nominating a new candidate.
///
/// Two-step transfer guards against typos and key compromise:
/// the candidate must actively claim the role, and the
/// current admin retains control until they do.
/// @param policyId The policy whose admin is being staged.
/// @param newAdmin The proposed new admin, or `address(0)` to clear.
function stageUpdateAdmin(uint64 policyId, address newAdmin) external;
/// @notice Completes a two-step admin transfer. Caller MUST be the
/// address most recently staged via `stageUpdateAdmin`.
/// Promotes the caller to active admin and clears the pending
/// slot. Reverts with `NoPendingAdmin` if no transfer is in
/// flight.
function finalizeUpdateAdmin(uint64 policyId) external;
/// @notice Single-step: the current admin permanently relinquishes
/// administration of `policyId`. Caller MUST be the current
/// admin. After this call, `policyAdmin(policyId)` returns
/// `address(0)` and no further admin-gated operations on this
/// policy can succeed: the policy's member set is frozen
/// forever, and the policy can never be re-administered.
/// @dev Any in-flight pending admin (set via `stageUpdateAdmin`)
/// is cleared as a side effect of renunciation. The policy
/// continues to exist and remains a valid target of
/// `isAuthorized` queries; only mutation is disabled.
function renounceAdmin(uint64 policyId) external;
/// @notice Adds or removes `accounts` from an ALLOWLIST policy. All
/// accounts receive the same `allowed` setting in one batch.
/// Caller MUST be the current policy admin.
/// @dev Reverts with `IncompatiblePolicyType` if the policy is not
/// ALLOWLIST. Emits a single `AllowlistUpdated` event
/// carrying the full batch.
function updateAllowlist(uint64 policyId, bool allowed, address[] calldata accounts) external;
/// @notice Adds or removes `accounts` from a BLOCKLIST policy. All
/// accounts receive the same `blocked` setting in one batch.
/// Caller MUST be the current policy admin.
/// @dev Reverts with `IncompatiblePolicyType` if the policy is not
/// BLOCKLIST. Emits a single `BlocklistUpdated` event
/// carrying the full batch.
function updateBlocklist(uint64 policyId, bool blocked, address[] calldata accounts) external;
/*//////////////////////////////////////////////////////////////
AUTHORIZATION QUERIES
//////////////////////////////////////////////////////////////*/
/// @notice Whether `account` is authorized under `policyId`.
/// - For ALLOWLIST: returns true iff `account` is on the
/// policy's member set.
/// - For BLOCKLIST: returns true iff `account` is NOT on the
/// policy's member set.
/// - For built-in ID `0` (always-allow): always returns true.
/// - For built-in ID `type(uint64).max` (always-reject):
/// always returns false.
/// @dev Reverts with `PolicyNotFound` if `policyId` is neither a
/// built-in nor a previously-created policy.
function isAuthorized(uint64 policyId, address account) external view returns (bool);
/*//////////////////////////////////////////////////////////////
POLICY QUERIES
//////////////////////////////////////////////////////////////*/
/// @notice The next fully-encoded policy ID that will be assigned by
/// the next `createPolicy(_, policyType)` /
/// `createPolicyWithAccounts(_, policyType, _)` call for the
/// given type. Each `PolicyType` has its own monotonic local
/// counter; the returned value is the local counter combined
/// with the type's top-byte discriminator (see the contract
/// docstring "Policy ID encoding" section for the layout).
/// @dev For ALLOWLIST the per-type counter starts at local-ID `1`
/// (skipping the encoded ID `0`, which is the always-allow
/// built-in). For all other current and future types the
/// per-type counter starts at local-ID `0`.
function nextPolicyId(PolicyType policyType) external view returns (uint64);
/// @notice Whether `policyId` exists. The built-in IDs (`0` and
/// `type(uint64).max`) always exist; custom IDs exist iff they
/// have been created. Custom IDs are recognizable by their
/// top-byte discriminator falling within the active type
/// range and their local-ID falling below the per-type counter.
function policyExists(uint64 policyId) external view returns (bool);
/// @notice The type of `policyId`. Reverts with `PolicyNotFound` for
/// unknown IDs. For built-in IDs the returned value is
/// implementation-defined (the built-ins have no member set
/// and are not categorized as ALLOWLIST or BLOCKLIST);
/// callers should treat the built-ins as a separate case.
/// @dev Per the policy-ID encoding scheme (see contract docstring),
/// conforming implementations resolve this view via pure bit
/// extraction from the top byte of `policyId` rather than via
/// a storage read — except for the two built-in IDs, which are
/// special-cased.
function policyType(uint64 policyId) external view returns (PolicyType);
/// @notice The current admin of `policyId`. Returns `address(0)` for
/// built-in policies (which have no admin) and for policies
/// whose admin has been renounced via `renounceAdmin`.
/// Reverts with `PolicyNotFound` for unknown IDs.
function policyAdmin(uint64 policyId) external view returns (address);
/// @notice The currently-staged pending admin for `policyId`, set by
/// the most recent `stageUpdateAdmin` and cleared on
/// `finalizeUpdateAdmin` or `renounceAdmin`. Returns
/// `address(0)` when no transfer is in flight. Always
/// `address(0)` for built-in policies.
function pendingPolicyAdmin(uint64 policyId) external view returns (address);
}