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

# B20 Native Token Standard

> B20 is Base's native token standard - designed for stablecoin issuers, real-world asset (RWA) and equity issuers, and long-tail token creators.

B20 is the Base ecosystem's own version of [ERC-20](https://eips.ethereum.org/EIPS/eip-20). It ships with a built-in compliance toolkit: transfer policies, freeze-and-seize, role-based access control, memos, and supply caps. The full interface specs are available in the [Base Standard Library](https://github.com/base/base-std) repository.

B20 supports two variants:

| Variant        | Decimals            | Additional Features                                        |
| -------------- | ------------------- | ---------------------------------------------------------- |
| **Asset**      | 6–18 (configurable) | Rebase multiplier, onchain announcements, batched issuance |
| **Stablecoin** | 6 (fixed)           | Self-declared fiat currency code                           |

## ERC-20 Compatibility

B20 tokens are implemented as **Rust precompiles** rather than EVM smart contracts, making them faster, cheaper, and more native to the chain. All tokens are deployed via the singleton `IB20Factory` precompile.

<Check>
  B20 implements the full ERC-20 standard surface with complete selector parity - it is a drop-in replacement for existing tooling and integrations.
</Check>

## Roles Model

B20 role-based access control extends OpenZeppelin `AccessControl` with a fixed set of roles and one behavioral override on admin renunciation.

| Role                 | Gates                                                                 |
| -------------------- | --------------------------------------------------------------------- |
| `DEFAULT_ADMIN_ROLE` | All admin operations: role grants, policy updates, supply-cap changes |
| `MINT_ROLE`          | `mint`, `mintWithMemo`                                                |
| `BURN_ROLE`          | Caller-side burns: `burn`, `burnWithMemo`                             |
| `BURN_BLOCKED_ROLE`  | Third-party burns against policy-blocked accounts: `burnBlocked`      |
| `PAUSE_ROLE`         | `pause`                                                               |
| `UNPAUSE_ROLE`       | `unpause`                                                             |
| `METADATA_ROLE`      | `updateName`, `updateSymbol`, `updateContractURI`                     |

User-defined roles are supported via `setRoleAdmin` and `grantRole`. They carry no built-in effect - B20 only enforces gates against the seven roles above.

### Admin Renunciation

The last `DEFAULT_ADMIN_ROLE` holder cannot be removed via `renounceRole` or `revokeRole` (both revert with `LastAdminCannotRenounce`). The dedicated `renounceLastAdmin()` is the only path to permanently transition a token to admin-less.

Tokens that intend to launch admin-less from the start pass `initialAdmin == address(0)` at creation, which never grants the role and skips the `renounceLastAdmin` step entirely.

After `renounceLastAdmin()` (or for tokens deployed with `initialAdmin == address(0)`):

* Operations gated by `DEFAULT_ADMIN_ROLE` become permanently uncallable.
* Roles already granted to other addresses (`MINT_ROLE`, `BURN_ROLE`, etc.) continue to function independently.
* Admin resurrection is blocked: `grantRole`, `revokeRole`, and `setRoleAdmin` all revert with `AccessControlUnauthorizedAccount` even if the caller holds a custom role.

## Policy Registry

The PolicyRegistry is a singleton precompile that manages allowlists and blocklists. B20 tokens reference policies by `uint64` ID. Any caller can create a policy and nominate its admin.

<Note>
  State-changing functions on the PolicyRegistry are gated by the ActivationRegistry, which tracks which Base features are live. Read functions (`isAuthorized`, `policyExists`, `policyAdmin`, `pendingPolicyAdmin`) are always callable.
</Note>

### Policy Types

| Type        | Default    | Behavior                                                                   |
| ----------- | ---------- | -------------------------------------------------------------------------- |
| `BLOCKLIST` | Authorized | All accounts authorized by default; explicitly listed accounts are denied. |
| `ALLOWLIST` | Denied     | All accounts denied by default; explicitly listed accounts are authorized. |

### Policy IDs

Policy IDs are `uint64` values. The top byte encodes the `PolicyType`; the low 56 bits are a global counter starting at `2`.

Two built-in IDs require no creation:

| Constant       | ID                               | Behavior                                                                         |
| -------------- | -------------------------------- | -------------------------------------------------------------------------------- |
| `ALWAYS_ALLOW` | `0`                              | Authorizes every account unconditionally. Default scope value on new B20 tokens. |
| `ALWAYS_BLOCK` | `(uint64(ALLOWLIST) << 56) \| 1` | Denies every account unconditionally.                                            |

