Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/base-chain/specs/reference/b20/constants-and-addresses.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ These addresses are identical on every network where B20 is active.
| Name | Value |
|---|---|
| `ALWAYS_ALLOW` | `0` |
| `ALWAYS_BLOCK` | `(uint64(uint8(IPolicyRegistry.PolicyType.ALLOWLIST)) << 56) \| 1` |
| `ALWAYS_BLOCK` | `(uint64(uint8(IPolicyRegistry.PolicyType.ALLOWLIST)) << 56) \| 1` |

Custom policy IDs use this layout:

```text
[8-bit PolicyType][56-bit counter]
```

Simple and composite policies share the same global counter. The counter starts at `2` because `0` and `1` are reserved for `ALWAYS_ALLOW` and `ALWAYS_BLOCK`.

Policy type bytes:

| PolicyType | Byte |
Expand All @@ -62,6 +64,12 @@ Policy type bytes:
| `UNION` | `0x02` |
| `INTERSECT` | `0x03` |

`UNION` and `INTERSECT` are composite policy types. A `UNION` policy authorizes an account if any child policy authorizes it. An `INTERSECT` policy authorizes an account only if every child policy authorizes it. Each composite references two to four existing simple (`ALLOWLIST` or `BLOCKLIST`) policies. Composite policies cannot reference other composites.

<Warning>
A well-formed but never-created `INTERSECT` policy ID returns `true` for every account (vacuously authorized). Always call `policyExists(policyId)` before storing a composite policy ID; an invalid `INTERSECT` ID behaves like `ALWAYS_ALLOW`.
</Warning>

## Variant bytes

