From a44ffc945bd5fcc6d3a6df47336699f5cc38963a Mon Sep 17 00:00:00 2001 From: katzman Date: Wed, 10 Jun 2026 11:14:09 -0700 Subject: [PATCH 1/2] docs(PolicyRegistry): document activation gating contract Add an Activation section to the PolicyRegistry reference spelling out the dispatcher's activation contract: - view entry points (isAuthorized, policyExists, policyAdmin, pendingPolicyAdmin) are always callable, regardless of activation state; - mutating entry points are gated and revert FeatureNotActivated while the feature is inactive; - selector/ABI error classification is independent of activation state, so UnknownFunctionSelector and ABI-decode errors are preserved even when the feature is inactive (the gate is reached only after a call resolves to a known mutator with well-formed args). Generated with Claude Code Co-Authored-By: Claude --- docs/PolicyRegistry/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/PolicyRegistry/README.md b/docs/PolicyRegistry/README.md index 02e3776..124a28f 100644 --- a/docs/PolicyRegistry/README.md +++ b/docs/PolicyRegistry/README.md @@ -26,6 +26,26 @@ Custom policy IDs are assigned from a single global counter starting at `2`. The > **Precondition for consumers.** `isAuthorized` never reverts on a non-existent or malformed `policyId` — it collapses to empty-member-set semantics (ALLOWLIST → `false`, BLOCKLIST → `true`). Consumers that store policy IDs (notably `IB20.updatePolicy`) MUST validate `policyExists(policyId)` at write time, since a typo'd BLOCKLIST ID would silently behave as `ALWAYS_ALLOW`. +## Activation + +The PolicyRegistry is gated by the [`ActivationRegistry`](../ActivationRegistry/README.md): the feature is either active or inactive, and the gate is applied per call by the precompile's dispatcher. + +The gate covers **mutating** entry points only. The **view** entry points are always callable regardless of activation state: + +| Always callable (views) | Gated while inactive (mutators) | +|---|---| +| `isAuthorized` | `createPolicy` | +| `policyExists` | `createPolicyWithAccounts` | +| `policyAdmin` | `stageUpdateAdmin` | +| `pendingPolicyAdmin` | `finalizeUpdateAdmin` | +| | `renounceAdmin` | +| | `updateAllowlist` | +| | `updateBlocklist` | + +Keeping the views ungated means consumers — B20 tokens running `isAuthorized` on transfer, off-chain indexers reading membership and admin state — observe identical behavior whether or not the feature is active. Calling a gated mutator while the feature is inactive reverts with `FeatureNotActivated`. + +> **Error classification is independent of activation state.** The dispatcher recognizes the selector and decodes arguments *before* it applies the activation gate. An unrecognized selector reverts `UnknownFunctionSelector` and malformed calldata reverts with an ABI-decode error whether the feature is active or not — the activation gate is reached only once a call has been resolved to a known mutating function with well-formed arguments. Clients, tests, and cross-environment simulations can therefore distinguish "feature inactive" from "bad call" reliably, and a given malformed call produces the same error on both sides of an activation boundary. + ## User Flows ### Create Policy From 47bc38480ab9b341a89f89acd610e8d7be9d4bcb Mon Sep 17 00:00:00 2001 From: katzman Date: Wed, 10 Jun 2026 11:37:18 -0700 Subject: [PATCH 2/2] docs(PolicyRegistry): slim Activation section for builders Address review feedback: - table of views/mutators -> two plain bullet lists - code-format `PolicyRegistry` to match `ActivationRegistry` - drop the dense error-classification blockquote; the selector/ABI error-semantics detail is auditor-oriented and is better placed in base/base. Keep the builder-facing contract: reads always callable, writes gated with FeatureNotActivated while inactive. Generated with Claude Code Co-Authored-By: Claude --- docs/PolicyRegistry/README.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/PolicyRegistry/README.md b/docs/PolicyRegistry/README.md index 124a28f..c2475e4 100644 --- a/docs/PolicyRegistry/README.md +++ b/docs/PolicyRegistry/README.md @@ -28,23 +28,26 @@ Custom policy IDs are assigned from a single global counter starting at `2`. The ## Activation -The PolicyRegistry is gated by the [`ActivationRegistry`](../ActivationRegistry/README.md): the feature is either active or inactive, and the gate is applied per call by the precompile's dispatcher. +The `PolicyRegistry` is gated by the [`ActivationRegistry`](../ActivationRegistry/README.md). The gate applies only to functions that change state; read-only functions are always callable, whether or not the feature is active. -The gate covers **mutating** entry points only. The **view** entry points are always callable regardless of activation state: +**Always callable:** -| Always callable (views) | Gated while inactive (mutators) | -|---|---| -| `isAuthorized` | `createPolicy` | -| `policyExists` | `createPolicyWithAccounts` | -| `policyAdmin` | `stageUpdateAdmin` | -| `pendingPolicyAdmin` | `finalizeUpdateAdmin` | -| | `renounceAdmin` | -| | `updateAllowlist` | -| | `updateBlocklist` | +- `isAuthorized` +- `policyExists` +- `policyAdmin` +- `pendingPolicyAdmin` -Keeping the views ungated means consumers — B20 tokens running `isAuthorized` on transfer, off-chain indexers reading membership and admin state — observe identical behavior whether or not the feature is active. Calling a gated mutator while the feature is inactive reverts with `FeatureNotActivated`. +**Gated** — revert with `FeatureNotActivated` while the feature is inactive: -> **Error classification is independent of activation state.** The dispatcher recognizes the selector and decodes arguments *before* it applies the activation gate. An unrecognized selector reverts `UnknownFunctionSelector` and malformed calldata reverts with an ABI-decode error whether the feature is active or not — the activation gate is reached only once a call has been resolved to a known mutating function with well-formed arguments. Clients, tests, and cross-environment simulations can therefore distinguish "feature inactive" from "bad call" reliably, and a given malformed call produces the same error on both sides of an activation boundary. +- `createPolicy` +- `createPolicyWithAccounts` +- `stageUpdateAdmin` +- `finalizeUpdateAdmin` +- `renounceAdmin` +- `updateAllowlist` +- `updateBlocklist` + +Because reads are never gated, a consumer — a B20 token calling `isAuthorized` on transfer, or an indexer reading membership and admin state — sees the same behavior whether or not the feature is active. ## User Flows