`isAuthorized` never reverts on a non-existent policy ID - it collapses to empty-member-set semantics (non-existent `BLOCKLIST` authorizes everyone; non-existent `ALLOWLIST` denies everyone).

<Warning>
  Consumers that write a policy ID (e.g. `updatePolicy`) MUST validate `policyExists(policyId)` at write time to avoid silently binding to an unintended empty-set policy.
</Warning>

### Admin Model

Each policy has one admin. Admin transfers are two-step: the current admin calls `transferPolicyAdmin(policyId, newAdmin)`, then the pending admin calls `acceptPolicyAdmin(policyId)`. `renouncePolicyAdmin(policyId)` permanently freezes the policy - membership can never be changed again.

### Creating and Managing Policies

```solidity theme={null}
// Create a policy
uint64 policyId = policyRegistry.createPolicy(PolicyType.BLOCKLIST, adminAddress);

// Update membership (batched)
policyRegistry.addToPolicy(policyId, accounts);
policyRegistry.removeFromPolicy(policyId, accounts);
```

### Read Interface

| Method                            | Description                                                      |
| --------------------------------- | ---------------------------------------------------------------- |
| `isAuthorized(policyId, account)` | Whether `account` is authorized under `policyId`. Never reverts. |
| `policyExists(policyId)`          | Whether a policy with this ID has been created.                  |
| `policyAdmin(policyId)`           | Current admin address.                                           |
| `pendingPolicyAdmin(policyId)`    | Pending admin during a two-step transfer.                        |

## Policy Integration

B20 declares a fixed set of policy scopes. Each scope stores a `uint64` policy ID pointing into the PolicyRegistry. On every gated operation, B20 calls `isAuthorized` against the relevant scope and reverts with `PolicyForbids` if the account is not authorized.

| Scope                      | Gates                                                          |
| -------------------------- | -------------------------------------------------------------- |
| `TRANSFER_SENDER_POLICY`   | The `from` of `transfer` / `transferFrom`                      |
| `TRANSFER_RECEIVER_POLICY` | The `to` of `transfer` / `transferFrom`                        |
| `TRANSFER_EXECUTOR_POLICY` | The `msg.sender` of `transferFrom` (not checked on `transfer`) |
| `MINT_RECEIVER_POLICY`     | The `to` of `mint`                                             |

`approve` is not policy-gated - only actual balance movement via `transfer` / `transferFrom` is checked.

<Warning>
  Every scope defaults to `ALWAYS_ALLOW` at token creation unless overridden in the bootstrap `initCalls`. An unattended B20 deployment is fully open - token behavior must be intentionally constrained.
</Warning>

Scopes are read via `policyId(scope)` and written via `updatePolicy(scope, policyId)`. `updatePolicy` is admin-gated and reverts if the scope is not recognized.

## Mint

New supply is created via `mint` / `mintWithMemo`, gated by `MINT_ROLE`. The recipient is policy-checked against `MINT_RECEIVER_POLICY`. The operation reverts with `SupplyCapExceeded` if it would push `totalSupply` past the cap.

## Burn

Two burn paths exist:

* **`burn` / `burnWithMemo`** - caller burns from their own balance. Gated by `BURN_ROLE`.
* **`burnBlocked`** - burns from a third party's balance. Gated by `BURN_BLOCKED_ROLE`. The target account MUST be denied by `TRANSFER_SENDER_POLICY` - this is the freeze-and-seize path for regulated issuers.

## Supply Cap

The supply cap is optional. The sentinel `type(uint256).max` indicates no cap (the default at creation). `updateSupplyCap(newCap)` is admin-gated and emits `SupplyCapUpdated`. Lowering below the current `totalSupply` reverts with `InvalidSupplyCap`.

## Memos

A memo is an optional `bytes32` payload attached to a token operation for off-chain reference. Every memo-emitting operation emits `Memo(address indexed caller, bytes32 indexed memo)` immediately after the operation's primary event. Indexers join via `(transactionHash, logIndex − 1)`.

Memo-emitting entrypoints: `transferWithMemo`, `transferFromWithMemo`, `mintWithMemo`, `burnWithMemo`.

## Pause