| Variant | Byte | Address shape |
Expand Down
32 changes: 5 additions & 27 deletions docs/base-chain/specs/reference/b20/errors-and-events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ description: "Reverse lookup B20 errors and events by selector or topic."
| error | `BatchSizeTooLarge` | `0x083e2f67` | `IPolicyRegistry` | A membership batch exceeded the registry limit. |
| event | `BlocklistUpdated` | `0x2ff63c102b1b9fd7f5d39f83039c5d6aaf50a414a4f2def2704e41be2628f1e3` | `IPolicyRegistry` | One or more accounts had their BLOCKLIST membership set to `blocked` in a single batch. |
| event | `BurnedBlocked` | `0x0b552e96653fd6842da37c477005d3b5c08a8c7d3631b1f43787b2dc9a1006a3` | `IB20` | Emitted by the deprecated `burnBlocked` in addition to `Transfer(from, address(0), amount)`. |
| error | `ChildPoliciesOutsideOfRange` | `0x697ec868` | `IPolicyRegistry` | A composite policy was created or updated with a child-policy count outside the |
| event | `CompositePolicyUpdated` | `0x4ff6adaab31b0df87aa7b8b7320c52b8b3b5eede3bf28a6baaaa8b8b7e1d6363` | `IPolicyRegistry` | A composite policy's child set was set or replaced in full with `childPolicyIds`. Emitted |
| error | `ChildPoliciesOutsideOfRange` | `0x697ec868` | `IPolicyRegistry` | A composite policy was created or updated with a child-policy count outside the allowed range of 2 to 4. |
| event | `CompositePolicyUpdated` | `0x4ff6adaab31b0df87aa7b8b7320c52b8b3b5eede3bf28a6baaaa8b8b7e1d6363` | `IPolicyRegistry` | A composite policy's child set was set or replaced in full with `childPolicyIds`. Emitted by `createCompositePolicy` and `updateComposite`. |
| error | `ContractPaused` | `0xf9df5ac9` | `IB20` | The `PausableFeature` covering this operation is currently paused. |
| event | `ContractURIUpdated` | `0xa5d4097edda6d87cb9329af83fb3712ef77eeb13738ffe43cc35a4ce305ad962` | `IB20` | Emitted by `updateContractURI`. Per ERC-7572, parameterless: integrators re-fetch `contractURI()`. |
| error | `DelegateCallNotAllowed` | `0x0d89438e` | `IActivationRegistry` | The precompile was invoked via `DELEGATECALL` or `CALLCODE`. |
Expand All @@ -38,15 +38,15 @@ description: "Reverse lookup B20 errors and events by selector or topic."
| event | `FeatureActivated` | `0x8c7a0ecdbb8d96e867e43ec1aef80027976ee493c18bef399fa799ed19752451` | `IActivationRegistry` | Emitted when `feature` is activated. |
| event | `FeatureDeactivated` | `0x15bf65a782c3258c63268ba9d7aed710cf9f9315d3687d9a4632ccdad7926c84` | `IActivationRegistry` | Emitted when `feature` is deactivated. |
| error | `FeatureNotActivated` | `0xb9b2a425` | `IActivationRegistry` | Feature is not activated. |
| error | `IncompatiblePolicyType` | `0xf1011ef5` | `IPolicyRegistry` | The operation is incompatible with the policy's type. |
| error | `IncompatiblePolicyType` | `0xf1011ef5` | `IPolicyRegistry` | The operation is incompatible with the policy's type. `createPolicy` and `createPolicyWithAccounts` revert with this error when called with `UNION` or `INTERSECT`. |
| error | `InitCallFailed` | `0x4eae0860` | `IB20Factory` | One of the `initCalls` reverted. The factory bubbles the underlying revert reason |
| error | `InsufficientAllowance` | `0x192b9e4e` | `IB20` | `spender`'s allowance is less than `needed` for the requested `transferFrom`. |
| error | `InsufficientBalance` | `0xdb42144d` | `IB20` | `sender`'s balance is less than `needed` for the requested transfer or burn. |
| error | `InternalCallFailed` | `0xb288a127` | `IB20Asset` | An inner call dispatched by `announce` reverted with an ordinary revert; its reason is |
| error | `InternalCallMalformed` | `0x4e2f143e` | `IB20Asset` | An inner call dispatched by `announce` was shorter than four bytes. |
| error | `InvalidAmount` | `0x2c5211c6` | `IB20` | An amount argument was zero where a non-zero value is required. Not used for ERC-20 amount arguments. |
| error | `InvalidApprover` | `0x8bc146c4` | `IB20` | The approval's `owner` address is invalid (typically `address(0)`). |
| error | `InvalidChildPolicy` | `0x46508ef6` | `IPolicyRegistry` | Composite policies are not simple policies. Child policies must be existing |
| error | `InvalidChildPolicy` | `0x46508ef6` | `IPolicyRegistry` | A child policy ID passed to `createCompositePolicy` or `updateComposite` is not a valid simple policy. Child policies must be existing `ALLOWLIST` or `BLOCKLIST` policies; composite policies and the built-in sentinels `ALWAYS_ALLOW` and `ALWAYS_BLOCK` are not valid children. |
| error | `InvalidCurrency` | `0x997c1de8` | `IB20Factory` | The stablecoin `currency` was non-empty but contained a non-`A`-`Z` byte. |
| error | `InvalidDecimals` | `0xca950391` | `IB20Factory` | The asset `decimals` was outside the allowed inclusive range |
| error | `InvalidMetadataKey` | `0x86ea3abb` | `IB20Asset` | `updateExtraMetadata` was called with an empty `key`. |
Expand Down Expand Up @@ -75,26 +75,4 @@ description: "Reverse lookup B20 errors and events by selector or topic."
| event | `PolicyAdminStaged` | `0xdbf3b34a4c956c56ca05cd4b8f9293a4347ad61994445a4b89817d4a19561136` | `IPolicyRegistry` | A new admin was staged. `pendingAdmin == address(0)` clears a prior nomination. |
| event | `PolicyAdminUpdated` | `0x98925cfb1bc09c5b43dd0dd56d3d95aa04fb3300927580cc588c3f5dd58c15e1` | `IPolicyRegistry` | The active admin changed. `newAdmin == address(0)` indicates renunciation; |
| event | `PolicyCreated` | `0xdc870ce85be577234b8548f42b93f84bbff6d0c1f38ee725c809c49932a13885` | `IPolicyRegistry` | A new policy was created. |
| error | `PolicyForbids` | `0xa43fec12` | `IB20` | A policy slot denied the operation. |
| error | `PolicyNotFound` | `0xcccad523` | `IB20` | The provided policy ID does not exist in the policy registry. |
| error | `PolicyNotFound` | `0x720caa4f` | `IPolicyRegistry` | The referenced policy ID does not exist. |
| event | `PolicyUpdated` | `0x8b4790f7ff717fc8f60f07ae099e47ef318dc04b37ae98056b50a22b79056626` | `IB20` | Emitted by `updatePolicy` when a token's policy slot is changed. Initial slot assignment at |
| event | `RoleAdminChanged` | `0xbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff` | `IB20` | Emitted by `setRoleAdmin` when the admin role for `role` changes. |
| event | `RoleGranted` | `0x2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d` | `IB20` | Emitted when `account` is granted `role`. `sender` is the originating caller. |
| event | `RoleRevoked` | `0xf6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b` | `IB20` | Emitted when `role` is revoked from `account`. `sender` is the originating caller |
| error | `UIMultiplierUpdateExists` | `0x4481a68e` | `IB20Asset` | `updateUIMultiplier` was called while a live pending update already exists |
| event | `Seized` | `0xa9aec5d8b86e2fa2fd6ac3af62f2622e3dfdab1967d4cbbb56a5df7d74cb887c` | `IB20` | Emitted by `seizeWithMemo` in addition to `Transfer(from, to, amount)` (and the |
| error | `StaticCallNotAllowed` | `0xbeaba5b7` | `IActivationRegistry` | A state-mutating entry point was invoked from a `STATICCALL` frame. |
| error | `SupplyCapExceeded` | `0x4b344b11` | `IB20` | The mint would push `totalSupply` past the configured cap. |
| event | `SupplyCapUpdated` | `0x6d14f44808ce024f263432bc38d019a9951fbe674e9898b54844dbc8dc09c23a` | `IB20` | Emitted by `updateSupplyCap`. |
| event | `SymbolUpdated` | `0x64e8b5c6dcea43dd79766bb3b8af7c45968d12b68c960cf2da23856f34d598d4` | `IB20` | Emitted by `updateSymbol`. Carries the new symbol string. |
| error | `TokenAlreadyExists` | `0x15ef3a57` | `IB20Factory` | A token already exists at the deterministic address derived from |
| event | `Transfer` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | `IB20` | ERC-20 transfer event. Emitted on every successful transfer (including memo'd variants), |
| error | `Unauthorized` | `0x8e4a23d6` | `IActivationRegistry` | Caller is not the activation admin. |
| error | `Unauthorized` | `0x82b42900` | `IB20` | Caller failed a positional authorization check that is not expressible as "missing role X". |
| error | `Unauthorized` | `0x82b42900` | `IPolicyRegistry` | Caller is not the admin required by the attempted operation. |
| event | `UIMultiplierUpdated` | `0x2205df4534432b2f60654a3fdb48737ffdaf3e9edb1a498bd985bc026b15b055` | `IB20Asset` | Emitted when the UI multiplier is updated. |
| event | `Unpaused` | `0xdc6281474ea3dea2a856e225c7ba3edf427de9164495817c4d49a595e683fed4` | `IB20` | Emitted by `unpause`. `features` is the argument to the call (not the resulting paused state). |
| error | `UnsupportedPolicyType` | `0xcdd98a4a` | `IB20` | `policyScope` is not a slot this token (or its variant) supports. |
| error | `UnsupportedVersion` | `0xc0d8b4e0` | `IB20Factory` | The leading `version` byte in `params` does not match any known encoding for the requested variant. |
| error | `ZeroAddress` | `0xd92e233d` | `IPolicyRegistry` | A required address argument was the zero address. |
| error | `PolicyForbids` | `
41 changes: 34 additions & 7 deletions docs/base-chain/specs/reference/b20/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,16 @@ State-changing PolicyRegistry calls are ActivationRegistry-gated. Read functions
|---|---|
| `BLOCKLIST` | Account is authorized unless listed. |
| `ALLOWLIST` | Account is authorized only if listed. |
| `UNION` | Composite: account is authorized if any child simple policy authorizes it. |
| `INTERSECT` | Composite: account is authorized only if every child simple policy authorizes it. |
| `UNION` | Composite: account is authorized if any child simple policy authorizes it (OR). |
| `INTERSECT` | Composite: account is authorized only if every child simple policy authorizes it (AND). |

Composite policies reference existing simple `ALLOWLIST` or `BLOCKLIST` child policies. They cannot reference composites or built-ins as children.
Composite policies reference two to four existing simple `ALLOWLIST` or `BLOCKLIST` child policies. They cannot reference other composites or built-in sentinel policies as children. This constraint is enforced at write time, so `isAuthorized` on a composite never recurses beyond depth 1.

Evaluation is live: each `isAuthorized` call reads the current membership of each evaluated child. `UNION` short-circuits on the first authorizing child; `INTERSECT` short-circuits on the first non-authorizing child. Child order affects gas cost but never the authorization result.

<Warning>
`isAuthorized` on an uncreated `UNION` ID returns `false` (deny-all). `isAuthorized` on an uncreated `INTERSECT` ID returns `true` (allow-all), which behaves identically to `ALWAYS_ALLOW`. Always call `policyExists(policyId)` before storing a policy ID in a token scope.
</Warning>

### Policy IDs

Expand All @@ -71,13 +77,13 @@ Counters `0` and `1` are reserved for built-ins:
| Built-in | Value | Behavior |
|---|---:|---|
| `ALWAYS_ALLOW` | `0` | Authorizes every account. |
| `ALWAYS_BLOCK` | `(uint64(ALLOWLIST) &lt;&lt; 56) \| 1` | Denies every account. |
| `ALWAYS_BLOCK` | `(uint64(ALLOWLIST) << 56) \| 1` | Denies every account. |

Custom policy creation starts at counter `2`.
Custom policy creation starts at counter `2`. Simple and composite policies share the same global counter; composite policies do not use a separate counter.

### Admin Model

Each policy has one admin. Admin transfer is two-step: `stageUpdateAdmin(policyId, newAdmin)` followed by `finalizeUpdateAdmin(policyId)` from the pending admin. `renounceAdmin(policyId)` permanently freezes membership or child-policy updates for that policy.
Each policy has one admin. Admin transfer is two-step: `stageUpdateAdmin(policyId, newAdmin)` followed by `finalizeUpdateAdmin(policyId)` from the pending admin. `renounceAdmin(policyId)` permanently freezes membership or child-policy updates for that policy. A renounced child policy remains effective in composites that reference it; its current authorization results continue to apply.

### Read Interface

Expand All @@ -87,10 +93,29 @@ Each policy has one admin. Admin transfer is two-step: `stageUpdateAdmin(policyI
| `policyExists(policyId)` | Returns whether a policy exists. |
| `policyAdmin(policyId)` | Returns the current admin or zero. |
| `pendingPolicyAdmin(policyId)` | Returns the staged admin or zero. |
| `compositePolicyChildIds(policyId)` | Returns child policy IDs for composite policies. |
| `compositePolicyChildIds(policyId)` | Returns child policy IDs for composite policies; empty for non-composites. |

`isAuthorized` collapses uncreated IDs to empty-set semantics. Callers that write policy IDs into token scopes must validate `policyExists` unless writing a built-in.

### Composite Policy Interface

```solidity
function createCompositePolicy(address admin, PolicyType policyType, uint64[] calldata childPolicyIds)
external
returns (uint64 newPolicyId);

function updateComposite(uint64 policyId, uint64[] calldata childPolicyIds) external;

function compositePolicyChildIds(uint64 policyId) external view returns (uint64[] memory);

function MIN_COMPOSITE_CHILD_POLICIES() external view returns (uint256);
function MAX_COMPOSITE_CHILD_POLICIES() external view returns (uint256);
```

`createCompositePolicy` accepts `UNION` or `INTERSECT` as `policyType` and requires `childPolicyIds` to contain at least `MIN_COMPOSITE_CHILD_POLICIES` (`2`) and no more than `MAX_COMPOSITE_CHILD_POLICIES` (`4`) entries. `updateComposite` replaces the entire child set and enforces the same range. Neither function supports partial updates or an empty child set.

`createPolicy` and `createPolicyWithAccounts` revert with `IncompatiblePolicyType` when called with `UNION` or `INTERSECT`.

## Policy Integration

B20 tokens store one `uint64 policyId` per supported policy scope.
Expand All @@ -106,6 +131,8 @@ B20 tokens store one `uint64 policyId` per supported policy scope.

All scopes default to `ALWAYS_ALLOW` at creation. `approve` and `permit` are not policy-gated.

A composite policy ID is passed to a B20 policy scope exactly like a simple policy ID. B20 stores policy scopes as an opaque `uint64` and calls `isAuthorized` generically; no B20 contract changes are required to use composite policies.

## Mint

`mint` and `mintWithMemo` are gated by `MINT_ROLE`, checked against `MINT_RECEIVER_POLICY`, and bounded by `supplyCap`.
Expand Down
38 changes: 1 addition & 37 deletions docs/base-chain/specs/reference/b20/interfaces/IB20.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,40 +76,4 @@ description: "Generated B20 reference for IB20 functions, events, and errors."
| `RoleRevoked` | `0xf6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b` | Emitted when `role` is revoked from `account`. `sender` is the originating caller |
| `RoleAdminChanged` | `0xbd79b86ffe0ab8e8776151514217cd7cacd52c909f66475c3af44e129f0b00ff` | Emitted by `setRoleAdmin` when the admin role for `role` changes. |
| `LastAdminRenounced` | `0xe8d3a9872e7ca325571ff1e4c51ddd69090a0345240cc605ccde365ec867cc67` | Emitted by `renounceLastAdmin` in addition to the standard |
| `Paused` | `0x43e072977b8112813d7c2aa0b63d3c121c1fe3e714e6d2eacb5735fe4027e976` | Emitted by `pause`. `features` is the argument to the call (not the resulting paused state). |
| `Unpaused` | `0xdc6281474ea3dea2a856e225c7ba3edf427de9164495817c4d49a595e683fed4` | Emitted by `unpause`. `features` is the argument to the call (not the resulting paused state). |
| `PolicyUpdated` | `0x8b4790f7ff717fc8f60f07ae099e47ef318dc04b37ae98056b50a22b79056626` | Emitted by `updatePolicy` when a token's policy slot is changed. Initial slot assignment at |
| `SupplyCapUpdated` | `0x6d14f44808ce024f263432bc38d019a9951fbe674e9898b54844dbc8dc09c23a` | Emitted by `updateSupplyCap`. |
| `ContractURIUpdated` | `0xa5d4097edda6d87cb9329af83fb3712ef77eeb13738ffe43cc35a4ce305ad962` | Emitted by `updateContractURI`. Per ERC-7572, parameterless: integrators re-fetch `contractURI()`. |
| `NameUpdated` | `0x74321da206c1b9fa34367f7ece59ca49371dcd13820b9a5c3767ae1ecceed51a` | Emitted by `updateName`. Carries the new name string. |
| `SymbolUpdated` | `0x64e8b5c6dcea43dd79766bb3b8af7c45968d12b68c960cf2da23856f34d598d4` | Emitted by `updateSymbol`. Carries the new symbol string. |
| `EIP712DomainChanged` | `0x0a6387c9ea3628b88a633bb4f3b151770f70085117a15f9bf3787cda53f13d31` | ERC-5267 domain-change signal. Emitted exactly once per successful `updateName` call, |

## Errors

| Error | Selector | Summary |
|---|---|---|
| `NonPayable` | `0x6fb1b0e9` | ETH was attached to a call targeting a nonpayable token selector. |
| `AccessControlUnauthorizedAccount` | `0xe2517d3f` | `account` does not hold `neededRole`. |
| `Unauthorized` | `0x82b42900` | Caller failed a positional authorization check that is not expressible as "missing role X". |
| `ContractPaused` | `0xf9df5ac9` | The `PausableFeature` covering this operation is currently paused. |
| `InsufficientAllowance` | `0x192b9e4e` | `spender`'s allowance is less than `needed` for the requested `transferFrom`. |
| `InsufficientBalance` | `0xdb42144d` | `sender`'s balance is less than `needed` for the requested transfer or burn. |
| `InvalidSender` | `0x4c14f64c` | The transfer's source address is invalid (typically `address(0)`). |
| `InvalidReceiver` | `0x9cfea583` | The transfer's destination address is invalid (typically `address(0)`). |
| `InvalidApprover` | `0x8bc146c4` | The approval's `owner` address is invalid (typically `address(0)`). |
| `InvalidSpender` | `0x4e15efda` | The approval's `spender` address is invalid (typically `address(0)`). |
| `InvalidAmount` | `0x2c5211c6` | An amount argument was zero where a non-zero value is required. Not used for ERC-20 amount arguments. |
| `EmptyFeatureSet` | `0x4861ff45` | An empty array was passed to a function that requires at least one element. |
| `InvalidSupplyCap` | `0x0a3780ce` | The proposed supply cap is outside the permitted range: below the current |
| `SupplyCapExceeded` | `0x4b344b11` | The mint would push `totalSupply` past the configured cap. |
| `PolicyForbids` | `0xa43fec12` | A policy slot denied the operation. |
| `PolicyNotFound` | `0xcccad523` | The provided policy ID does not exist in the policy registry. |
| `UnsupportedPolicyType` | `0xcdd98a4a` | `policyScope` is not a slot this token (or its variant) supports. |
| `AccountNotSeizable` | `0x91dbbc8d` | `seizeWithMemo` was called against a `from` that is currently authorized under |
| `AccountNotBlocked` | `0x64a5cb46` | The deprecated `burnBlocked` was called against a `from` that is currently authorized under |
| `ExpiredSignature` | `0xbd2a913c` | An EIP-2612 `permit` was submitted with a `deadline` strictly less than `block.timestamp`. |
| `InvalidSigner` | `0x7ba5ffb5` | ECDSA recovery on an EIP-2612 `permit` returned `signer`, which does not match the claimed `owner`. |
| `LastAdminCannotRenounce` | `0x361513e7` | `renounceRole(DEFAULT_ADMIN_ROLE, ...)` was called by the sole remaining admin. |
| `NotSoleAdmin` | `0x2a98e73b` | `renounceLastAdmin()` was called when other accounts also hold `DEFAULT_ADMIN_ROLE`. |
| `AccessControlBadConfirmation` | `0x6697b232` | The `callerConfirmation` argument to `renounceRole` was not `msg.sender`. |
| `Paused` | `0x43e072977b8
Loading