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
4 changes: 3 additions & 1 deletion docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ npx skills add base/base-skills
|apps:index
|apps/builder-codes:agent-developers,app-developers,builder-codes,wallet-developers
|apps/growth:rewards
|apps/guides:accept-b20-payments,migrate-to-standard-web-app
|apps/guides:accept-b20-payments,migrate-to-standard-web-app,verify-onchain
|apps/quickstart:build-app,deploy-on-base
|apps/resources:design-resources,templates
|apps/technical-guides:base-notifications
Expand Down Expand Up @@ -94,6 +94,8 @@ npx skills add base/base-skills
|base-chain/specs/reference:configurability,glossary
|base-chain/specs/upgrades/azul:exec-engine,node-upgrade,overview,proofs
|base-chain/specs/upgrades/beryl:b20-playground,b20,overview
|base-chain/specs/upgrades/beryl/b20/specification:overview
|base-chain/specs/upgrades/beryl/b20/specification/concepts:architecture-and-precompiles,policies-and-scopes,roles-and-access-control,token-lifecycle,variants-asset-vs-stablecoin
|base-chain/specs/upgrades/canyon:overview
|base-chain/specs/upgrades/cobalt:eip-8130
|base-chain/specs/upgrades/delta:overview,span-batches
Expand Down
2 changes: 1 addition & 1 deletion docs/apps/guides/accept-b20-payments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,6 @@ Call `publicClient.simulateContract` with the same arguments before sending. It

## Related pages