Pauses are granular: the `PausableFeature` enum partitions the token surface into independently pausable operations - `TRANSFER`, `MINT`, and `BURN`. The enum is append-only. `pause(features)` and `unpause(features)` are gated by separate roles (`PAUSE_ROLE` and `UNPAUSE_ROLE`) by design.

## ERC-2612 Permit / EIP-712

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

## Contract URI (ERC-7572)

`contractURI()` returns a string pointing to off-chain metadata per ERC-7572. `updateContractURI(newUri)` is gated by `METADATA_ROLE`.

## Metadata Updates

`METADATA_ROLE` gates:

* `updateName(newName)` - updates `name` and rotates the EIP-712 domain separator. Emits `NameUpdated` and `EIP712DomainChanged`.
* `updateSymbol(newSymbol)` - updates `symbol` only. Emits `SymbolUpdated`.

## Factory

All B20 tokens are created through the singleton `IB20Factory` precompile via `createB20(variant, params, initCalls, salt)`.

| Parameter   | Description                                                                              |
| ----------- | ---------------------------------------------------------------------------------------- |
| `variant`   | `ASSET` or `STABLECOIN`                                                                  |
| `params`    | ABI-encoded, variant-specific creation struct (versioned by leading byte)                |
| `initCalls` | Optional array of ABI-encoded calls dispatched post-creation with admin privilege bypass |
| `salt`      | Caller-chosen entropy for address derivation                                             |

### Address Derivation

B20 addresses are deterministic and encode the variant directly:

```
[10-byte B20 prefix][1-byte variant][9-byte keccak256(deployer, salt)]
```

The variant is recoverable from the address alone without an RPC call. Helper functions `getB20Address(variant, deployer, salt)`, `isB20(addr)`, and `isB20Initialized(addr)` are available on the factory.

### initCalls Semantics

`initCalls` are dispatched after token creation with admin privilege bypass, allowing configuration (e.g. setting policies, granting roles) in the same transaction as deployment. The bypass is partial:

* `MINT_RECEIVER_POLICY` is always enforced even during `initCalls`.
* Pause state is never bypassed.
* Token invariants (supply cap, etc.) are never bypassed.

## Variants

### Asset

The general-purpose variant for assets of all kinds. Decimals are configurable between 6 and 18 at deployment time and are immutable after creation.

In addition to the base B20 surface, Asset tokens add an `OPERATOR_ROLE` that gates the following capabilities:

#### Multiplier

A WAD-precision rebase multiplier applied to all balance reads. Raw balances are stored unchanged; the multiplier scales the view returned to callers.

| Method                            | Description                                      |
| --------------------------------- | ------------------------------------------------ |
| `multiplier()`                    | Current WAD-precision multiplier                 |
| `scaledBalanceOf(account)`        | Raw balance × multiplier                         |
| `toScaledBalance(raw)`            | Convert raw amount to scaled                     |
| `toRawBalance(scaled)`            | Convert scaled amount to raw                     |
| `updateMultiplier(newMultiplier)` | Update the multiplier. Gated by `OPERATOR_ROLE`. |

#### Announcements

On-chain disclosure brackets that wrap sensitive operations (e.g. batch mints, multiplier updates) with a public notice period. Gated by `OPERATOR_ROLE`.

`announce(internalCalls, id, description, uri)` emits an `Announcement` event, executes `internalCalls`, then emits `EndAnnouncement`. The `id` must be unique and is enforced forever. Inner call reverts are wrapped in `InternalCallFailed`.

#### Batch Mint

`batchMint(recipients, amounts)` mints to multiple recipients in a single call. Gated by `MINT_ROLE`. Should be wrapped in `announce()` for transparency.

#### Extra Metadata

An arbitrary key/value store for issuer-defined on-chain metadata.

| Method                            | Description                                                                        |
| --------------------------------- | ---------------------------------------------------------------------------------- |
| `extraMetadata(key)`              | Read a value by key                                                                |
| `updateExtraMetadata(key, value)` | Write a value. Gated by `METADATA_ROLE`. Setting an empty value removes the entry. |

### Stablecoin

The fixed-decimals, fiat-backed carveout. Decimals are hard-wired to `6` and are not configurable.

Adds `currency()`, which returns an ISO-style currency code string (e.g. `"USD"`, `"EUR"`). The code is set once at creation via `B20StablecoinCreateParams.currency`, restricted to characters `A`-`Z` only. It is self-declared and not verified against any external registry.
