diff --git a/docs/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy.mdx b/docs/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy.mdx index 1a967a355..6051cd33b 100644 --- a/docs/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy.mdx +++ b/docs/base-chain/specs/reference/b20/changelog/03-denim-policyregistry-not-policy.mdx @@ -54,7 +54,7 @@ function invertedPolicyId(uint64 policyId) external view returns (uint64); | Symbol | Selector | Status | Behavior | | --- | --- | --- | --- | | `invertedPolicyId(uint64)` | `0x6b468933` | New view | Toggles bit 63 (`policyId ^ INVERTED_POLICY_BIT`). Never reverts, reads no state, and is involutive. | -| `isAuthorized(uint64,address)` | Unchanged | Extended | An inverted ID resolves the base and returns the negated result. Fail-closed on an unknown or malformed base. | +| `isAuthorized(uint64,address)` | Unchanged | Extended | An inverted ID resolves the base and returns the negated result when the base exists. An inverted unknown or malformed base returns `false`. | | `policyExists(uint64)` | Unchanged | Extended | Strips to base: `policyExists(invertedPolicyId(id)) == policyExists(id)`. | | `policyAdmin(uint64)` | Unchanged | Extended | Strips to base. | | `pendingPolicyAdmin(uint64)` | Unchanged | Extended | Strips to base. | @@ -69,7 +69,8 @@ function invertedPolicyId(uint64 policyId) external view returns (uint64); #### Authorization -`isAuthorized` gains a leading invert branch. All non-inverted paths are unchanged. +`isAuthorized` gains a leading invert branch. All non-inverted paths are unchanged. `isAuthorized` +never reverts. ```text Authorization Evaluation lines expandable wrap highlight={2-6} isAuthorized(policyId, account): @@ -83,7 +84,16 @@ isAuthorized(policyId, account): ``` Inverting a composite negates the composite's combined result. An inverted ID over a never-created -base returns `false`; it never becomes allow-everyone. +or malformed base returns `false`; it never becomes allow-everyone. + +The following table shows how `isAuthorized` resolves each kind of ID. + +| ID | Result | +| --- | --- | +| Malformed | `false`. The invert flag (bit 63) is cleared before the type check, so an ID is malformed when the remaining top byte is outside `PolicyType`. | +| Well-formed but unknown | Treated as an empty set: `ALLOWLIST` and `UNION` return `false`; `BLOCKLIST` and `INTERSECT` return `true`. | +| Inverted, base exists | The negated result of the base. | +| Inverted, base unknown or malformed | `false`. | #### Getters Strip to Base @@ -162,6 +172,8 @@ composites are unaffected. To adopt: it treats the ID as an opaque `uint64`. 3. Consumers that store policy IDs must still validate `policyExists(policyId)` at write time. This works for inverted IDs because existence resolves to the base. +4. Do not rely on an inverted ID to allow an unknown or malformed base. `isAuthorized` returns + `false` for it, and returns `false` for any malformed ID. For the current interface, see the [IPolicyRegistry reference](/specifications/b20/reference/interfaces/i-policy-registry/index). diff --git a/docs/build-on-base/integrate-defi/list-tokenized-stocks.mdx b/docs/build-on-base/integrate-defi/list-tokenized-stocks.mdx index 36a2c2216..da7f4284e 100644 --- a/docs/build-on-base/integrate-defi/list-tokenized-stocks.mdx +++ b/docs/build-on-base/integrate-defi/list-tokenized-stocks.mdx @@ -60,7 +60,7 @@ The B20 contract provides ERC-8056 helper functions for common calculations, whe To comply with any regulatory requirements that apply to a particular asset, [policies](/specifications/b20/reference/interfaces/i-policy-registry) that manage allowlists and blocklists may be implemented. Policies determine whether a transfer is allowed or rejected. -The B20 contract provides the [`isAuthorized(policyID, account)`](/specifications/b20/reference/interfaces/i-policy-registry/is-authorized) function, which you can use to determine whether a specific account is allowed to transfer funds. +The B20 contract provides the [`isAuthorized(policyID, account)`](/specifications/b20/reference/interfaces/i-policy-registry/is-authorized) function, which you can use to determine whether a specific account is allowed to transfer funds. It never reverts, so a policy ID that doesn't exist still returns a result; check `policyExists(policyId)` before you rely on it. The standard [`approve()`](/specifications/b20/reference/interfaces/ib20/approve) function is not policy gated. Checking whether a quantity of funds is approved to be transferred does not guarantee that the funds aren't blocked by a policy. diff --git a/docs/build-on-base/issue-rwa/seize-and-cancel-units.mdx b/docs/build-on-base/issue-rwa/seize-and-cancel-units.mdx index 21282b6fa..e5d767d3a 100644 --- a/docs/build-on-base/issue-rwa/seize-and-cancel-units.mdx +++ b/docs/build-on-base/issue-rwa/seize-and-cancel-units.mdx @@ -45,6 +45,8 @@ How the seize scope works: Because the holder check is inverted, attach a **blocklist** to `SEIZE_EXEMPT_POLICY`. Accounts on the list are unauthorized and therefore seizable; every other holder stays exempt. Never attach an allowlist or `ALWAYS_BLOCK` to this scope: an empty allowlist authorizes nobody, so every holder would be seizable. +`isAuthorized` never reverts: a malformed ID, or an inverted ID whose base doesn't exist, returns `false`. On `SEIZE_EXEMPT_POLICY`, `false` makes the holder seizable, so confirm `policyExists(policyId)` before you attach an ID. See [`isAuthorized`](/specifications/b20/reference/interfaces/i-policy-registry/is-authorized) for the full rules. + ## Seize, Cancel, and Verify Grant the roles once per token: diff --git a/docs/specifications/b20/concepts/policies.mdx b/docs/specifications/b20/concepts/policies.mdx index 8414e3522..123ec1d8d 100644 --- a/docs/specifications/b20/concepts/policies.mdx +++ b/docs/specifications/b20/concepts/policies.mdx @@ -87,7 +87,7 @@ An issuer may want the opposite of an existing policy without a second member se Call `invertedPolicyId(policyId)` to set or clear that bit. You can also set bit 63 yourself. Bind the inverted ID to a token scope, or pass it as a composite child ("A AND NOT X"). The flag applies to every type: `ALLOWLIST`, `BLOCKLIST`, `UNION`, and `INTERSECT`. -`isAuthorized` on an inverted ID returns the opposite of the base. If the base does not exist, the result is `false`. That fail-closed guard prevents a mistyped inverted ID from becoming allow-everyone. +`isAuthorized` on an inverted ID returns the opposite of the base when that base exists. If the base is unknown or malformed, the result is `false`. That fail-closed guard prevents a mistyped inverted ID from becoming allow-everyone. Read views strip bit 63 and load the base. `policyExists` and `policyAdmin` on an inverted ID match the base. An inverted ID has no record of its own. diff --git a/docs/specifications/b20/introduction.mdx b/docs/specifications/b20/introduction.mdx index a8ed537eb..2dd4a81d8 100644 --- a/docs/specifications/b20/introduction.mdx +++ b/docs/specifications/b20/introduction.mdx @@ -143,6 +143,8 @@ Those lists live in the Policy Registry, a global singleton precompile, not on t A token admin binds a policy ID to a policy scope with `updatePolicy`. A scope sits in a similar place to a hook: it runs on a specific function. When that function runs, the token asks the registry `isAuthorized(policyId, account)` and reverts with `PolicyForbids` if the check fails. Which scope runs on which function is in [Policies](/specifications/b20/concepts/policies). +`isAuthorized` never reverts, even for a policy ID that doesn't exist: a malformed ID returns `false`, and an unknown ID behaves as an empty set. Validate `policyExists(policyId)` when you store a policy ID. See [`isAuthorized`](/specifications/b20/reference/interfaces/i-policy-registry/is-authorized) for every case. + A policy-gated transfer looks like this: ```mermaid Integrating Compliance Checks Diagram lines wrap expandable highlight={1} diff --git a/docs/specifications/b20/reference/constants.mdx b/docs/specifications/b20/reference/constants.mdx index c8cfa4df0..1f9907f59 100644 --- a/docs/specifications/b20/reference/constants.mdx +++ b/docs/specifications/b20/reference/constants.mdx @@ -12,7 +12,7 @@ description: "B20 precompile addresses, role identifiers, policy scopes, and val | Name | Value | Purpose | |---|---|---| | `B20_FACTORY_ADDRESS` | `0xB20f000000000000000000000000000000000000` | Deploys and looks up B-20 tokens; every asset and stablecoin instance is created through the [`IB20Factory`](/specifications/b20/reference/interfaces#ib20factory) at this address. | -| `POLICY_REGISTRY_ADDRESS` | `0x8453000000000000000000000000000000000002` | Stores allowlist/blocklist/composite policies and answers `isAuthorized` checks consulted by every policy scope (see [Policies](/specifications/b20/concepts/policies)). | +| `POLICY_REGISTRY_ADDRESS` | `0x8453000000000000000000000000000000000002` | Stores allowlist/blocklist/composite policies and answers `isAuthorized` checks consulted by every policy scope (see [Policies](/specifications/b20/concepts/policies)). `isAuthorized` never reverts and returns false for a malformed policy ID. | | `ACTIVATION_REGISTRY_ADDRESS` | `0x8453000000000000000000000000000000000001` | Gates whether a B-20 variant or feature is live on a given chain; checked by the factory before it will create that variant. | ## Roles diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/index.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/index.mdx index 79a07d469..63b3756d8 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/index.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/index.mdx @@ -18,7 +18,7 @@ description: "Generated B20 reference for IPolicyRegistry functions, events, and | [`updateAllowlist`](/specifications/b20/reference/interfaces/i-policy-registry/update-allowlist) | `0x3388fb5b` | Sets `accounts` membership in an ALLOWLIST policy to `allowed` in one batch. | | [`updateBlocklist`](/specifications/b20/reference/interfaces/i-policy-registry/update-blocklist) | `0x5c4e51b8` | Sets `accounts` membership in a BLOCKLIST policy to `blocked` in one batch. | | [`updateComposite`](/specifications/b20/reference/interfaces/i-policy-registry/update-composite) | `0xbfe142c0` | Replaces a composite policy's child-policy set in full with `childPolicyIds`. | -| [`isAuthorized`](/specifications/b20/reference/interfaces/i-policy-registry/is-authorized) | `0x55a1179e` | Returns whether `account` is authorized under `policyId`. Never reverts; unknown | +| [`isAuthorized`](/specifications/b20/reference/interfaces/i-policy-registry/is-authorized) | `0x55a1179e` | Returns whether `account` is authorized under `policyId`. Never reverts. A malformed ID returns false; a well-formed unknown ID is an empty set (ALLOWLIST and UNION return false, BLOCKLIST and INTERSECT return true). An inverted ID negates the base result only when the base exists; an inverted unknown or malformed base returns false. | | [`MIN_COMPOSITE_CHILD_POLICIES`](/specifications/b20/reference/interfaces/i-policy-registry/min-composite-child-policies) | `0xb3ae29f7` | Minimum number of child policies a composite must reference, inclusive. Never reverts. | | [`MAX_COMPOSITE_CHILD_POLICIES`](/specifications/b20/reference/interfaces/i-policy-registry/max-composite-child-policies) | `0x54309870` | Maximum number of child policies a composite may reference, inclusive. Never reverts. | | [`policyExists`](/specifications/b20/reference/interfaces/i-policy-registry/policy-exists) | `0x330f5637` | Returns whether `policyId` is a built-in sentinel or a previously-assigned custom ID. Never reverts. | diff --git a/docs/specifications/b20/reference/interfaces/i-policy-registry/is-authorized.mdx b/docs/specifications/b20/reference/interfaces/i-policy-registry/is-authorized.mdx index 57eeebf25..4bb2f4759 100644 --- a/docs/specifications/b20/reference/interfaces/i-policy-registry/is-authorized.mdx +++ b/docs/specifications/b20/reference/interfaces/i-policy-registry/is-authorized.mdx @@ -1,6 +1,6 @@ --- title: "IPolicyRegistry.isAuthorized" -description: "Generated B20 reference for isAuthorized(uint64,address)." +description: "Returns whether an account is authorized under a policy ID, including results for malformed, unknown, and inverted IDs." --- @@ -18,17 +18,38 @@ function isAuthorized(uint64 policyId, address account) external view returns (b ## Description -Returns whether `account` is authorized under `policyId`. Never reverts; unknown -or malformed IDs collapse to empty-member-set semantics (ALLOWLIST -> false, -BLOCKLIST -> true). -Dev: Callers that store policy IDs MUST validate `policyExists(policyId)` at write time. -Param: policyId Policy to query. -Param: account Account to check. -Return: Whether `account` is authorized. +Returns whether `account` is authorized under `policyId`. Never reverts. + +## Result by policy ID + +| Policy ID | Result | +|---|---| +| Malformed | `false`. The invert flag (bit 63) is cleared before the type check, so an ID is malformed when the remaining top byte is outside `PolicyType`. | +| Well-formed but unknown, ALLOWLIST or UNION | `false`. An unknown ID is treated as an empty set. | +| Well-formed but unknown, BLOCKLIST or INTERSECT | `true`. An unknown ID is treated as an empty set. | +| Inverted, base exists | The negated result of the base. Applies to every policy type. | +| Inverted, base unknown or malformed | `false` | + + +Callers that store policy IDs MUST validate `policyExists(policyId)` at write time. An unknown `BLOCKLIST` or `INTERSECT` ID returns `true`, and an inverted unknown or malformed base returns `false` rather than the negation of the empty-set result. + + +## Parameters + +| Name | Type | Description | +|---|---|---| +| `policyId` | `uint64` | Policy to query. | +| `account` | `address` | Account to check. | + +## Returns + +| Type | Description | +|---|---| +| `bool` | Whether `account` is authorized under `policyId`. | ## Access Control -Read-only or ERC-20-standard access rules unless the NatSpec states otherwise. +Read-only. No role required. ## Policy Interaction