- [B20 token standard](/base-chain/specs/upgrades/beryl/b20): the full interface, including memos, policies, pausing, and roles.
- [B20 token standard](/base-chain/specs/upgrades/beryl/b20/specification/overview): the full interface, including memos, policies, pausing, and roles.
- [Query B20 events](https://docs.cdp.coinbase.com/data/sql-api/b20-events): index `Transfer` and `Memo` events with the CDP SQL API to reconcile payments against orders at scale.
- [Launch a B20 token](/get-started/launch-b20-token): create your own B20 token.
8 changes: 8 additions & 0 deletions docs/base-chain/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
### Introduction
- [Chain Overview](https://docs.base.org/base-chain/overview.md) — Entry point for Base Chain docs

### B20 Token Standard
- [B20 token standard](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/overview.md) — Base-native ERC-20 superset implemented as Rust precompiles
- [Architecture & precompiles](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/architecture-and-precompiles.md) — Fixed precompiles, activation gates, and deterministic token addresses
- [Policies & scopes](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/policies-and-scopes.md) — Token policy slots, PolicyRegistry pointers, built-ins, and composite policies
- [Roles & access control](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/roles-and-access-control.md) — Built-in roles, user-defined role limits, and admin renunciation
- [Token lifecycle](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/token-lifecycle.md) — Creation, initCalls, operation, memos, pausing, seizure, and admin renunciation
- [Variants: Asset vs Stablecoin](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/variants-asset-vs-stablecoin.md) — Variant choice, Asset extras, and Stablecoin currency

### Core Concepts
- [Transaction Ordering](https://docs.base.org/base-chain/network-information/transaction-ordering.md) — How blocks are ordered on Base
- [Transaction Finality](https://docs.base.org/base-chain/network-information/transaction-finality.md) — Detailed information about transaction finality on Base
Expand Down
5 changes: 5 additions & 0 deletions docs/base-chain/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
## Introduction
- [Chain Overview](https://docs.base.org/base-chain/overview.md) — Start here for Base Chain docs, including concepts, network reference, node operation, APIs, and protocol specifications.

## B20 Token Standard
- [B20 token standard](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/overview.md) — ERC-20-compatible native token standard with policy scopes, roles, memos, pausing, supply caps, and variants
- [Architecture & precompiles](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/architecture-and-precompiles.md) — Fixed precompiles, activation gates, and deterministic token addresses
- [Policies & scopes](https://docs.base.org/base-chain/specs/upgrades/beryl/b20/specification/concepts/policies-and-scopes.md) — How B20 token scopes point to PolicyRegistry policy IDs

## Core Concepts
- [Transaction Ordering](https://docs.base.org/base-chain/network-information/transaction-ordering.md) — How blocks are ordered on Base
- [Transaction Finality](https://docs.base.org/base-chain/network-information/transaction-finality.md) — Detailed information about transaction finality on Base
Expand Down
6 changes: 3 additions & 3 deletions docs/base-chain/specs/upgrades/beryl/b20-playground.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ B20 is a superset of ERC-20: every ERC-20 call works unchanged, so there's no co

## A compliance order arrives

A court order targets one account. Freeze it so it can't send, then seize the balance with one call. Seizure only works on an account that is already frozen. Every other holder is untouched and the token keeps trading. See [transfer policies](/base-chain/specs/upgrades/beryl/b20#policy-integration) and [burn](/base-chain/specs/upgrades/beryl/b20#burn) in the spec.
A court order targets one account. Freeze it so it can't send, then seize the balance with one call. Seizure only works on an account that is already frozen. Every other holder is untouched and the token keeps trading. See [transfer policies](/base-chain/specs/upgrades/beryl/b20/specification/concepts/policies-and-scopes) and [freeze-and-seize](/base-chain/specs/upgrades/beryl/b20/specification/concepts/token-lifecycle#freeze-and-seize) in the docs.

<Visibility for="humans">
<B20FlowDemo flow="seize" />
Expand All @@ -40,7 +40,7 @@ A new user shouldn't have to buy ETH first. With `permit`, they sign an approval

## The stock splits

A 2-for-1 split is one call. Every balance doubles without a migration or a new contract. Dividends run inside a public announcement window, so the disclosure lives onchain next to the action. The token also stores identifiers like a CUSIP. See the [Asset variant](/base-chain/specs/upgrades/beryl/b20#asset) in the spec.
A 2-for-1 split is one call. Every balance doubles without a migration or a new contract. Dividends run inside a public announcement window, so the disclosure lives onchain next to the action. The token also stores identifiers like a CUSIP. See the [Asset variant](/base-chain/specs/upgrades/beryl/b20/specification/concepts/variants-asset-vs-stablecoin#asset-extras) in the spec.

<Visibility for="humans">
<B20FlowDemo flow="equity" />
Expand All @@ -55,7 +55,7 @@ A 2-for-1 split is one call. Every balance doubles without a migration or a new
<Card title="Accept B20 payments" href="/apps/guides/accept-b20-payments">
Integration code for memo-tagged payments and B20-specific reverts.
</Card>
<Card title="B20 native token standard" href="/base-chain/specs/upgrades/beryl/b20">
<Card title="B20 native token standard" href="/base-chain/specs/upgrades/beryl/b20/specification/overview">
Every method, event, role, and policy in the standard.
</Card>
<Card title="Beryl overview" href="/base-chain/specs/upgrades/beryl/overview">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: "Architecture & precompiles"
description: "Understand the B20 precompile architecture, fixed addresses, activation gates, and deterministic token addresses."
---

A B20 token is not a contract you deploy. It is a chain-native token surface implemented as a Rust precompile and created through the singleton B20 Factory. Solidity and offchain clients still call normal ABI methods, but there is no token bytecode to verify on an explorer.

## System map

<Frame>
![B20 architecture swimlane: issuer or app calls the B20 Factory, the factory checks ActivationRegistry and creates a B20 token, and token operations consult PolicyRegistry.](/images/b20/b20-architecture-precompiles.png)
</Frame>

| Surface | Address |
|---|---|
| B20 Factory | `0xB20f000000000000000000000000000000000000` |
| ActivationRegistry | `0x8453000000000000000000000000000000000001` |
| PolicyRegistry | `0x8453000000000000000000000000000000000002` |

These addresses are identical on every network where B20 is active.

## Activation gates

The ActivationRegistry is the chain-level feature flag surface. The factory checks activation before creating a token variant, and state-changing PolicyRegistry calls are activation-gated.

<Check>
PolicyRegistry read functions are always callable. Integrations can safely call `isAuthorized`, `policyExists`, `policyAdmin`, and `pendingPolicyAdmin` without an activation write path.
</Check>

## Address derivation

B20 token addresses are deterministic and include the variant byte:

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

You can identify a token's variant from the address without an RPC call by inspecting byte 10, zero-indexed:

| Variant | Byte | Address shape |
|---|---:|---|
| Asset | `0x00` | `0xB200...` |
| Stablecoin | `0x01` | `0xB201...` |

Use `IB20Factory.getB20Address(variant, deployer, salt)` to precompute the address and `isB20` / `isB20Initialized` to verify it.

## What precompiles change for developers

- Import interfaces from `base-std` instead of copying ABIs by hand.
- Use `StdPrecompiles` handles for the factory and registries.
- Do not expect explorer bytecode verification for token or registry addresses.
- Test with base-std mocks, `base-forge`, or `base-anvil` because stock EVM tooling does not know these precompiles by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
title: "Policies & scopes"
description: "Learn how B20 policy scopes point to PolicyRegistry policies and gate transfers, mints, and seizures."
---

A B20 token chooses policies by scope. Each scope stores one `uint64` policy ID that points to a policy in the singleton PolicyRegistry. The token only checks the policy assigned to the relevant scope.

<Frame>
![B20 policies and scopes diagram: fixed token policy scopes store uint64 pointers to PolicyRegistry policies, and gated operations call isAuthorized before continuing or reverting.](/images/b20/b20-policies-and-scopes.png)
</Frame>

On a gated operation, the token reads the relevant scope, calls `PolicyRegistry.isAuthorized(policyId, account)`, and reverts with `PolicyForbids` or a seize-specific error when the policy result does not permit the action.

## Policy scopes

| Scope | Checked account | Gated 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` only when `msg.sender != from` |
| `MINT_RECEIVER_POLICY` | `to` | `mint` and `mintWithMemo` |
| `SEIZE_HOLDER_POLICY` | `from` | `seizeWithMemo`; the holder is seizable only when not authorized by this policy |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the 6th policy scope. B20Constants.sol / IB20.sol:274 define SEIZE_RECEIVER_POLICY (gates the seize to recipient), so there are 6 scopes, not 5. Add a row here and update the bytes32[5] array below (~line 75) to include it — as written the audit sample silently skips a real scope.


`approve` and `permit` are not policy-gated. Only balance movement is checked.

<Warning>
Every scope defaults to `ALWAYS_ALLOW` (`0`) at creation. An unattended B20 deployment is fully open, and no account is seizable until `SEIZE_HOLDER_POLICY` is intentionally configured.
</Warning>

## Policy types

| Type | Behavior |
|---|---|
| `BLOCKLIST` | Accounts are authorized by default; listed accounts are denied. |
| `ALLOWLIST` | Accounts are denied by default; listed accounts are authorized. |
| `UNION` | Composite policy: account is authorized if any child policy authorizes it. |
| `INTERSECT` | Composite policy: account is authorized only if every child policy authorizes it. |

Composite policies combine existing simple `ALLOWLIST` and `BLOCKLIST` policies. Child policies must be simple policies, not other composites or built-ins.

## Built-ins and ID anatomy

Policy IDs are `uint64` values. The top byte is the `PolicyType`; the low 56 bits are a global counter. Counters `0` and `1` are reserved for built-ins, and custom policy creation starts at counter `2`.

| Constant | ID | Behavior |
|---|---:|---|
| `ALWAYS_ALLOW` | `0` | Authorizes every account. Default value for every scope. |
| `ALWAYS_BLOCK` | `(uint64(ALLOWLIST) &lt;&lt; 56) \| 1` | Denies every account. |

<Warning>
`isAuthorized` never reverts on a non-existent policy ID. Malformed or uncreated IDs collapse to empty-set semantics: `ALLOWLIST` denies and `BLOCKLIST` allows. Validate `policyExists(policyId)` before binding a token scope.
</Warning>

```solidity
uint64 policyId = 0x0100000000000002;
require(StdPrecompiles.POLICY_REGISTRY.policyExists(policyId), "policy missing");
IB20(token).updatePolicy(B20Constants.MINT_RECEIVER_POLICY, policyId);
```

## Registry administration

Each policy has one admin. The current admin can:

- Update allowlist, blocklist, or composite membership.
- Stage a two-step admin transfer with `stageUpdateAdmin`.
- Permanently renounce policy administration with `renounceAdmin`.

<Warning>
`renounceAdmin(policyId)` freezes the policy forever. Membership and child-policy updates become impossible.
</Warning>

## Read a token's configured policies

```solidity
bytes32[5] memory scopes = [
B20Constants.TRANSFER_SENDER_POLICY,
B20Constants.TRANSFER_RECEIVER_POLICY,
B20Constants.TRANSFER_EXECUTOR_POLICY,
B20Constants.MINT_RECEIVER_POLICY,
B20Constants.SEIZE_HOLDER_POLICY
];

for (uint256 i; i < scopes.length; i++) {
uint64 id = IB20(token).policyId(scopes[i]);
bool exists = id == 0 || id == ((uint64(uint8(IPolicyRegistry.PolicyType.ALLOWLIST)) << 56) | 1)
|| StdPrecompiles.POLICY_REGISTRY.policyExists(id);
require(exists, "scope points to missing policy");
}
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: "Roles & access control"
description: "Understand B20 roles, access-control gates, user-defined roles, and irreversible admin renunciation."
---

B20 is an ERC-20 superset with built-in role-based access control. Its role API uses familiar admin, grant, revoke, and renounce patterns, but B20 only enforces the built-in roles listed here.

## Base roles

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

<Warning>
User-defined roles have no built-in enforcement. You can create them with `setRoleAdmin` and grant them with `grantRole`, but B20 token functions only check the built-in roles.
</Warning>

## Admin renunciation

B20 has a specific last-admin rule. The last `DEFAULT_ADMIN_ROLE` holder cannot renounce or be revoked through normal role methods; those calls revert with `LastAdminCannotRenounce`.

Use `renounceLastAdmin()` to permanently move the token to an admin-less state. A token can also launch admin-less by passing `initialAdmin == address(0)` at creation.

After admin renunciation:

- `DEFAULT_ADMIN_ROLE`-gated operations are permanently uncallable.
- Existing operational role grants continue to work.
- Admin resurrection is blocked; `grantRole`, `revokeRole`, and `setRoleAdmin` revert even through custom admin-role chains.

## Pre-renunciation checklist

Configure every surviving operational path before renouncing the last admin.

```solidity
IB20 token = IB20(tokenAddress);

// 1. Grant roles that must survive admin renunciation.
token.grantRole(B20Constants.MINT_ROLE, issuerOps);
token.grantRole(B20Constants.PAUSE_ROLE, incidentResponder);
token.grantRole(B20Constants.UNPAUSE_ROLE, governanceSafe);
token.grantRole(B20Constants.SEIZE_ROLE, complianceSafe);

// 2. Bind policies and supply cap while DEFAULT_ADMIN_ROLE still exists.
token.updatePolicy(B20Constants.MINT_RECEIVER_POLICY, mintAllowlistPolicyId);
token.updatePolicy(B20Constants.SEIZE_HOLDER_POLICY, seizeHolderPolicyId);
token.updateSupplyCap(1_000_000e18);

// 3. Permanently remove token administration.
token.renounceLastAdmin();
```

<Note>
For an admin-less launch from creation, put the required grants and policy updates in `initCalls`, then set `initialAdmin` to `address(0)` in the create params.
</Note>

Loading
Loading