diff --git a/docs/README.md b/docs/README.md index e2d3311d..ed88e514 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,8 @@ New to B20? 1. [B20 Overview](overview.md) -2. [How B20 Works](architecture.md) +2. [Integration architecture](integration-architecture.md) +3. [How B20 Works](architecture.md) Building something? diff --git a/docs/concepts/token-types.md b/docs/concepts/token-types.md index 29a7087a..b932112d 100644 --- a/docs/concepts/token-types.md +++ b/docs/concepts/token-types.md @@ -1,6 +1,6 @@ # Token Types -*What Asset and Stablecoin are, how `createB20` seals the type into the address, and what each type adds on top of `IB20`. Address encoding and node dispatch are in [Architecture](../architecture.md). Roles and policies are shared across types; see [Roles and Pause](roles-and-pause.md) and [Policies](policies.md).* +*What Asset and Stablecoin are, how `createB20` seals the type into the address, and what each type adds on top of `IB20`. Address shape and how you recognize a live token are in [Identity and code](../integration-architecture.md#3-identity-and-code). Roles and policies are shared across types; see [Roles and Pause](roles-and-pause.md) and [Policies](policies.md).* ## 1. What a token type is @@ -28,7 +28,7 @@ flowchart TD General-purpose tokens, including RWAs, and fiat-pegged tokens need different class-defining fields. One combined surface would put a currency code on every Asset and announcements on every Stablecoin. B20 splits the surface: Asset carries configurable decimals, announcements, a scheduled UI multiplier, extra metadata, and batched mint. Stablecoin carries an immutable currency code and a fixed `6` decimal convention. -If those extras were optional flags on one binary, class rules would be runtime checks. A Stablecoin address could then execute Asset selectors. B20 compiles each variant as a separate native implementation. The node reads address byte `[10]` and runs that variant's logic. A Stablecoin address never executes Asset selectors. An Asset address never executes Stablecoin selectors. +If those extras were optional flags on one binary, class rules would be runtime checks. A Stablecoin address could then execute Asset selectors. B20 keeps the variants separate: byte `[10]` selects which selectors that address accepts. A Stablecoin address never executes Asset selectors. An Asset address never executes Stablecoin selectors. Wallets, indexers, and issuers still need one Factory, one policy model, and one ERC-20 surface. Duplicating that stack per type would split every integration. Shared infrastructure stays shared: Factory, Policy Registry, Activation Registry, and `IB20`. Callers that only need balances, transfers, roles, or policies use `IB20`. Type-specific calls use `IB20Asset` or `IB20Stablecoin` at the same address. @@ -38,7 +38,7 @@ The issuer chooses the variant only in Factory `createB20(variant, salt, params, `params` carries identity. Name, symbol, and `initialAdmin` are shared. Asset adds `decimals`. Stablecoin adds `currency`. The blob is ABI-encoded with a leading `version` byte (currently `1`): `B20AssetCreateParams` or `B20StablecoinCreateParams`. Optional `initCalls` run on the new token in the same transaction. Then the Factory drops access. If that variant is not activated (`B20Asset` / `B20Stablecoin`), `createB20` reverts `FeatureNotActivated`. Deactivating a variant blocks new creation. Existing tokens keep running. -After creation, the node reads the variant byte in the address and runs that variant's logic. How the node recognizes the `0xB2` prefix and the `0xef` stub, and how it dispatches on byte `[10]`, is in [Architecture §2](../architecture.md#2-how-a-token-is-created). +After creation, byte `[10]` is what selects the variant. How you recognize a live token from its `0xB2` prefix and `0xef` code is in [Identity and code](../integration-architecture.md#3-identity-and-code). ## 4. Asset @@ -111,7 +111,7 @@ After return, address byte `[10]` is `0x00`. `decimals()` is `18`. [`IB20Asset`] ### 6.3 A later call -A call to the token address does not choose the type again. The node reads byte `[10]` and runs that variant's logic. +A call to the token address does not choose the type again. Byte `[10]` already selected which selectors that address accepts. ```mermaid flowchart TD diff --git a/docs/integration-architecture.md b/docs/integration-architecture.md new file mode 100644 index 00000000..aad664f4 --- /dev/null +++ b/docs/integration-architecture.md @@ -0,0 +1,114 @@ +# B20 Integration Architecture + +*What a caller can observe and rely on. This document describes B20's externally observable architecture and the properties third-party integrators can build against. It does not describe the internal implementation of B20 in `base/base`.* + +*For the product tour, see [Overview](overview.md). For roles, policies, and token types, see [Concepts](concepts/). For the event list, see [Events](reference/events.md).* + +## 1. Mental Model + +A B20 is an account you call like a contract. You send ABI-encoded calldata to an address. The call returns data, reverts with a custom error, or emits events. Balances, transfers, and approvals follow ERC-20. Roles, pause, mint, burn, seize, and policies are part of the same interface. + +The protocol provides that execution. You do not deploy per-token logic, and the account does not carry a program you maintain. Every token of a variant exposes the same interface, so a wallet, an issuer, or an app integrates once. + +Two properties follow for a caller: + +- A successful call commits state at the target address. A revert restores that call's writes, the same way a contract revert does. +- A later view returns the state those writes committed. `balanceOf`, `hasRole`, `isPaused`, and `isAuthorized` read that state directly. + +Asset and Stablecoin share this shape. They differ in the extra interface at the same address. See [Token Types](concepts/token-types.md). + +## 2. System Surface + +Four components matter to an integrator. Three are singletons at fixed addresses. Tokens are many addresses, each created by the Factory. + +| Component | Address | What you call it for | +| --- | --- | --- | +| Factory | `0xB20f000000000000000000000000000000000000` | `createB20`, `getB20Address`, `isB20`, `isB20Initialized` | +| Policy Registry | `0x8453000000000000000000000000000000000002` | Shared allowlists, blocklists, and composite policies | +| Activation Registry | `0x8453000000000000000000000000000000000001` | Whether a Factory or token feature is on | +| B20 token | Derived by `createB20` | Balances, transfers, roles, pause, mint, burn, seize, and the policy IDs bound to that token | + +The Factory creates a token and then stops. After `createB20` returns, the Factory has no further access to that token. + +The Policy Registry holds the lists. A token stores a policy ID, not the members. Many tokens can store the same ID. An update to that policy changes `isAuthorized` for every token that stores it. Those tokens do not need another `updatePolicy`. + +The Activation Registry is a Base-operated switch. Issuers and apps read it. When a feature is inactive, writes that require it revert with `FeatureNotActivated`. Reads stay available. Turning a variant off stops new `createB20` calls for that variant. Tokens that already exist keep running. + +Constants for these addresses live in [`StdPrecompiles`](../src/StdPrecompiles.sol). + +## 3. Identity and Code + +### 3.1 What appears at a token address + +`getB20Address(variant, sender, salt)` returns the address `createB20` will use. It never reverts. If that account is already occupied, `createB20` reverts `TokenAlreadyExists`. + +The address is self-describing: + +- Byte `[0]` is `0xB2`. +- Bytes `[1:9]` are zero. +- Byte `[10]` is the variant. Asset is `0x00`. Stablecoin is `0x01`. +- The remaining bytes are derived from `keccak256(sender, salt)`. + +Byte `[10]` is the type, and it does not change after `createB20` returns. An Asset address exposes [`IB20`](../src/interfaces/IB20.sol) and [`IB20Asset`](../src/interfaces/IB20Asset.sol). A Stablecoin address exposes `IB20` and [`IB20Stablecoin`](../src/interfaces/IB20Stablecoin.sol). A selector that belongs to the other variant does not run on that address. + +`isB20(address)` reports the `0xB2` prefix only. It can return true for an address the Factory has not created, and it never reverts. `isB20Initialized(address)` is the liveness check. It flips once, when the creating `createB20` returns, and it never reverts. During `initCalls` in that same call, it is still false. + +Before creation, the predicted address has no code. A call to it returns no output. It is not a token yet. + +### 3.2 What `0xef` means + +On creation, the Factory sets the account code to a single byte, `0xef`. That is the entire code. It is a marker, not a program. B20 tokens are not EVM contracts, so there is no bytecode to verify or upgrade at that address. + +`0xef` is the [EIP-3541](https://eips.ethereum.org/EIPS/eip-3541) reserved prefix. Ordinary `CREATE` and `CREATE2` cannot deploy code that starts with it. An address with the `0xB2` prefix and this code came from the Factory. + +### 3.3 How tooling should read that code + +Use the code as an identity check. Call the token through its ABI. + +- `eth_getCode` on a created token returns `0xef`. The size is 1. Both variants use that same byte. Read the variant from address byte `[10]`, or from `B20Created.variant`. +- Treat `isB20` as a prefix filter. Treat `isB20Initialized`, or the pair of prefix plus code `0xef`, as "this token exists." +- Decode calls and logs with `IB20`, plus `IB20Asset` or `IB20Stablecoin`. The code byte is not the interface. +- Index creation from `B20Created`. The Factory emits it once, after identity is sealed and before `initCalls`. + +## 4. Protocol Evolution + +Base changes B20 in protocol upgrades. On Base those upgrades are hardforks. For an integrator, a hardfork can introduce a new precompile, or it can change the logic that answers at an address you already call. + +What stays stable: + +- Singleton addresses do not move. The Factory, the Policy Registry, and the Activation Registry stay at the addresses in [§2](#2-system-surface). +- A token address does not move, and byte `[10]` does not change. +- A call in a past block keeps the result it had in that block. A node that replays history from genesis reaches the same state as a node that was live for those blocks. An upgrade does not rewrite blocks that already executed. +- After the upgrade, new calls use the upgraded behavior at the same address. You do not point your integration at a new token address to pick up the upgrade. +- A call the active protocol cannot serve reverts. There is no fallback to some other behavior. + +Integrate against the interface of the release you are on. A later hardfork can add selectors at these same addresses. Those selectors are absent until that hardfork. + +## 5. Integration Guarantees + +### 5.1 Contractual + +These properties are the external behavior of the interfaces in this repository. You can build on them. + +- Call B20 with the contract ABI at the singleton addresses in [§2](#2-system-surface), and at token addresses from `getB20Address` and `createB20`. +- `createB20` is the only creation path. The address is determined by `(variant, sender, salt)`. +- A completed token has code `0xef` and the address shape in [§3.1](#31-what-appears-at-a-token-address). `isB20Initialized` means creation has returned. `isB20` means the prefix matches. +- Byte `[10]` is fixed after creation. It selects `IB20Asset` or `IB20Stablecoin` in addition to `IB20`. +- A revert restores the state writes of that call. +- After `createB20` returns, the Factory cannot act on that token. +- A Policy Registry update is visible to every token that stores that policy ID. +- Across a hardfork, the addresses in [§2](#2-system-surface) stay put, and historical execution stays put, as in [§4](#4-protocol-evolution). +- An inactive feature rejects the writes that require it with `FeatureNotActivated`. It does not remove existing tokens, and it does not disable reads. + +### 5.2 Non-contractual + +These are outside the integration contract. + +- How the client decides to run the call, and any other detail of the implementation in `base/base`. +- Storage slot numbers and packing. Use the views. Account storage is readable with `eth_getStorageAt`, and the slot layout is not an interface. +- Exact gas. A call can run out of gas and revert. The amounts can change across upgrades. +- Treating `Paused.features` or `Unpaused.features` as the full paused set. +- Treating `isB20 == true` as proof that the token exists. +- Calling `activate` or `deactivate`. Base operates the Activation Registry. Integrations read `isActivated`. +- Selectors, events, or variants that a future hardfork might add. They are not available early, and this document does not promise a specific future feature. +- Executing or extending the `0xef` byte. It is the marker the Factory plants. EIP-3541 rejects a normal deployment whose code starts with that byte. diff --git a/docs/overview.md b/docs/overview.md index 09f92aba..653af217 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -4,7 +4,7 @@ B20 is Base's native token standard for issuing and managing programmable assets This document provides a high-level introduction to B20: what it is, why it exists, the core primitives it exposes, and how those pieces fit together. -For a deeper technical explanation, see [How B20 Works](./architecture.md). +For the contract a caller can rely on, see [Integration architecture](./integration-architecture.md). For how execution works, see [How B20 works](./architecture.md). --- @@ -181,9 +181,13 @@ sequenceDiagram ## Where to Go Next -If you want to understand how B20 works internally: +If you want the contract a caller can rely on: -→ [B20 Architecture](./architecture.md) +→ [Integration architecture](./integration-architecture.md) + +If you want how execution works: + +→ [How B20 works](./architecture.md) If you are integrating B20: