> ## Documentation Index
> Fetch the complete documentation index at: https://base-a060aa97-fix-get-started-governance-approval.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# B20 Specification

> Normative Beryl specification for Base's B20 native token standard, precompile registries, policy scopes, factory, and variants.

B20 is Base's native ERC-20-compatible token standard implemented as Rust precompiles. It adds chain-native roles, policy scopes, memos, pausing, supply caps, ERC-2612 `permit`, deterministic factory creation, and variant-specific surfaces for Asset and Stablecoin tokens.

## ERC-20 Compatibility

B20 is a superset of ERC-20. Standard ERC-20 calls and events keep selector and behavior parity for `transfer`, `transferFrom`, `approve`, `allowance`, `balanceOf`, `totalSupply`, `name`, `symbol`, `decimals`, `Transfer`, and `Approval`.

B20-specific methods extend the standard without changing the ERC-20 surface.

## Roles Model

B20 includes role-based access control with fixed built-in roles.

| Role | Gates |
| - | - |
| `DEFAULT_ADMIN_ROLE` | `grantRole`, `revokeRole`, `setRoleAdmin`, `updatePolicy`, `updateSupplyCap` |
| `MINT_ROLE` | `mint`, `mintWithMemo` |
| `BURN_ROLE` | `burn`, `burnWithMemo` |
| `BURN_BLOCKED_ROLE` | Deprecated back-compat `burnBlocked` path |
| `SEIZE_ROLE` | `seizeWithMemo` |
| `PAUSE_ROLE` | `pause` |
| `UNPAUSE_ROLE` | `unpause` |
| `METADATA_ROLE` | `updateName`, `updateSymbol`, `updateContractURI` |
| `OPERATOR_ROLE` | Asset-only multiplier and announcement operations |

User-defined roles are supported by the role graph but have no built-in enforcement by B20 token functions.

### Admin Renunciation

The final `DEFAULT_ADMIN_ROLE` holder cannot be removed with normal `renounceRole` or `revokeRole`; both revert with `LastAdminCannotRenounce`. `renounceLastAdmin()` is the only normal path to permanently transition to admin-less operation.

A token can launch admin-less by setting `initialAdmin == address(0)` at creation. After admin renunciation, `DEFAULT_ADMIN_ROLE`-gated functions are permanently uncallable and admin resurrection is blocked.

## Policy Registry

The PolicyRegistry is a singleton precompile that stores policies addressed by `uint64 policyId`. B20 tokens store policy IDs in fixed scopes and call `isAuthorized(policyId, account)` during gated operations.

State-changing PolicyRegistry calls are ActivationRegistry-gated. Read functions are always callable.

### Policy Types

| PolicyType | Behavior |
| - | - |
| `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. |

Composite policies reference existing simple `ALLOWLIST` or `BLOCKLIST` child policies. They cannot reference composites or built-ins as children.

### Policy IDs

Policy IDs are laid out as:

```text Policy ID Layout theme={null}
[top 8 bits: PolicyType][low 56 bits: counter]
```

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. |

Custom policy creation starts at counter `2`.

### 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.

### Read Interface

| Method | Description |
| - | - |
| `isAuthorized(policyId, account)` | Returns authorization and never reverts for uncreated IDs. |
| `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. |

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

## Policy Integration

B20 tokens store one `uint64 policyId` per supported policy scope.

| Scope | Checked account | Operation |
| - | - | - |
| `TRANSFER_SENDER_POLICY` | `from` | `transfer`, `transferFrom`, and memo variants |
| `TRANSFER_RECEIVER_POLICY` | `to` | `transfer`, `transferFrom`, and memo variants |
| `TRANSFER_EXECUTOR_POLICY` | `msg.sender` | `transferFrom` when `msg.sender != from` |
| `MINT_RECEIVER_POLICY` | `to` | `mint`, `mintWithMemo` |
| `SEIZE_EXEMPT_POLICY` | `from` | `seizeWithMemo`; an authorized holder is seize-exempt, so only unauthorized holders are seizable |
| `SEIZE_RECEIVER_POLICY` | `to` | `seizeWithMemo`; destination must be authorized |

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

## Mint

`mint` and `mintWithMemo` are gated by `MINT_ROLE`, checked against `MINT_RECEIVER_POLICY`, and bounded by `supplyCap`.

## Burn

`burn` and `burnWithMemo` burn from the caller and are gated by `BURN_ROLE`.

The legacy `burnBlocked` path is deprecated and retained for backwards compatibility. New seizure flows use `seizeWithMemo`.

## Seize

`seizeWithMemo(from, to, amount, memo)` transfers balance from `from` to `to` and emits `Transfer`, `Memo`, and `Seized`. It is gated by `SEIZE_ROLE`, skips allowance and transfer policies, requires `from` to be denied by `SEIZE_EXEMPT_POLICY`, and requires `to` to be authorized by `SEIZE_RECEIVER_POLICY`.

## Supply Cap

The supply cap is optional. The sentinel `type(uint128).max` indicates no practical cap and is also the maximum permitted `totalSupply`. `updateSupplyCap` is admin-gated and reverts with `InvalidSupplyCap` if the proposed cap is below current supply or above the maximum.

## Memos

Memo-enabled operations emit `Memo(address indexed caller, bytes32 indexed memo)` immediately after the primary operation event. Indexers join memo logs to the parent log with `(transactionHash, logIndex - 1)`.

Memo entrypoints include `transferWithMemo`, `transferFromWithMemo`, `mintWithMemo`, `burnWithMemo`, and `seizeWithMemo`.

## Pause

B20 supports granular pausing by `PausableFeature`: `TRANSFER`, `MINT`, `BURN`, and `SEIZE`. The enum is append-only. `pause` is gated by `PAUSE_ROLE`; `unpause` is gated by `UNPAUSE_ROLE`.

## ERC-2612 Permit / EIP-712

B20 implements ERC-2612 signed approvals with an EIP-712 domain shaped as `(name, version, chainId, verifyingContract)`, with `version` fixed at `"1"`. `updateName` rotates the domain separator and emits `EIP712DomainChanged`. ERC-1271 contract signatures are not accepted.

## Contract URI (ERC-7572)

`contractURI()` returns offchain token metadata per ERC-7572. `updateContractURI(newURI)` is gated by `METADATA_ROLE`.

## Metadata Updates

`updateName` and `updateSymbol` are gated by `METADATA_ROLE`. `updateName` also rotates the EIP-712 domain separator.

## Factory

All B20 tokens are created through the singleton factory precompile:

```solidity IB20Factory.sol theme={null}
createB20(B20Variant variant, bytes32 salt, bytes params, bytes[] initCalls)
```

| Parameter | Description |
| - | - |
| `variant` | `ASSET` or `STABLECOIN` |
| `salt` | Caller-chosen entropy for deterministic address derivation |
| `params` | Versioned, variant-specific create params |
| `initCalls` | ABI-encoded bootstrap calls dispatched to the new token |

The factory reverts with `FeatureNotActivated` if the requested variant is not activated.

### Address Derivation

B20 token addresses are deterministic and encode the variant:

```text Address Derivation theme={null}
[10-byte B20 prefix][1-byte variant][9-byte keccak256(deployer, salt)]
```

`getB20Address`, `isB20`, and `isB20Initialized` are available on the factory.

### initCalls Semantics

During initCalls, factory-originated calls bypass token role gates and transfer-side policy gates: `TRANSFER_SENDER_POLICY`, `TRANSFER_RECEIVER_POLICY`, and `TRANSFER_EXECUTOR_POLICY`.

The bypass does not apply to `MINT_RECEIVER_POLICY`, pause state, supply cap, or balance accounting invariants. The bootstrap window closes when `createB20` returns.

## Variants

| Variant | Byte | Decimals | Additional surface |
| - | -: | - | - |
| `ASSET` | `0x00` | 6-18, configured at creation | Multiplier, announcements, batch mint, extra metadata |
| `STABLECOIN` | `0x01` | Fixed 6 | `currency()` |

### Asset

Asset tokens add `OPERATOR_ROLE`, scaled UI balance support, scheduled and instant multiplier updates, announcements, batch minting, and extra metadata.

#### Multiplier

The multiplier is WAD-precision and scales UI balance reads while raw balances remain unchanged.

#### Announcements

`announce` emits `Announcement`, dispatches internal calls, and emits `EndAnnouncement`. Announcement IDs are unique forever. Non-panic inner reverts are wrapped in `InternalCallFailed`.

#### Batch Mint

`batchMint` mints to parallel recipient and amount arrays atomically and is gated by `MINT_ROLE`.

#### Extra Metadata

`extraMetadata(key)` reads issuer-defined metadata. `updateExtraMetadata(key, value)` writes it and deletes the entry when `value` is empty.

### Stablecoin

Stablecoin tokens add `currency()`, set once at creation. The value must contain uppercase `A`-`Z` characters only. B20 validates the code format, not the issuer claim, reserves, legal status, or external registration.

## Precompile Addresses

| Precompile | Address |
| - | - |
| B20Factory | `0xB20f000000000000000000000000000000000000` |
| ActivationRegistry | `0x8453000000000000000000000000000000000001` |
| PolicyRegistry | `0x8453000000000000000000000000000000000002` |

## Learn More

* [B20 token standard](/build-on-base/issue-rwa/create-an-asset-token): conceptual introduction to B20 and when to use it
* [Generated reference](/specifications/b20/reference/interfaces/ib20)
* [B20 changelog](/specifications/b20/changelog)
