From 07e20d04a72382a8222371f521ec3a8a54364769 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 11:03:04 -0400 Subject: [PATCH 01/13] docs: restructure B20 docs into overview/architecture/guides/concepts/reference Replaces the old docs/B20, docs/PolicyRegistry, and docs/ActivationRegistry pages with an audience-layered structure: a short overview, a canonical architecture doc, per-audience guides (integrator/indexer/implementer), evergreen concept pages, and a reference section. Root README now points into the new docs/ entry point instead of the removed paths. Co-Authored-By: Claude --- README.md | 10 +- docs/ActivationRegistry/README.md | 49 -------- docs/B20/Asset.md | 90 --------------- docs/B20/Factory.md | 48 -------- docs/B20/README.md | 117 ------------------- docs/B20/Stablecoin.md | 13 --- docs/PolicyRegistry/README.md | 181 ------------------------------ docs/README.md | 17 +++ docs/architecture.md | 57 ++++++++++ docs/concepts/assets.md | 26 +++++ docs/concepts/execution.md | 19 ++++ docs/concepts/policies.md | 19 ++++ docs/concepts/roles.md | 15 +++ docs/concepts/versioning.md | 19 ++++ docs/guides/implementers.md | 27 +++++ docs/guides/indexers.md | 27 +++++ docs/guides/integrators.md | 31 +++++ docs/overview.md | 38 +++++++ docs/reference/constants.md | 7 ++ docs/reference/errors.md | 7 ++ docs/reference/events.md | 7 ++ docs/reference/interfaces.md | 16 +++ docs/reference/versions.md | 7 ++ 23 files changed, 346 insertions(+), 501 deletions(-) delete mode 100644 docs/ActivationRegistry/README.md delete mode 100644 docs/B20/Asset.md delete mode 100644 docs/B20/Factory.md delete mode 100644 docs/B20/README.md delete mode 100644 docs/B20/Stablecoin.md delete mode 100644 docs/PolicyRegistry/README.md create mode 100644 docs/README.md create mode 100644 docs/architecture.md create mode 100644 docs/concepts/assets.md create mode 100644 docs/concepts/execution.md create mode 100644 docs/concepts/policies.md create mode 100644 docs/concepts/roles.md create mode 100644 docs/concepts/versioning.md create mode 100644 docs/guides/implementers.md create mode 100644 docs/guides/indexers.md create mode 100644 docs/guides/integrators.md create mode 100644 docs/overview.md create mode 100644 docs/reference/constants.md create mode 100644 docs/reference/errors.md create mode 100644 docs/reference/events.md create mode 100644 docs/reference/interfaces.md create mode 100644 docs/reference/versions.md diff --git a/README.md b/README.md index bb6bc054..e5379b7e 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,13 @@ A collection of Solidity interfaces, libraries, and mock implementations for Bas ## Products -- [**ActivationRegistry**](docs/ActivationRegistry/README.md) — Feature flags controlled by Base team to activate/deactivate features. -- [**PolicyRegistry**](docs/PolicyRegistry/README.md) — Membership sets controlled by custom admins, initially providing allow and block lists for B20 token operations. -- [**B20**](docs/B20/README.md) — Standard ERC-20 implementation with extensions for roles, policies, memos, pausing, ERC-2612 permits, and a variant system. +- [**ActivationRegistry**](src/interfaces/IActivationRegistry.sol) — Feature flags controlled by Base team to activate/deactivate features. +- [**PolicyRegistry**](docs/concepts/policies.md) — Membership sets controlled by custom admins, initially providing allow and block lists for B20 token operations. +- [**B20**](docs/overview.md) — Standard ERC-20 implementation with extensions for roles, policies, memos, pausing, ERC-2612 permits, and a variant system. + +## Documentation + +See [`docs/`](docs/README.md) for the full documentation map: overview, architecture, audience guides (integrator/indexer/implementer), concepts, and reference. ## Changelog diff --git a/docs/ActivationRegistry/README.md b/docs/ActivationRegistry/README.md deleted file mode 100644 index 3d630a7d..00000000 --- a/docs/ActivationRegistry/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# ActivationRegistry - -The ActivationRegistry tracks which Base features are live. This is managed exclusive by the Base and integrators don't typically need to query it. See [`IActivationRegistry`](../../src/interfaces/IActivationRegistry.sol) for the full Solidity interface. - -## Feature IDs - -Feature IDs are opaque `bytes32` values. By convention each is the keccak256 digest of a human-readable feature name (e.g., `keccak256("base.b20_asset")`); a feature ID is permanently bound to its semantic and is never recycled. - -The canonical IDs in use today, defined in [`ActivationRegistryFeatureList`](../../test/lib/mocks/ActivationRegistryFeatureList.sol): - -| Constant | Preimage | Value | -|---|---|---| -| `B20_ASSET` | `"base.b20_asset"` | `0xcdcc772fe4cbdb1029f822861176d09e646db96723d4c1e82ddfdeb8163ef54c` | -| `B20_STABLECOIN` | `"base.b20_stablecoin"` | `0xecfa0def2c10020caaf65e6155aa69c84b24892aaef76eeac52e0e2b3a0b8601` | -| `POLICY_REGISTRY` | `"base.policy_registry"` | `0xb582ebae03f16fee49a6763f78df482fb11ae73f103ed0d330bbe556aa90a43f` | - -## User Flows - -### Activate Feature - -The admin marks a feature as live; downstream consumers can immediately observe the change. - -```mermaid -sequenceDiagram - participant Admin - participant ActivationRegistry - - Admin->>ActivationRegistry: activate(featureId) - Note over ActivationRegistry: features[featureId] = true - ActivationRegistry-->>Admin: emit FeatureActivated(feature, caller) -``` - -Reverts: `Unauthorized` (non-admin caller), `AlreadyActivated`, `DelegateCallNotAllowed` / `StaticCallNotAllowed`. - -### Deactivate Feature - -The admin marks a previously-active feature as inactive. - -```mermaid -sequenceDiagram - participant Admin - participant ActivationRegistry - - Admin->>ActivationRegistry: deactivate(featureId) - Note over ActivationRegistry: features[featureId] = false - ActivationRegistry-->>Admin: emit FeatureDeactivated(feature, caller) -``` - -Reverts: `Unauthorized`, `AlreadyDeactivated`, `DelegateCallNotAllowed` / `StaticCallNotAllowed`. diff --git a/docs/B20/Asset.md b/docs/B20/Asset.md deleted file mode 100644 index 67384bda..00000000 --- a/docs/B20/Asset.md +++ /dev/null @@ -1,90 +0,0 @@ -# B20 Asset - -The Asset variant of B20 — designed for assets of all kinds. Everything in [B20/README.md](README.md) applies; this page covers the deltas only. See [`IB20Asset`](../../src/interfaces/IB20Asset.sol) for the full Solidity interface. - -## Multiplier - -Each account's stored balance is the **raw** balance. A uniform on-chain **multiplier** scales that raw balance into a derived **scaled** view that consumers display. The multiplier applies to all accounts equally, which lets issuers rebase every balance at once — without rewriting individual balances — the shape is similar to wstETH wrapping stETH, where the stored unit is the unwrapped quantity and the derived unit is the rebased view. Because it only rescales the *displayed* balance, the multiplier is purely cosmetic: `balanceOf`, `transfer`, and `totalSupply` stay raw, so raw-denominated venues (AMMs, etc.) are mechanically unaffected by an update. - -Read the current multiplier with `multiplier()`; the value is in WAD precision (`1e18`, exposed as `WAD_PRECISION()`). `toUIAmount(rawAmount)` converts a raw amount to its scaled view, `fromUIAmount(uiAmount)` is the reverse converter (integer-floored, so the round-trip can lose up to one ULP), and `scaledBalanceOf(account)` is a convenience over ERC-20's `balanceOf` that returns the same account's raw balance in its scaled form. (The legacy `toScaledBalance` / `toRawBalance` are retained in `IB20Asset` as deprecated aliases — see [ERC-8056 conformance](#erc-8056-conformance).) - -Both multiplier setters validate `newMultiplier` is non-zero and at most `type(uint128).max` (exposed as `MAX_UI_MULTIPLIER()`, reverting `InvalidMultiplier` otherwise). The `uint128` ceiling is the overflow guard: with supply capped at `type(uint128).max`, a `uint128` multiplier keeps `balance * multiplier` inside `uint256`, so balance-derived reads never overflow. - -### Scheduling multiplier updates - -The standard path for a corporate action (a stock split or reinvested stock dividend) is to **schedule** the change ahead of time with `updateUIMultiplier(newMultiplier, effectiveAt)`, wrapped in an [announcement](#announcements). Evaluation is lazy, so `multiplier()` / `uiMultiplier()` flip on their own once `block.timestamp` reaches `effectiveAt`. - -Only **one pending update is live at a time**. Attempting to schedule over an existing pending update reverts `UIMultiplierUpdateExists`. To reorder overlapping corporate actions, explicitly cancel and re-schedule in a single announcement bracket using `announce([cancelUIMultiplierUpdate, updateUIMultiplier(...)])`. `cancelUIMultiplierUpdate()` clears the live pending and restores the no-pending state (reverting `UIMultiplierUpdateDoesNotExist` when nothing live is scheduled). - -`updateMultiplier(newMultiplier)` is the **deprecated instant failsafe / emergency override**: it sets the multiplier immediately, stamping `effectiveAt = block.timestamp` and clearing any pending update. It is retained in `IB20Asset` (marked deprecated, still dialable) for backward compatibility; prefer the scheduled `updateUIMultiplier` for routine corporate actions. - -The pending schedule is observable through the ERC-8056 surface: `newUIMultiplier()` returns the scheduled target while it is live (otherwise it mirrors `uiMultiplier()`). - -### ERC-8056 conformance - -The Asset variant conforms to [ERC-8056](https://eips.ethereum.org/EIPS/eip-8056) ("Scaled UI Amount"): - -- `uiMultiplier()` is the standard alias of `multiplier()` (core interface `0xa60bf13d`). -- `newUIMultiplier()` / `effectiveAt()` expose the pending schedule (required extension `0x4bd27648`). -- `balanceOfUI(account)` aliases `scaledBalanceOf`, and `totalSupplyUI()` returns `totalSupply() * uiMultiplier() / 1e18` (optional Balances extension `0xd890fd71`). -- `toUIAmount(rawAmount)` / `fromUIAmount(uiAmount)` are the canonical raw ⇄ UI converters (optional Conversion extension `0x57854fc3`), applying the effective multiplier. The legacy `toScaledBalance` / `toRawBalance` are retained as deprecated aliases. -- `supportsInterface(bytes4)` (ERC-165, `0x01ffc9a7`) returns `true` for those four extension IDs and for ERC-165 itself. - -**Events.** Every multiplier change emits `UIMultiplierUpdated(oldMultiplier, newMultiplier, effectiveAtTimestamp)` — from `updateUIMultiplier` and from `updateMultiplier` (which stamps `effectiveAtTimestamp = block.timestamp`), satisfying ERC-8056's "emit on every multiplier change". The deprecated instant setter (`updateMultiplier`) additionally emits the **deprecated** `MultiplierUpdated(newMultiplier)` event alongside `UIMultiplierUpdated`, so indexers still watching the legacy topic keep working through the transition; the scheduled `updateUIMultiplier` emits only `UIMultiplierUpdated`. `UIMultiplierUpdateCancelled(cancelledMultiplier, cancelledEffectiveAt)` is emitted by `cancelUIMultiplierUpdate` and by the instant setter when it clears a *live* pending — so an instant override that supersedes a live schedule emits the cancel, then `MultiplierUpdated`, then `UIMultiplierUpdated`. The optional ERC-8056 `TransferWithUIAmount` event is intentionally omitted — scaled balances are derivable from the raw `Transfer` and the active multiplier. - -### Precision & decimals - -All multiplier-derived reads (`toUIAmount` / `scaledBalanceOf` / `totalSupplyUI` divide by `WAD_PRECISION`; `fromUIAmount` divides by the multiplier) round **down**, and raw balances are never rewritten. This guarantees that rounding loss is rare and confined to the scaled view (and to `fromUIAmount` conversions). In the rare case where rounding loss occurs, the loss cannot exceed 1 wei of the *scaled* amount only. - -**Thus, prefer 18 decimals for equities**: at 6 decimals, a deep reverse split on a very valuable stock could make 1-wei floor dust economically visible; at 18 it stays noise - -### Pause & market-halt policy - -Because a multiplier update is value-neutral to raw venues, forward splits and reinvested dividends need no halt on-chain. A reverse split, however, warrants halting via `PausableFeature.TRANSFER` across the flip window so trading windows are paused and re-enabled in orderly fashion. The instant `updateMultiplier` bypasses the scheduling window entirely, so it should likewise be pause-bracketed. - -## Announcements - -Announcements are publicly viewable notifications posted by a token operator. They can represent anything the operator wants to create a record of and can be coupled with actual state changes on the token (updating the multiplier, batched mints, and so on). - -### Event Topology - -An announcement is delimited by a paired `Announcement(msg.sender, id, description, uri)` event (opens the bracket) and `EndAnnouncement(id)` event (closes it). Every state-changing call dispatched inside the bracket belongs to that announcement. A recursion guard prevents nesting, and each `id` is enforced unique forever (`AnnouncementIdAlreadyUsed`) so indexers can correlate brackets across transactions. - -Indexers should treat every `Announcement` log as the start of exactly one bracket; effects between `Announcement` and `EndAnnouncement` belong to the announced action; effects emitted *without* a surrounding bracket are direct invocations and should be flagged as emergency overrides. - -### Wrapping calls in announcements - -Wrap a set of operations in a single announcement by calling `announce(internalCalls, id, description, uri)`. The function (gated by `OPERATOR_ROLE`) emits `Announcement`, dispatches each internal call via self-`delegatecall` (which preserves `msg.sender` so the inner role checks see the operator), then emits `EndAnnouncement`. Inner reverts are wrapped in `InternalCallFailed` rather than bubbled — replay the call directly to debug. Nested calls to `announce` revert with `AnnouncementInProgress`; calls shorter than 4 bytes revert with `InternalCallMalformed`. - -```solidity -// Disclose and schedule a 2:1 forward split, effective at the ex-date. -bytes[] memory internalCalls = new bytes[](1); -internalCalls[0] = abi.encodeCall(IB20Asset.updateUIMultiplier, (2e18, exDateTimestamp)); - -IB20Asset(token).announce({ - internalCalls: internalCalls, - id: "2026-Q3-split", - description: "2:1 forward split, effective at ex-date", - uri: "https://disclosures.example.com/..." -}); -``` - -## Batch Mint - -`batchMint(recipients, amounts)` mints to many accounts in one call, gated by `MINT_ROLE`. It should be wrapped in `announce()`, which additionally requires the operator to hold `OPERATOR_ROLE` (typically granted as a single bundle). - -## Extra Metadata - -Each Asset token can carry an arbitrary set of named metadata entries — a general-purpose key/value store the issuer is free to use however they want (e.g. `"category"` → `"electronics"`, `"region"` → `"north-america"`, `"reference"` → `"REF-2024-001"`). Read with `extraMetadata(key)`; the value is a `string`. All entries are optional and added post-creation — the factory does not seed any entry at token creation. - -`updateExtraMetadata(key, value)` adds, updates, or removes an entry, gated by `METADATA_ROLE` (the same role that gates `updateName` / `updateSymbol`). It does NOT require `OPERATOR_ROLE` and can be invoked directly without an `announce()` wrapper. Passing an empty `value` removes the entry. An empty `key` reverts with `InvalidMetadataKey`. - -## Additional roles - -### `OPERATOR_ROLE` - -Gates `announce`, `updateUIMultiplier`, `cancelUIMultiplierUpdate`, and the deprecated `updateMultiplier`. These are metadata-like operations — they post disclosures and rescale the displayed balance rather than moving raw balances directly — but a compromised operator carries materially higher severity than ordinary metadata edits, so the capability is elevated into its own independent role instead of being folded into `METADATA_ROLE`. Held separately from `DEFAULT_ADMIN_ROLE` so operators don't need full admin authority. - -## Configurable Decimals - -`decimals()` is chosen at creation via `B20AssetCreateParams.decimals` and immutable thereafter. The factory enforces the inclusive range `[6, 18]` (exposed as `B20Constants.MIN_ASSET_DECIMALS` and `MAX_ASSET_DECIMALS`); out-of-range values revert `InvalidDecimals(decimals)`. `6` is the smallest unit any asset should use and `18` is a reasonable ceiling that encompasses the supermajority of assets. diff --git a/docs/B20/Factory.md b/docs/B20/Factory.md deleted file mode 100644 index e4c17ca5..00000000 --- a/docs/B20/Factory.md +++ /dev/null @@ -1,48 +0,0 @@ -# B20 Factory - -The B20 Factory is the singleton precompile that creates B20 tokens of every variant. Anyone can call its single entry point, `createB20`. See [`IB20Factory`](../../src/interfaces/IB20Factory.sol) for the full Solidity interface. - -## `createB20` parameters - -`createB20` takes four arguments: - -### `variant` - -Selects which variant of B20 to deploy — currently `ASSET` or `STABLECOIN`. See the [variant overview](README.md#variant-overview) for what each one bundles. - -### `params` - -Variant-specific creation arguments, ABI-encoded as a versioned struct (one struct per variant; the leading byte selects the encoding version). Required and optional fields differ per variant — see [`IB20Factory`](../../src/interfaces/IB20Factory.sol) for each variant's struct spec. - -### `initCalls` - -An optional array of ABI-encoded calls dispatched on the new token immediately after creation. These let you configure anything beyond the variant's defined `params` — role grants, mint operations, policy scopes, contract URI, and so on. They execute on the new token as if the factory were the admin, so admin-gated operations are permitted within this window. The factory itself receives no official roles and has no persisted access to the token. - -The bootstrap bypass is deliberately **not total**. During the window, factory-originated calls skip the token's role gates and its transfer-side policy gates (`TRANSFER_SENDER_POLICY`, `TRANSFER_RECEIVER_POLICY`, `TRANSFER_EXECUTOR_POLICY`), but: - -- **`MINT_RECEIVER_POLICY` is always enforced**, even for factory-originated mints — new supply is never issued to a policy-denied recipient, even at creation. If your `initCalls` set a restrictive `MINT_RECEIVER_POLICY` and then mint to a non-authorized account in the same bundle, the mint reverts `PolicyForbids` and the whole `createB20` reverts. Sequence the mint before the restrictive policy, or mint to an authorized recipient. -- **Pause is never bypassed.** It defaults to nothing-paused at creation, so a start-paused configuration must sequence its `pause(...)` call last. -- **Token invariants** (supply-cap math, balance accounting) are never bypassed. - -Build the array with [`B20FactoryLib`](../../src/lib/B20FactoryLib.sol) helpers (or encode manually): - -```solidity -// Configure the new token: cap supply and gate minting on an allowlist. -bytes[] memory initCalls = new bytes[](2); -initCalls[0] = B20FactoryLib.encodeUpdateSupplyCap(1_000_000e18); -initCalls[1] = B20FactoryLib.encodeUpdatePolicy(B20Constants.MINT_RECEIVER_POLICY, mintPolicyId); -``` - -### `salt` - -Caller-chosen entropy that influences the deployed token's address — see [B20 Address Derivation](#b20-address-derivation). - -## B20 Address Derivation - -B20 addresses are deterministic: `[B20 prefix (10 bytes)][variant byte (1 byte)][bytes9(keccak256(deployer, salt))]`. The variant byte being recoverable from the address means off-chain tooling can identify the variant without an RPC call. - -`getB20Address(variant, deployer, salt)` predicts the address before deployment. `isB20(address)` matches against the prefix pattern (recovered from the address with no storage read), and `isB20Initialized(address)` flips true exactly once when `createB20` completes at that address. - -## Composing with the factory - -The factory is callable from any account, including from your own contract. Wrapping the factory is the standard path for layering access control on top of permissionless creation, bundling defaults into a higher-level builder, or defining a custom salting scheme. diff --git a/docs/B20/README.md b/docs/B20/README.md deleted file mode 100644 index 2171d650..00000000 --- a/docs/B20/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# B20 - -B20 is an ERC-20 superset designed for Base. All B20s are deployed via the singleton `IB20Factory` precompile (see [Factory](Factory.md)). - -B20 supports two variants: - -- **[Asset](Asset.md)** — the general-purpose variant for assets of all kinds -- **[Stablecoin](Stablecoin.md)** — the fixed-decimals, fiat-backed carveout - -This document covers the behavior shared across the variant family. - -## ERC-20 - -Implements the [ERC-20](https://eips.ethereum.org/EIPS/eip-20) standard surface with full selector parity — drop-in for existing tooling. - -## Roles model - -B20 role-based access control follows from [OZ AccessControl](https://docs.openzeppelin.com/contracts/5.x/access-control) with a fixed set of custom roles and one behavior override on admin renunciation. - -Standard role taxonomy: - -| 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` | 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 have no built-in effect; B20 only enforces gates against the seven roles above. - -Roles are granted, revoked, and renounced through the standard OZ AccessControl methods. The one departure: the last `DEFAULT_ADMIN_ROLE` holder cannot be removed via `renounceRole` or `revokeRole` (both revert with `LastAdminCannotRenounce`); the dedicated `renounceLastAdmin()` is the only path that permanently transitions the 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 that were already granted to other addresses (`MINT_ROLE`, `BURN_ROLE`, `PAUSE_ROLE`, `UNPAUSE_ROLE`, `METADATA_ROLE`, etc.) continue to function independently. Admin-resurrection is blocked: `grantRole`, `revokeRole`, and `setRoleAdmin` all revert with `AccessControlUnauthorizedAccount` on an admin-less token, even if the caller holds a custom role that would normally satisfy the meta-role gate. A custom-admin chain such as `setRoleAdmin(MINT_ROLE, BURN_ROLE) → grantRole(BURN_ROLE, X)` cannot restore admin power. - -## Policy integration - -B20 declares a fixed set of *policy scopes*. Each scope stores a `uint64` policy ID that points into the [PolicyRegistry](../PolicyRegistry/README.md); on every gated operation, B20 calls `isAuthorized` against the relevant scope and reverts (`PolicyForbids`) if the account isn't authorized. - -Scope names follow the `{ACTION}_{ACTOR}_POLICY` convention: - -| 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 consulted on `transfer`) | -| `MINT_RECEIVER_POLICY` | The `to` of `mint` | - -`approve` itself is not policy-gated — only the actual movement of balance via `transfer` / `transferFrom` is checked. A blocked address can hold or receive allowances; the gate fires when balance moves. - -Because scopes are per-actor, send-side and receive-side rules can be configured independently. Common patterns include allowlisting receivers while leaving sends open (e.g. KYC-only deposits) and restricting `MINT_RECEIVER_POLICY` to a custodian set while leaving everyday transfers unrestricted. - -> ⚠️ **Every scope defaults to `ALWAYS_ALLOW` at token creation** unless overridden in the bootstrap `initCalls`. Token behavior must be intentionally constrained — an unattended deployment of B20 is fully open. - -Scopes are read via `policyId(scope)` and written via `updatePolicy(scope, policyId)`. `updatePolicy` is admin-gated and reverts if the scope isn't recognized. - -See [PolicyRegistry](../PolicyRegistry/README.md) for registry mechanics (built-in policy IDs, encoding, admin lifecycle). - -## Mint - -New supply is created via `mint` / `mintWithMemo`, gated by `MINT_ROLE`. The recipient is policy-checked against `MINT_RECEIVER_POLICY`, and the operation reverts with `SupplyCapExceeded` if it would push `totalSupply` past the cap. - -## Burn - -Two burn paths serve two operational needs: - -- **`burn` / `burnWithMemo`** — caller burns from their own balance. Gated by `BURN_ROLE`. Permissioned so asset issuers can maintain equivalent units for wrapped assets without exposing supply to arbitrary holders. -- **`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 required by regulated issuers, deliberately impossible against accounts that aren't policy-blocked. - -## Supply cap - -The supply cap is optional; the sentinel `type(uint256).max` indicates no cap and is the default at creation. `updateSupplyCap(newCap)` is admin-gated and emits `SupplyCapUpdated` — the cap may be raised or lowered freely, but lowering below current `totalSupply` reverts with `InvalidSupplyCap` because already-issued supply is never invalidated. - -## Memos - -A memo is an optional `bytes32` payload that callers attach to a token operation for off-chain reference — payment IDs, compliance tagging, settlement correlation, etc. - -Every memo'd operation emits a `Memo(address indexed caller, bytes32 indexed memo)` event immediately after the operation's primary event, with a `bytes32(0)` memo permitted as a "no memo content" signal. Indexers join the `Memo` log to its parent via `(transactionHash, logIndex − 1)` — the memo always sits immediately after its primary event in log order. - -Memo-emitting entrypoints: - -- `transferWithMemo`, `transferFromWithMemo` — same semantics as their non-memo counterparts plus the `Memo` event. -- `mintWithMemo`, `burnWithMemo` — same pattern on issuance and self-burn. - -## Pause - -B20 pauses are granular: the `PausableFeature` enum partitions the gated surface into independently pausable operations, currently `TRANSFER`, `MINT`, and `BURN`. The enum is append-only across protocol versions, so existing positions are stable forever. `isPaused(feature)` is `O(1)`; `pausedFeatures()` returns the full set as an array. - -`pause(features)` and `unpause(features)` are gated by *separate* roles (`PAUSE_ROLE` and `UNPAUSE_ROLE`) by design — an incident-response operator can pause without holding the authority to re-enable. - -## ERC-2612 Permit / EIP-712 - -B20 implements [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) (signed approvals) using an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) domain shaped as `(name, version, chainId, verifyingContract)`, with `version` fixed at `"1"` and `salt` unused. Because `name` is re-hashed into the domain on every signed call, `updateName` automatically rotates the domain separator; each successful `updateName` emits one `EIP712DomainChanged` event ([ERC-5267](https://eips.ethereum.org/EIPS/eip-5267)). - -`DOMAIN_SEPARATOR()` and `eip712Domain()` are exposed for callers that want to read the domain dynamically rather than reconstruct it. `nonces(owner)` is the per-account replay counter incremented on every `permit`. - -ERC-1271 contract signatures are deliberately NOT accepted — permit recovers via ECDSA from 65-byte signatures only. Smart-contract accounts should use call-batching or gasless flows. [Permit2](https://github.com/Uniswap/permit2) is usable as a periphery alternative. - -## Contract URI (ERC-7572) - -`contractURI()` returns a string pointing to off-chain metadata about the token (typically a JSON document) per [ERC-7572](https://eips.ethereum.org/EIPS/eip-7572). `updateContractURI(newUri)` is gated by `METADATA_ROLE`. - -## Metadata updates - -`METADATA_ROLE` gates two metadata setters: - -- `updateName(newName)` updates the token name AND rotates the EIP-712 domain separator (see [ERC-2612 Permit / EIP-712](#erc-2612-permit--eip-712)). Emits `NameUpdated` and `EIP712DomainChanged`. -- `updateSymbol(newSymbol)` updates the symbol with no other side effects. Emits `SymbolUpdated`. - -## Variant overview - -| Variant | Decimals | What it adds | -|---|---|---| -| [Asset](Asset.md) | 6-18 (configurable per token) | multiplier, announcements, extra metadata, batched issuance | -| [Stablecoin](Stablecoin.md) | 6 (fixed) | self-declared currency code | diff --git a/docs/B20/Stablecoin.md b/docs/B20/Stablecoin.md deleted file mode 100644 index 62e5fb5c..00000000 --- a/docs/B20/Stablecoin.md +++ /dev/null @@ -1,13 +0,0 @@ -# B20 Stablecoin - -The Stablecoin variant of B20. Everything in [B20/README.md](README.md) applies; this page covers the deltas only. See [`IB20Stablecoin`](../../src/interfaces/IB20Stablecoin.sol) for the Solidity interface. - -## Currency Codes - -`currency()` returns the ISO-style currency code as a `string` (e.g., `"USD"`, `"EUR"`). It is set once via `B20StablecoinCreateParams.currency` at creation, immutable thereafter, and restricted to `A`–`Z` bytes (no lowercase, no digits, no separators). - -The value is **self-declared** — the contract does not verify it against any registry or allowlist. Wallets and indexers can use it to group stablecoins by underlying fiat without an external lookup, but it is not a proof of fiat backing. - -## Fixed Decimals (6) - -`decimals()` is hard-wired to `6`. The choice matches existing popular stablecoins. diff --git a/docs/PolicyRegistry/README.md b/docs/PolicyRegistry/README.md deleted file mode 100644 index bc8e57a2..00000000 --- a/docs/PolicyRegistry/README.md +++ /dev/null @@ -1,181 +0,0 @@ -# PolicyRegistry - -The PolicyRegistry is a singleton precompile for list-based and composite access policies. Any caller can create a policy and nominate its admin; B20 tokens and other consumers reference policies by `uint64` ID for authorization checks. See [`IPolicyRegistry`](../../src/interfaces/IPolicyRegistry.sol) for the full Solidity interface. - -## Policy Types - -Four policy types are supported, split into two kinds: - -**Simple** policies decide from an address set: - -- **`BLOCKLIST`** — accounts are authorized by default; the admin maintains a list of accounts to explicitly deny. -- **`ALLOWLIST`** — accounts are denied by default; the admin maintains a list of accounts to explicitly authorize. - -**Composite** policies decide by combining existing simple policies under a logic gate: - -- **`UNION`** (OR) — authorized if *any* child policy authorizes the account. -- **`INTERSECT`** (AND) — authorized only if *every* child policy authorizes the account. - -A composite's child set is 2–4 existing simple (`ALLOWLIST`/`BLOCKLIST`) policy IDs — never another composite, and never a built-in sentinel (`ALWAYS_ALLOW`/`ALWAYS_BLOCK`). Composites reference their children live: `isAuthorized` reads current child membership on every call. So updating a child's membership immediately changes what the composite authorizes. - -## Policy IDs - -Each policy is identified by a `uint64` ID. The top byte (`[63:56]`) encodes the `PolicyType`; the low 56 bits (`[55:0]`) are a global counter. Type is recoverable from any ID via pure bit extraction, with no storage read. - -Custom policy IDs are assigned from a single global counter starting at `2`. The values `0` and `1` are reserved for two **built-in policies** that consumers can reference on a slot without creating a policy: - -| Policy | Value | Semantics | -|---|---|---| -| `ALWAYS_ALLOW` | `0` | `isAuthorized(ALWAYS_ALLOW, *) → true` | -| `ALWAYS_BLOCK` | `(uint64(ALLOWLIST) << 56) \| 1` | `isAuthorized(ALWAYS_BLOCK, *) → false` | - -`ALWAYS_ALLOW` is also the default state of every unassigned policy slot on a B20 token. - -> **Precondition for consumers.** `isAuthorized` never reverts on a non-existent or malformed `policyId` — it collapses to empty-member-set semantics (ALLOWLIST → `false`, BLOCKLIST → `true`). Consumers that store policy IDs (notably `IB20.updatePolicy`) MUST validate `policyExists(policyId)` at write time, since a typo'd BLOCKLIST ID would silently behave as `ALWAYS_ALLOW`. - -## Activation - -The `PolicyRegistry` is gated by the [`ActivationRegistry`](../ActivationRegistry/README.md). The gate applies only to functions that change state; read-only functions are always callable, whether or not the feature is active. - -**Always callable:** - -- `isAuthorized` -- `policyExists` -- `policyAdmin` -- `pendingPolicyAdmin` -- `compositePolicyChildIds` -- `MIN_COMPOSITE_CHILD_POLICIES` -- `MAX_COMPOSITE_CHILD_POLICIES` - -**Gated** — revert with `FeatureNotActivated` while the feature is inactive: - -- `createPolicy` -- `createPolicyWithAccounts` -- `createCompositePolicy` -- `stageUpdateAdmin` -- `finalizeUpdateAdmin` -- `renounceAdmin` -- `updateAllowlist` -- `updateBlocklist` -- `updateComposite` - -Because reads are never gated, a consumer — a B20 token calling `isAuthorized` on transfer, or an indexer reading membership and admin state — sees the same behavior whether or not the feature is active. - -## User Flows - -### Create Policy - -A caller deploys a new policy, nominates its admin (often themselves or a multisig), and optionally seeds an initial member set in the same call. - -```mermaid -sequenceDiagram - participant Creator - participant PolicyRegistry - - Creator->>PolicyRegistry: createPolicy(admin, policyType) - Note over PolicyRegistry: allocate new policyId
store type and admin - PolicyRegistry-->>Creator: emit PolicyCreated(policyId, creator, policyType) - PolicyRegistry-->>Creator: emit PolicyAdminUpdated(policyId, 0, admin) -``` - -Use `createPolicyWithAccounts(admin, policyType, accounts)` for the seeded variant — same shape, plus a membership seeding step that emits `AllowlistUpdated` or `BlocklistUpdated` (depending on `policyType`) carrying the full batch. - -Reverts: `ZeroAddress` (if `admin` is `address(0)`), `BatchSizeTooLarge` (seeded variant only). - -### Create Composite Policy - -A caller combines 2–4 existing simple policies under a `UNION` or `INTERSECT` gate and nominates an admin for the composite. - -```mermaid -sequenceDiagram - participant Creator - participant PolicyRegistry - - Creator->>PolicyRegistry: createCompositePolicy(admin, policyType, childPolicyIds) - Note over PolicyRegistry: validate children
allocate new policyId
store type, admin, children - PolicyRegistry-->>Creator: emit PolicyCreated(policyId, creator, policyType) - PolicyRegistry-->>Creator: emit PolicyAdminUpdated(policyId, 0, admin) - PolicyRegistry-->>Creator: emit CompositePolicyUpdated(policyId, creator, childPolicyIds) -``` - -Every entry in `childPolicyIds` must be an existing simple (`ALLOWLIST`/`BLOCKLIST`) policy — never another composite and never a built-in sentinel (`ALWAYS_ALLOW`/`ALWAYS_BLOCK`). The set size must fall within `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]` (2–4, inclusive). - -Reverts: `ZeroAddress` (if `admin` is `address(0)`), `IncompatiblePolicyType` (`policyType` isn't `UNION`/`INTERSECT`), `ChildPoliciesOutsideOfRange` (child count outside `[2, 4]`), `PolicyNotFound` (a child doesn't exist), `InvalidChildPolicy` (a child is a composite or a built-in sentinel). - -### Update Membership - -The policy admin sets `accounts` to a uniform membership state — all included or all excluded — in a single batch. - -```mermaid -sequenceDiagram - participant PolicyAdmin - participant PolicyRegistry - - PolicyAdmin->>PolicyRegistry: updateAllowlist(policyId, allowed, accounts) - Note over PolicyRegistry: set each account's
membership to `allowed` - PolicyRegistry-->>PolicyAdmin: emit AllowlistUpdated(policyId, updater, allowed, accounts) -``` - -`updateBlocklist(policyId, blocked, accounts)` has the same shape for `BLOCKLIST` policies; it emits `BlocklistUpdated` instead. Use the matching call for the policy's type — mixing them reverts. - -Reverts: `PolicyNotFound` (unknown `policyId`), `IncompatiblePolicyType` (wrong call for the policy's type), `Unauthorized` (caller isn't current admin), `BatchSizeTooLarge`. - -### Update Composite Children - -The composite's admin replaces its child-policy set in full with `updateComposite`. - -```mermaid -sequenceDiagram - participant PolicyAdmin - participant PolicyRegistry - - PolicyAdmin->>PolicyRegistry: updateComposite(policyId, childPolicyIds) - Note over PolicyRegistry: validate children
replace child set in full - PolicyRegistry-->>PolicyAdmin: emit CompositePolicyUpdated(policyId, updater, childPolicyIds) -``` - -`childPolicyIds` is a full replacement, a child omitted from the new set no longer governs the composite. The new set must still satisfy the same size and child-validity rules as creation. - -Reverts: `PolicyNotFound` (unknown `policyId` or a child that doesn't exist), `IncompatiblePolicyType` (`policyId` isn't `UNION`/`INTERSECT`), `Unauthorized` (caller isn't current admin — a renounced composite can never be updated), `ChildPoliciesOutsideOfRange` (child count outside `[2, 4]`), `InvalidChildPolicy` (a child is a composite or a built-in sentinel). - -### Transfer Admin - -A two-step transfer: the current admin proposes a successor, then the proposed admin accepts. The active admin doesn't change until the second step. - -```mermaid -sequenceDiagram - participant CurrentAdmin - participant PolicyRegistry - participant NewAdmin - - CurrentAdmin->>PolicyRegistry: stageUpdateAdmin(policyId, newAdmin) - Note over PolicyRegistry: pendingAdmin = newAdmin - PolicyRegistry-->>CurrentAdmin: emit PolicyAdminStaged(policyId, currentAdmin, newAdmin) - - NewAdmin->>PolicyRegistry: finalizeUpdateAdmin(policyId) - Note over PolicyRegistry: admin = newAdmin
clear pendingAdmin - PolicyRegistry-->>NewAdmin: emit PolicyAdminUpdated(policyId, currentAdmin, newAdmin) -``` - -`stageUpdateAdmin(policyId, address(0))` cancels an in-flight transfer. Re-staging while a pending admin already exists overwrites the prior nomination — the previous candidate loses their ability to finalize. - -Reverts (Step 1): `PolicyNotFound`, `Unauthorized` (caller isn't current admin). -Reverts (Step 2): `PolicyNotFound`, `NoPendingAdmin` (no transfer in flight), `Unauthorized` (caller isn't the staged pending admin). - -### Renounce Admin - -The current admin permanently relinquishes administration of the policy. The membership set is frozen forever; the policy can never be re-administered. - -```mermaid -sequenceDiagram - participant PolicyAdmin - participant PolicyRegistry - - PolicyAdmin->>PolicyRegistry: renounceAdmin(policyId) - Note over PolicyRegistry: admin = address(0)
clear pendingAdmin - PolicyRegistry-->>PolicyAdmin: emit PolicyAdminUpdated(policyId, oldAdmin, 0) -``` - -The policy continues to exist and remains a valid target of `isAuthorized` queries forever — only mutation is disabled. - -Reverts: `PolicyNotFound`, `Unauthorized` (caller isn't current admin). diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..9e4618d6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,17 @@ +## Understanding B20 + +New to B20? + +1. [B20 Overview](overview.md) +2. [How B20 Works](architecture.md) + +Building something? + +- [Integrator Guide](guides/integrators.md) +- [Indexer Guide](guides/indexers.md) +- [Implementer Guide](guides/implementers.md) + +Looking for exact technical details? + +- [Concepts](concepts/) — the mental model: assets, policies, roles, execution, versioning +- [Reference](reference/) — interfaces, events, errors, constants, versions diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..8f66224c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,57 @@ +# How B20 Works + +*The canonical explanation of the B20 system. Every serious B20 consumer — integrator, indexer, or implementer — should read this document. Audience guides link back here instead of re-explaining these mechanics.* + +## Execution model + +```text +Application + ↓ +B20 ABI + ↓ +Precompile dispatcher + ↓ +Version resolution + ↓ +Asset logic + ↓ +Policies / authorization + ↓ +State mutation + ↓ +Events +``` + +_TODO — narrative walkthrough of the pipeline above._ + +## Lifecycle of an asset + +_TODO — creation via the factory through to end-of-life states._ + +## State model + +_TODO_ + +## Policy model + +_TODO — see [Policies](concepts/policies.md) for the full model._ + +## Permissions / roles + +_TODO — see [Roles](concepts/roles.md) for the full model._ + +## Events + +_TODO — see [Events reference](reference/events.md) for the exhaustive list._ + +## Upgrades / versioning + +_TODO — see [Versioning](concepts/versioning.md) for the full model._ + +## Invariants + +_TODO_ + +## Full transaction walkthrough + +_TODO — trace one transaction end-to-end through every layer above._ diff --git a/docs/concepts/assets.md b/docs/concepts/assets.md new file mode 100644 index 00000000..18c435cb --- /dev/null +++ b/docs/concepts/assets.md @@ -0,0 +1,26 @@ +# Assets + +*The B20 asset model: the core standard and its variants.* + +## The core standard + +_TODO — B20 as an ERC-20 superset. See [`IB20`](../../src/interfaces/IB20.sol)._ + +## Variant family + +_TODO_ + +- **Asset** — the general-purpose variant. See [`IB20Asset`](../../src/interfaces/IB20Asset.sol). +- **Stablecoin** — the fixed-decimals, fiat-backed carveout. See [`IB20Stablecoin`](../../src/interfaces/IB20Stablecoin.sol). + +## Creation + +_TODO — assets are deployed via the singleton [`IB20Factory`](../../src/interfaces/IB20Factory.sol)._ + +## Multiplier (Asset variant) + +_TODO_ + +## Extra metadata (Asset variant) + +_TODO_ diff --git a/docs/concepts/execution.md b/docs/concepts/execution.md new file mode 100644 index 00000000..ad16651f --- /dev/null +++ b/docs/concepts/execution.md @@ -0,0 +1,19 @@ +# Execution Model + +*How a B20 call actually resolves and runs. See [Architecture: Execution model](../architecture.md#execution-model) for where this fits in the full pipeline.* + +## The precompile dispatcher + +_TODO_ + +## Version resolution + +_TODO — see [Versioning](versioning.md)._ + +## Asset logic + +_TODO_ + +## State mutation + +_TODO_ diff --git a/docs/concepts/policies.md b/docs/concepts/policies.md new file mode 100644 index 00000000..e7f62049 --- /dev/null +++ b/docs/concepts/policies.md @@ -0,0 +1,19 @@ +# Policies + +*The B20 authorization model: policy scopes and their relationship to the PolicyRegistry.* + +## Policy scopes + +_TODO — per-actor scopes gating transfer/mint operations._ + +## The PolicyRegistry + +_TODO — see [`IPolicyRegistry`](../../src/interfaces/IPolicyRegistry.sol)._ + +## Defaults + +_TODO — every scope defaults to `ALWAYS_ALLOW` at token creation unless overridden._ + +## Configuring policies + +_TODO_ diff --git a/docs/concepts/roles.md b/docs/concepts/roles.md new file mode 100644 index 00000000..34922fa8 --- /dev/null +++ b/docs/concepts/roles.md @@ -0,0 +1,15 @@ +# Roles + +*The B20 role-based access control model.* + +## Role taxonomy + +_TODO — built-in roles (`DEFAULT_ADMIN_ROLE`, `MINT_ROLE`, `BURN_ROLE`, etc.). See [`B20Constants`](../../src/lib/B20Constants.sol)._ + +## Custom roles + +_TODO — `setRoleAdmin` / `grantRole` for user-defined roles._ + +## Admin lifecycle + +_TODO — `renounceLastAdmin` and the admin-less end state._ diff --git a/docs/concepts/versioning.md b/docs/concepts/versioning.md new file mode 100644 index 00000000..7aa7705f --- /dev/null +++ b/docs/concepts/versioning.md @@ -0,0 +1,19 @@ +# Versioning + +*How B20 evolves across hardforks, and what that means for callers.* + +## The frozen-vN pattern + +_TODO — each hardfork version's logic is frozen once shipped; the dispatcher resolves calls to the correct version._ + +## What can change between versions + +_TODO_ + +## What's guaranteed to stay stable + +_TODO — ABI/selector stability guarantees for existing integrations._ + +## Where to check the current version + +_TODO — see [Versions reference](../reference/versions.md) and [`CHANGELOG.md`](../../CHANGELOG.md)._ diff --git a/docs/guides/implementers.md b/docs/guides/implementers.md new file mode 100644 index 00000000..c40438ce --- /dev/null +++ b/docs/guides/implementers.md @@ -0,0 +1,27 @@ +# Implementer Guide + +*Now that you understand B20 (see [Architecture](../architecture.md)), here's what you specifically need to care about as someone implementing or modifying B20 itself.* + +## Implementation boundaries + +_TODO_ + +## Version resolution + +_TODO — see [Versioning](../concepts/versioning.md) for the underlying model._ + +## Historical execution + +_TODO — how older hardfork versions must remain callable._ + +## Invariants + +_TODO — see [Architecture: Invariants](../architecture.md#invariants)._ + +## Testing + +_TODO — see the root [README: Test Integration](../../README.md#test-integration) and [Live precompile testing](../../README.md#live-precompile-testing)._ + +## Consensus considerations + +_TODO_ diff --git a/docs/guides/indexers.md b/docs/guides/indexers.md new file mode 100644 index 00000000..1eb7cb95 --- /dev/null +++ b/docs/guides/indexers.md @@ -0,0 +1,27 @@ +# Indexer Guide + +*Now that you understand B20 (see [Architecture](../architecture.md)), here's what you specifically need to care about as someone indexing B20 activity.* + +## Discovery + +_TODO — how to discover B20 assets on-chain._ + +## Event guarantees + +_TODO — see [Events reference](../reference/events.md) for the exhaustive list._ + +## State reconstruction + +_TODO_ + +## Reorgs and ordering + +_TODO_ + +## Fork boundaries + +_TODO — see [Versioning](../concepts/versioning.md) for the underlying model._ + +## Schema / version compatibility + +_TODO — see [Versions reference](../reference/versions.md)._ diff --git a/docs/guides/integrators.md b/docs/guides/integrators.md new file mode 100644 index 00000000..ab818aad --- /dev/null +++ b/docs/guides/integrators.md @@ -0,0 +1,31 @@ +# Integrator Guide + +*Now that you understand B20 (see [Architecture](../architecture.md)), here's what you specifically need to care about as someone calling B20 from an application.* + +## Quickstart + +_TODO_ + +## Calling B20 + +_TODO_ + +## Reading assets + +_TODO — see [Assets](../concepts/assets.md) for the underlying model._ + +## Submitting transactions + +_TODO_ + +## Policies and roles you'll run into + +_TODO — see [Policies](../concepts/policies.md) and [Roles](../concepts/roles.md) for the underlying model._ + +## Handling failures + +_TODO — see [Errors reference](../reference/errors.md) for the exhaustive list._ + +## Compatibility across versions + +_TODO — see [Versioning](../concepts/versioning.md) for the underlying model._ diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 00000000..b64ba468 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,38 @@ +# B20 Overview + +*B20 in 10 minutes. Read this to get oriented, then go to [Architecture](architecture.md) for how it actually works.* + +## What is B20? + +_TODO_ + +## Why does it exist? + +_TODO_ + +## What problems does it solve? + +_TODO_ + +## Where does it sit in Base? + +_TODO_ + +## What is a B20 Asset? + +_TODO — see [Assets](concepts/assets.md) for the full model._ + +## What are policies? + +_TODO — see [Policies](concepts/policies.md) for the full model._ + +## What does a basic transaction look like? + +_TODO_ + +## Where should I go next? + +- Building against B20? Start with the [Integrator Guide](guides/integrators.md). +- Indexing B20 activity? Start with the [Indexer Guide](guides/indexers.md). +- Implementing or modifying B20 itself? Start with the [Implementer Guide](guides/implementers.md). +- Want the precise mechanics? See [Architecture](architecture.md). diff --git a/docs/reference/constants.md b/docs/reference/constants.md new file mode 100644 index 00000000..7e03b7d7 --- /dev/null +++ b/docs/reference/constants.md @@ -0,0 +1,7 @@ +# Constants + +*Role identifiers, policy-type identifiers, and other fixed constants. See [`B20Constants`](../../src/lib/B20Constants.sol).* + +| Name | Value | Purpose | +|---|---|---| +| _TODO_ | | | diff --git a/docs/reference/errors.md b/docs/reference/errors.md new file mode 100644 index 00000000..0a42d780 --- /dev/null +++ b/docs/reference/errors.md @@ -0,0 +1,7 @@ +# Errors + +*Exhaustive list of custom errors, selectors, and the conditions that trigger them.* + +| Error | Selector | Thrown when | +|---|---|---| +| _TODO_ | | | diff --git a/docs/reference/events.md b/docs/reference/events.md new file mode 100644 index 00000000..c93b1228 --- /dev/null +++ b/docs/reference/events.md @@ -0,0 +1,7 @@ +# Events + +*Exhaustive list of events emitted by the B20 system.* + +| Event | Emitted by | When | +|---|---|---| +| _TODO_ | | | diff --git a/docs/reference/interfaces.md b/docs/reference/interfaces.md new file mode 100644 index 00000000..de911de2 --- /dev/null +++ b/docs/reference/interfaces.md @@ -0,0 +1,16 @@ +# Interfaces + +*Solidity interfaces for the B20 system and its supporting precompiles.* + +| Interface | Description | +|---|---| +| [`IB20`](../../src/interfaces/IB20.sol) | Core token standard | +| [`IB20Asset`](../../src/interfaces/IB20Asset.sol) | Asset variant of B20 | +| [`IB20Stablecoin`](../../src/interfaces/IB20Stablecoin.sol) | Stablecoin variant of B20 | +| [`IB20Factory`](../../src/interfaces/IB20Factory.sol) | B20 factory precompile | +| [`IPolicyRegistry`](../../src/interfaces/IPolicyRegistry.sol) | Policy registry precompile | +| [`IActivationRegistry`](../../src/interfaces/IActivationRegistry.sol) | Activation registry precompile | +| [`IERC8056`](../../src/interfaces/IERC8056.sol) | Scaled UI Amount standard (Asset variant multiplier) | +| [`IERC165`](../../src/interfaces/IERC165.sol) | Interface detection | + +See [`StdPrecompiles.sol`](../../src/StdPrecompiles.sol) for canonical precompile addresses. diff --git a/docs/reference/versions.md b/docs/reference/versions.md new file mode 100644 index 00000000..83190624 --- /dev/null +++ b/docs/reference/versions.md @@ -0,0 +1,7 @@ +# Versions + +*B20 versions by hardfork. See [`CHANGELOG.md`](../../CHANGELOG.md) for behavioral changes and [`changelog/`](../../changelog/README.md) for selector-level detail.* + +| Version | Hardfork | Changes | +|---|---|---| +| _TODO_ | | | From b8fbf51901ac1e819403b1e201cb0fbffd9e8e2b Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 11:22:36 -0400 Subject: [PATCH 02/13] start: overview --- docs/overview.md | 291 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 272 insertions(+), 19 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index b64ba468..f2e8f9eb 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,38 +1,291 @@ +“B20 in 10 Minutes.” Very readable, probably 5–10 minutes. + +It answers: + +What is B20? +Why does it exist? +What problems does it solve? +Where does it sit in Base? +What is a B20 Asset? +What are policies? +What does a basic transaction look like? +Where should I go next? + +Someone should be able to read just this and explain B20 at a high level. + + # B20 Overview -*B20 in 10 minutes. Read this to get oriented, then go to [Architecture](architecture.md) for how it actually works.* +B20 is Base's native token standard for issuing and managing programmable assets onchain. + +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). + +--- ## What is B20? -_TODO_ +B20 is Base's native token standard for issuing and managing programmable assets onchain. Base created it to standardize real-world asset (RWA) and stablecoin issuance. B20 is an ERC-20 superset: balances, transfers, and approvals work like ERC-20, and every B20 asset shares the same additional interfaces and protocol logic rather than each issuer deploying a custom token implementation. + +The standard also includes compliance and administrative controls. Issuers can configure roles and permissions, attach policies, mint and burn supply, pause operations, and perform other administrative actions that regulated-asset workflows typically require. + +B20 runs as precompiles in the Base node, not as per-token Solidity. Applications call ERC-20-style interfaces; the execution layer runs the shared B20 logic natively. Base upgrades that logic through hardforks, so issuers and holders get consistent behavior and native execution across all B20 assets. + +### At a Glance + +Optional small diagram: + +Application + | + v +B20 Interfaces + | + v +B20 Precompiles + | + v +Base Execution Layer + +One sentence explaining that applications interact with familiar contract-style interfaces while execution happens through native B20 functionality. + +--- + +## Why B20? + +Explain the problem B20 is trying to solve. + +Potential themes: + +- Tokenized assets repeatedly need the same primitives. +- Implementing these independently creates inconsistency and duplicated engineering effort. +- Assets may require richer controls than basic token transfers. +- Native primitives can provide standardized behavior and stronger ecosystem interoperability. + +Avoid going deep into implementation rationale here. + +The goal is to answer: + +> Why would someone use B20 instead of building everything themselves? + +--- + +## Core Mental Model + +Introduce the main B20 concepts at the highest level. + +### Assets + +A B20 Asset represents an issued onchain asset. + +Explain in a few sentences: + +- balances +- supply +- asset configuration +- administrative capabilities + +Do not describe every method. + +### Roles + +Roles determine who is authorized to perform privileged operations. + +Examples might include: + +- issuing +- administrative changes +- seizing +- managing policies + +Link to deeper documentation. + +### Policies + +Policies define conditions that must be satisfied for certain operations. + +Example mental model: + +Transfer + | + v +Policy Evaluation + | + +--> Allowed + | + +--> Rejected + +Explain that policies allow asset behavior to encode eligibility or transfer requirements without explaining every policy type yet. + +### Native Functionality + +Explain briefly that B20 functionality is implemented through Base precompiles and exposed through contract-compatible interfaces. + +Do not explain dispatcher/version resolution yet. + +Link to architecture.md. + +--- + +## The Lifecycle of a B20 Asset + +Give readers one simple end-to-end sequence. + +Create Asset + | + v +Configure Roles / Policies + | + v +Issue Units + | + v +Transfer / Manage + | + v +Administrative Actions + +Then describe each step in one sentence. + +For example: + +1. An issuer creates an asset. +2. The issuer configures who can administer it and what policies govern it. +3. Units are issued to holders. +4. Holders interact with the asset subject to its configured rules. +5. Authorized parties can perform administrative operations when required. + +This section should establish the lifecycle without teaching the API. + +--- + +## How a B20 Operation Works + +Show one extremely simple transaction path. + +Example: + +User / Application + | + | transfer(...) + v + B20 Asset + | + v +Authorization / Policy Checks + | + v +State Transition + | + v +Events + +Then explain: + +- applications submit a B20 operation +- B20 evaluates the relevant authorization and policy rules +- if valid, canonical state changes +- events expose the resulting transition to downstream systems + +This prepares readers for architecture.md. + +--- + +## Example: Issuing and Transferring an Asset + +Use one example throughout the documentation. + +For example: + +> ACME creates `ACME-TBILL`, a token representing units of a treasury product. + +Walk through: + +1. ACME creates the asset. +2. ACME configures the appropriate permissions. +3. ACME attaches a holder eligibility policy. +4. ACME issues 1,000 units to Alice. +5. Alice transfers 100 units to Bob. +6. B20 evaluates whether Bob satisfies the required policy. +7. If allowed, balances are updated and the relevant events are emitted. + +The point isn't to show code. + +The point is to connect all the concepts introduced above. + +--- + +## B20 in the Base Stack + +Show where B20 sits. + +Application / Wallet / Backend + | + v + B20 Interface + | + v + B20 Precompiles + | + v + Base Execution + | + v + Canonical State + +Explain the separation between: + +- application-facing interface +- B20 protocol functionality +- underlying Base execution + +This should be enough context for readers before they enter architecture.md. + +--- + +## Who Builds Against B20? + +Very briefly introduce your three audiences. + +### Integrators + +Applications, issuers, wallets, backends, or other systems that interact with B20 assets. + +→ [Integrator Guide](./guides/integrators.md) + +### Indexers + +Systems that ingest B20 events and state to provide APIs, analytics, explorers, portfolio views, or other derived data. + +→ [Indexer Guide](./guides/indexers.md) + +### Implementers -## Why does it exist? +Engineers working on B20 execution, client support, precompiles, testing, upgrades, or protocol behavior. -_TODO_ +→ [Implementer Guide](./guides/implementers.md) -## What problems does it solve? +--- -_TODO_ +## Where to Go Next -## Where does it sit in Base? +If you want to understand how B20 works internally: -_TODO_ +→ [B20 Architecture](./architecture.md) -## What is a B20 Asset? +If you are integrating B20: -_TODO — see [Assets](concepts/assets.md) for the full model._ +→ [Integrator Guide](./guides/integrators.md) -## What are policies? +If you are indexing B20: -_TODO — see [Policies](concepts/policies.md) for the full model._ +→ [Indexer Guide](./guides/indexers.md) -## What does a basic transaction look like? +If you are implementing B20: -_TODO_ +→ [Implementer Guide](./guides/implementers.md) -## Where should I go next? +For exact interfaces and protocol definitions: -- Building against B20? Start with the [Integrator Guide](guides/integrators.md). -- Indexing B20 activity? Start with the [Indexer Guide](guides/indexers.md). -- Implementing or modifying B20 itself? Start with the [Implementer Guide](guides/implementers.md). -- Want the precise mechanics? See [Architecture](architecture.md). +→ [Reference](./reference/) +→ [Specifications](./specs/) \ No newline at end of file From 707bc8c7dc4b9392c2e9fbc9089ebf2790b116c8 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 12:00:52 -0400 Subject: [PATCH 03/13] feat: why section --- docs/overview.md | 49 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index f2e8f9eb..fe58d7ce 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -30,43 +30,38 @@ B20 is Base's native token standard for issuing and managing programmable assets The standard also includes compliance and administrative controls. Issuers can configure roles and permissions, attach policies, mint and burn supply, pause operations, and perform other administrative actions that regulated-asset workflows typically require. -B20 runs as precompiles in the Base node, not as per-token Solidity. Applications call ERC-20-style interfaces; the execution layer runs the shared B20 logic natively. Base upgrades that logic through hardforks, so issuers and holders get consistent behavior and native execution across all B20 assets. +B20 runs as precompiles in the Base node, not as per-token Solidity. Wallets, issuers, and apps call ERC-20-style interfaces; the node runs the shared B20 logic natively. Base upgrades that logic through hardforks, so every caller gets consistent behavior and native execution across all B20 assets. ### At a Glance -Optional small diagram: - -Application - | - v -B20 Interfaces - | - v -B20 Precompiles - | - v -Base Execution Layer - -One sentence explaining that applications interact with familiar contract-style interfaces while execution happens through native B20 functionality. +```mermaid +flowchart TD + W[Wallets] + I[Issuers] + A[Apps] + B[B20 interface] + N[Node] + P[Precompile] + L[Shared logic] + W --> B + I --> B + A --> B + B -->|call| N + N --> P + P --> L +``` + +You call a B20 asset the same way you call any other contract: through its interface at the asset address. Every B20 asset uses that same interface and the same precompile logic, so integrators have one source of truth. --- ## Why B20? -Explain the problem B20 is trying to solve. - -Potential themes: - -- Tokenized assets repeatedly need the same primitives. -- Implementing these independently creates inconsistency and duplicated engineering effort. -- Assets may require richer controls than basic token transfers. -- Native primitives can provide standardized behavior and stronger ecosystem interoperability. - -Avoid going deep into implementation rationale here. +Real-world asset (RWA) issuance onchain needs a shared token standard with compliance built into the asset. ERC-20 covers balances, transfers, and approvals. Regulated assets also need eligibility checks, roles, mint and burn, pausing, and other administrative controls. Issuers rebuild those primitives for almost every tokenized asset. -The goal is to answer: +Issuers who implement that stack themselves repeat the same logic, diverge in behavior, and force every wallet and app to integrate a custom token. B20 is the alternative: you create a B20 asset and configure its roles and policies instead of writing and maintaining a one-off token. Compliance is a first-class primitive, not an add-on each issuer designs around transfers. -> Why would someone use B20 instead of building everything themselves? +A single standard also helps integrators and issuers. Wallets and apps integrate against one interface. Issuers can use shared services, such as oracles, without designing a new integration for each asset. --- From d040040be23ec0f5ba2941377751d34776cc4108 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 13:12:15 -0400 Subject: [PATCH 04/13] feat: overview --- docs/overview.md | 103 ++++++++++++++++++----------------------------- 1 file changed, 39 insertions(+), 64 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index fe58d7ce..6568fc66 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -67,90 +67,65 @@ A single standard also helps integrators and issuers. Wallets and apps integrate ## Core Mental Model -Introduce the main B20 concepts at the highest level. +Three services matter when you issue or use a B20. Each has one job. A fourth singleton, the Activation Registry, is a Base-operated safety switch that turns these features on; issuers and apps do not operate it. -### Assets - -A B20 Asset represents an issued onchain asset. - -Explain in a few sentences: - -- balances -- supply -- asset configuration -- administrative capabilities - -Do not describe every method. - -### Roles - -Roles determine who is authorized to perform privileged operations. +```mermaid +flowchart LR + F[B20 Factory] + T[B20 token] + R[Policy Registry] + F -->|creates| T + T -->|consults| R +``` -Examples might include: +The Factory creates tokens. Each token holds balances and configuration for one asset. The Policy Registry answers whether an account is allowed under a given policy. -- issuing -- administrative changes -- seizing -- managing policies +### B20 Factory -Link to deeper documentation. +The Factory is a singleton precompile. Every B20 token is created through it. At creation you choose a variant: Asset, for general-purpose issuance including RWAs, or Stablecoin, for a fiat-pegged token with a fixed currency code. -### Policies +The Factory does not hold balances, assign roles, or decide who may transfer. It creates the token and retains no ongoing access. -Policies define conditions that must be satisfied for certain operations. +### B20 token -Example mental model: +The token is the asset. The issuer creates it through the Factory and maintains it. It holds balances and supply, and it exposes the ERC-20 surface plus administrative controls: mint, burn, pause, seize, and metadata. -Transfer - | - v -Policy Evaluation - | - +--> Allowed - | - +--> Rejected +[Roles](./concepts/roles.md) live on the token. They answer who may call those privileged operations on this asset. They are not a separate registry. -Explain that policies allow asset behavior to encode eligibility or transfer requirements without explaining every policy type yet. +Asset and Stablecoin share this model. They differ in a few variant-specific fields, not in how issuance, roles, or policies work. See [Assets](./concepts/assets.md). -### Native Functionality +### Policy Registry -Explain briefly that B20 functionality is implemented through Base precompiles and exposed through contract-compatible interfaces. +The Policy Registry is a singleton precompile that stores reusable authorization policies. The token does not keep membership lists. It stores which policy applies to an operation, then asks the registry whether the relevant account is allowed. -Do not explain dispatcher/version resolution yet. +The token owns which rule applies. The registry owns whether an account satisfies that rule. One policy can be attached to many tokens. -Link to architecture.md. +See [Policies](./concepts/policies.md). For how a call moves through these services, see [How B20 Works](./architecture.md). --- ## The Lifecycle of a B20 Asset -Give readers one simple end-to-end sequence. +An issuer brings a B20 asset into use through this sequence. -Create Asset - | - v -Configure Roles / Policies - | - v -Issue Units - | - v -Transfer / Manage - | - v -Administrative Actions - -Then describe each step in one sentence. - -For example: - -1. An issuer creates an asset. -2. The issuer configures who can administer it and what policies govern it. -3. Units are issued to holders. -4. Holders interact with the asset subject to its configured rules. -5. Authorized parties can perform administrative operations when required. +```mermaid +flowchart TD + C["Create Asset
Issuer through Factory"] + G["Configure Roles / Policies
Admin"] + IU["Issue Units
Issuer"] + T["Transfer
Holders"] + A["Administrative Actions
Admin"] + C --> G + G --> IU + IU --> T + T --> A +``` -This section should establish the lifecycle without teaching the API. +1. An issuer creates an asset through the Factory. The asset now exists: it has a name, a symbol, a variant, and an initial admin. +2. The admin assigns operating roles and attaches the policies that govern the asset. +3. The issuer mints new units on the asset to holder accounts. The asset allows the mint only if the caller has the mint role and the recipient is allowed by policy. +4. Holders who received those units can transfer them to other accounts. The asset allows each transfer only if the sender and the recipient are allowed by policy. +5. The admin can pause the asset, seize units, burn supply, or update metadata. --- From c710dfe8fc1dc30174b23089c7a1e48bfce05ba8 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 13:22:42 -0400 Subject: [PATCH 05/13] feat: clean up issuances --- docs/overview.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index 6568fc66..cc0311f3 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -104,28 +104,27 @@ See [Policies](./concepts/policies.md). For how a call moves through these servi --- -## The Lifecycle of a B20 Asset +## Bringing a B20 Asset into Use -An issuer brings a B20 asset into use through this sequence. +An issuer follows this sequence. ```mermaid flowchart TD - C["Create Asset
Issuer through Factory"] - G["Configure Roles / Policies
Admin"] - IU["Issue Units
Issuer"] - T["Transfer
Holders"] - A["Administrative Actions
Admin"] - C --> G - G --> IU - IU --> T - T --> A + subgraph s1 [1. Create] + direction LR + I1[Issuer] -->|createB20| F[Factory] + end + subgraph s2 [2. Configure and mint] + direction LR + I2[Issuer] -->|grantRole / updatePolicy| G[Configure] + I2 -->|mint| M[Mint] + end + s1 --> s2 ``` -1. An issuer creates an asset through the Factory. The asset now exists: it has a name, a symbol, a variant, and an initial admin. -2. The admin assigns operating roles and attaches the policies that govern the asset. -3. The issuer mints new units on the asset to holder accounts. The asset allows the mint only if the caller has the mint role and the recipient is allowed by policy. -4. Holders who received those units can transfer them to other accounts. The asset allows each transfer only if the sender and the recipient are allowed by policy. -5. The admin can pause the asset, seize units, burn supply, or update metadata. +1. The issuer creates an asset through the Factory. The asset now exists: it has a name, a symbol, a variant, and an initial admin. +2. The issuer configures roles and policies on the asset and mints units to holders. The asset allows the mint only if the caller has the mint role and the recipient is allowed by policy. +3. Holders who received those units can transfer them to other accounts. The asset allows each transfer only if the sender and the recipient are allowed by policy. --- From cb4987ca58cdb07a804c8070b27126ac006b2ae6 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 13:28:01 -0400 Subject: [PATCH 06/13] feat: add configuring roles --- docs/overview.md | 224 ++++++++++++++++++----------------------------- 1 file changed, 86 insertions(+), 138 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index cc0311f3..05ede4bd 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -4,11 +4,10 @@ It answers: What is B20? Why does it exist? -What problems does it solve? -Where does it sit in Base? -What is a B20 Asset? -What are policies? -What does a basic transaction look like? +How does the Factory create an asset? +How do roles and pause work? +How do compliance checks integrate? +Where does B20 sit in the Base stack? Where should I go next? Someone should be able to read just this and explain B20 at a high level. @@ -65,174 +64,123 @@ A single standard also helps integrators and issuers. Wallets and apps integrate --- -## Core Mental Model +## Creating a B20 Asset -Three services matter when you issue or use a B20. Each has one job. A fourth singleton, the Activation Registry, is a Base-operated safety switch that turns these features on; issuers and apps do not operate it. +Every B20 token is created through the Factory, a singleton precompile. You submit `createB20` to a Base node the same way you submit any other contract call. ```mermaid -flowchart LR - F[B20 Factory] - T[B20 token] - R[Policy Registry] - F -->|creates| T - T -->|consults| R +sequenceDiagram + participant Issuer + participant Factory + participant Token as B20 token + + Issuer->>Factory: createB20(variant, salt, params, initCalls) + Factory->>Token: seal identity + Factory->>Token: initCalls (grantRole, updatePolicy, mint) + Factory-->>Issuer: token address ``` -The Factory creates tokens. Each token holds balances and configuration for one asset. The Policy Registry answers whether an account is allowed under a given policy. +1. The issuer calls `createB20` with a variant, a salt, and creation parameters (name, symbol, initial admin, and variant-specific fields). +2. The Factory assigns a deterministic address from `(variant, sender, salt)` and seals the token's identity. +3. Optional `initCalls` run on the new token so the issuer can grant roles, attach policies, or mint in the same transaction. +4. `createB20` returns. The Factory retains no ongoing access to the token. -### B20 Factory +Choose **Asset** for general-purpose issuance, including RWAs, or **Stablecoin** for a fiat-pegged token with a fixed currency code. Both variants share roles, policies, and the ERC-20 surface. See [Assets](./concepts/assets.md). -The Factory is a singleton precompile. Every B20 token is created through it. At creation you choose a variant: Asset, for general-purpose issuance including RWAs, or Stablecoin, for a fiat-pegged token with a fixed currency code. +The Activation Registry is a Base-operated safety switch that turns Factory and token features on. Issuers and apps do not operate it. -The Factory does not hold balances, assign roles, or decide who may transfer. It creates the token and retains no ongoing access. - -### B20 token +--- -The token is the asset. The issuer creates it through the Factory and maintains it. It holds balances and supply, and it exposes the ERC-20 surface plus administrative controls: mint, burn, pause, seize, and metadata. +## Configuring Roles -[Roles](./concepts/roles.md) live on the token. They answer who may call those privileged operations on this asset. They are not a separate registry. +Privileged operations on a token use [OpenZeppelin AccessControl](https://docs.openzeppelin.com/contracts/5.x/access-control). Roles live on the token. They are not a separate registry. -Asset and Stablecoin share this model. They differ in a few variant-specific fields, not in how issuance, roles, or policies work. See [Assets](./concepts/assets.md). +The admin grants and revokes roles with the standard AccessControl methods: `grantRole`, `revokeRole`, `renounceRole`, and `setRoleAdmin`. `DEFAULT_ADMIN_ROLE` is the top-level admin. It is the role required to grant other roles, attach policies, and set the supply cap. -### Policy Registry +| Role | Gates | +| --- | --- | +| `DEFAULT_ADMIN_ROLE` | `grantRole`, `revokeRole`, `setRoleAdmin`, `updatePolicy`, `updateSupplyCap` | +| `MINT_ROLE` | `mint` | +| `BURN_ROLE` | `burn` | +| `SEIZE_ROLE` | `seizeWithMemo` | +| `PAUSE_ROLE` / `UNPAUSE_ROLE` | `pause` / `unpause` | +| `METADATA_ROLE` | name, symbol, and contract URI updates | -The Policy Registry is a singleton precompile that stores reusable authorization policies. The token does not keep membership lists. It stores which policy applies to an operation, then asks the registry whether the relevant account is allowed. +The Asset variant also has `OPERATOR_ROLE` for announcements and multiplier updates. See [Roles](./concepts/roles.md). -The token owns which rule applies. The registry owns whether an account satisfies that rule. One policy can be attached to many tokens. +Pause is per feature, not global. `PAUSE_ROLE` can pause any of four vectors: `TRANSFER`, `MINT`, `BURN`, and `SEIZE`. `UNPAUSE_ROLE` is a separate role, so the account that pauses does not have to be the account that resumes. `approve` is not pause-gated. -See [Policies](./concepts/policies.md). For how a call moves through these services, see [How B20 Works](./architecture.md). +Holder `transfer` is not role-gated. Anyone who holds units can transfer them, subject to pause and policy. --- -## Bringing a B20 Asset into Use +## Integrating Compliance Checks -An issuer follows this sequence. +The Policy Registry is a singleton precompile that stores reusable allowlists, blocklists, and composite policies. The token does not keep membership lists. It stores which policy applies to an operation, then asks the registry whether the relevant account is allowed. ```mermaid -flowchart TD - subgraph s1 [1. Create] - direction LR - I1[Issuer] -->|createB20| F[Factory] - end - subgraph s2 [2. Configure and mint] - direction LR - I2[Issuer] -->|grantRole / updatePolicy| G[Configure] - I2 -->|mint| M[Mint] - end - s1 --> s2 +flowchart LR + E[Policy engine] + A[Token admin] + R[Policy Registry] + T[B20 token] + E -->|update membership| R + A -->|updatePolicy| T + T -->|isAuthorized| R ``` -1. The issuer creates an asset through the Factory. The asset now exists: it has a name, a symbol, a variant, and an initial admin. -2. The issuer configures roles and policies on the asset and mints units to holders. The asset allows the mint only if the caller has the mint role and the recipient is allowed by policy. -3. Holders who received those units can transfer them to other accounts. The asset allows each transfer only if the sender and the recipient are allowed by policy. - ---- - -## How a B20 Operation Works - -Show one extremely simple transaction path. - -Example: - -User / Application - | - | transfer(...) - v - B20 Asset - | - v -Authorization / Policy Checks - | - v -State Transition - | - v -Events - -Then explain: - -- applications submit a B20 operation -- B20 evaluates the relevant authorization and policy rules -- if valid, canonical state changes -- events expose the resulting transition to downstream systems - -This prepares readers for architecture.md. - ---- - -## Example: Issuing and Transferring an Asset - -Use one example throughout the documentation. - -For example: - -> ACME creates `ACME-TBILL`, a token representing units of a treasury product. +A compliance system connects as a **policy engine** by administering membership on the registry. The token admin binds that policy to a scope with `updatePolicy`. One policy can be attached to many tokens. The token never calls the engine; the engine writes to the registry, and the node reads `isAuthorized` when it executes the call. -Walk through: +### Transfer control path -1. ACME creates the asset. -2. ACME configures the appropriate permissions. -3. ACME attaches a holder eligibility policy. -4. ACME issues 1,000 units to Alice. -5. Alice transfers 100 units to Bob. -6. B20 evaluates whether Bob satisfies the required policy. -7. If allowed, balances are updated and the relevant events are emitted. +You submit a transfer the same way you submit any other onchain call: as a transaction to a Base node, targeting the asset address. -The point isn't to show code. - -The point is to connect all the concepts introduced above. - ---- - -## B20 in the Base Stack - -Show where B20 sits. - -Application / Wallet / Backend - | - v - B20 Interface - | - v - B20 Precompiles - | - v - Base Execution - | - v - Canonical State - -Explain the separation between: +```mermaid +flowchart TD + U[User / Application] + N[Base node] + A[B20 Asset] + P[Pause check] + R[Policy Registry] + S[State + events] + U -->|transfer| N + N --> A + A --> P + P -->|sender and receiver| R + R --> S +``` -- application-facing interface -- B20 protocol functionality -- underlying Base execution +1. A wallet or application submits a transaction that calls `transfer` on the asset. +2. The node executes the call. If `TRANSFER` is paused, the transaction reverts. +3. The asset reads the policy IDs on `TRANSFER_SENDER_POLICY` (`from`) and `TRANSFER_RECEIVER_POLICY` (`to`), then asks the Policy Registry whether each account is authorized. `transferFrom` also checks `TRANSFER_EXECUTOR_POLICY` against `msg.sender`. +4. If those checks pass, the node updates balances and emits `Transfer`. If a check fails, the transaction reverts and state does not change. -This should be enough context for readers before they enter architecture.md. +Slots default to always-allow until the admin attaches a policy. `approve` is not policy-gated. See [Policies](./concepts/policies.md). --- -## Who Builds Against B20? - -Very briefly introduce your three audiences. +## B20 in the Stack -### Integrators +A B20 call uses the same submission path as any other contract call. The node runs shared precompile logic instead of per-token bytecode. -Applications, issuers, wallets, backends, or other systems that interact with B20 assets. - -→ [Integrator Guide](./guides/integrators.md) - -### Indexers - -Systems that ingest B20 events and state to provide APIs, analytics, explorers, portfolio views, or other derived data. - -→ [Indexer Guide](./guides/indexers.md) - -### Implementers +```mermaid +flowchart TD + A[Application / Wallet / Backend] + I[B20 interface] + P[B20 precompiles] + X[Base execution] + S[Canonical state] + A -->|transaction| I + I --> P + P --> X + X --> S +``` -Engineers working on B20 execution, client support, precompiles, testing, upgrades, or protocol behavior. +- **Application-facing interface.** Wallets and apps call ERC-20-style functions at the asset address. +- **B20 precompiles.** The Factory, each token, and the Policy Registry are node-native. Every asset shares the same logic. +- **Base execution.** The node applies authorization, policy, and pause checks, then commits canonical state and events. -→ [Implementer Guide](./guides/implementers.md) +[How B20 Works](./architecture.md) walks this path through the dispatcher, version resolution, and storage. --- From 43636303c578b3dee98dbb511b4b5bb98f1179ef Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 14:04:55 -0400 Subject: [PATCH 07/13] feat: add compliance checks and role flow Walk grant-then-call and attach-then-gate in the overview, including revert paths, and move the role and policy-scope tables to the concept pages. Co-authored-by: Cursor --- docs/concepts/policies.md | 13 +++++- docs/concepts/roles.md | 16 ++++++- docs/overview.md | 95 ++++++++++++++++++++------------------- 3 files changed, 75 insertions(+), 49 deletions(-) diff --git a/docs/concepts/policies.md b/docs/concepts/policies.md index e7f62049..3dfc27e5 100644 --- a/docs/concepts/policies.md +++ b/docs/concepts/policies.md @@ -4,7 +4,18 @@ ## Policy scopes -_TODO — per-actor scopes gating transfer/mint operations._ +Each movement path consults its own scopes. The token stores a policy ID per scope and asks the Policy Registry `isAuthorized(policyId, account)`. + +| Scope | Consulted on | Account | +| --- | --- | --- | +| `TRANSFER_SENDER_POLICY` | `transfer`, `transferFrom` | `from` | +| `TRANSFER_RECEIVER_POLICY` | `transfer`, `transferFrom` | `to` | +| `TRANSFER_EXECUTOR_POLICY` | `transferFrom` only | `msg.sender` | +| `MINT_RECEIVER_POLICY` | `mint` | `to` | +| `SEIZE_HOLDER_POLICY` | `seizeWithMemo` | `from` (seizable when unauthorized) | +| `SEIZE_RECEIVER_POLICY` | `seizeWithMemo` | `to` | + +A denied check reverts with `PolicyForbids`. `approve` is not policy-gated. ## The PolicyRegistry diff --git a/docs/concepts/roles.md b/docs/concepts/roles.md index 34922fa8..e0c6b296 100644 --- a/docs/concepts/roles.md +++ b/docs/concepts/roles.md @@ -4,7 +4,21 @@ ## Role taxonomy -_TODO — built-in roles (`DEFAULT_ADMIN_ROLE`, `MINT_ROLE`, `BURN_ROLE`, etc.). See [`B20Constants`](../../src/lib/B20Constants.sol)._ +B20 follows [OpenZeppelin AccessControl](https://docs.openzeppelin.com/contracts/5.x/access-control) with a fixed set of built-in roles. The admin uses `grantRole`, `revokeRole`, `renounceRole`, and `setRoleAdmin`. See [`B20Constants`](../../src/lib/B20Constants.sol) for the identifier values. + +| Role | Gates | +| --- | --- | +| `DEFAULT_ADMIN_ROLE` | `grantRole`, `revokeRole`, `setRoleAdmin`, `updatePolicy`, `updateSupplyCap` | +| `MINT_ROLE` | `mint`, `mintWithMemo` | +| `BURN_ROLE` | `burn`, `burnWithMemo` | +| `BURN_BLOCKED_ROLE` | `burnBlocked` (deprecated) | +| `SEIZE_ROLE` | `seizeWithMemo` | +| `PAUSE_ROLE` | `pause` | +| `UNPAUSE_ROLE` | `unpause` | +| `METADATA_ROLE` | `updateName`, `updateSymbol`, `updateContractURI` | +| `OPERATOR_ROLE` | Asset-only: `announce`, multiplier updates | + +Pause is per feature, not global. `PAUSE_ROLE` can pause any of `TRANSFER`, `MINT`, `BURN`, and `SEIZE`. `UNPAUSE_ROLE` is a separate role. `approve` is not pause-gated. Holder `transfer` is not role-gated. ## Custom roles diff --git a/docs/overview.md b/docs/overview.md index 05ede4bd..7f478ea4 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -93,69 +93,70 @@ The Activation Registry is a Base-operated safety switch that turns Factory and ## Configuring Roles -Privileged operations on a token use [OpenZeppelin AccessControl](https://docs.openzeppelin.com/contracts/5.x/access-control). Roles live on the token. They are not a separate registry. +B20 uses [OpenZeppelin AccessControl](https://docs.openzeppelin.com/contracts/5.x/access-control) on the token to gate privileged features: mint, burn, seize, metadata, and the pause vectors (`TRANSFER`, `MINT`, `BURN`, `SEIZE`). Roles are not a separate registry. -The admin grants and revokes roles with the standard AccessControl methods: `grantRole`, `revokeRole`, `renounceRole`, and `setRoleAdmin`. `DEFAULT_ADMIN_ROLE` is the top-level admin. It is the role required to grant other roles, attach policies, and set the supply cap. +One `DEFAULT_ADMIN_ROLE` holder grants and revokes those roles. A privileged call checks the role first, then the matching pause vector. Holder `transfer` skips the role check; it still hits the `TRANSFER` pause vector and policy. -| Role | Gates | -| --- | --- | -| `DEFAULT_ADMIN_ROLE` | `grantRole`, `revokeRole`, `setRoleAdmin`, `updatePolicy`, `updateSupplyCap` | -| `MINT_ROLE` | `mint` | -| `BURN_ROLE` | `burn` | -| `SEIZE_ROLE` | `seizeWithMemo` | -| `PAUSE_ROLE` / `UNPAUSE_ROLE` | `pause` / `unpause` | -| `METADATA_ROLE` | name, symbol, and contract URI updates | +The full role list and what each role gates is in [Roles](./concepts/roles.md). A grant-then-call looks like this: -The Asset variant also has `OPERATOR_ROLE` for announcements and multiplier updates. See [Roles](./concepts/roles.md). +```mermaid +sequenceDiagram + participant Admin + participant Token as B20 token + participant Caller -Pause is per feature, not global. `PAUSE_ROLE` can pause any of four vectors: `TRANSFER`, `MINT`, `BURN`, and `SEIZE`. `UNPAUSE_ROLE` is a separate role, so the account that pauses does not have to be the account that resumes. `approve` is not pause-gated. + Caller->>Token: mint(to, amount) + Token-->>Caller: revert AccessControlUnauthorizedAccount -Holder `transfer` is not role-gated. Anyone who holds units can transfer them, subject to pause and policy. + Admin->>Token: grantRole(MINT_ROLE, Caller) + Caller->>Token: mint(to, amount) + Token-->>Caller: allowed +``` + +1. At creation, `initialAdmin` holds `DEFAULT_ADMIN_ROLE`. +2. That admin grants operating roles such as `MINT_ROLE` and `PAUSE_ROLE`. +3. A caller without the required role is rejected with `AccessControlUnauthorizedAccount`. +4. A caller who holds the role still reverts with `ContractPaused` if that feature's pause vector is on. +5. `PAUSE_ROLE` and `UNPAUSE_ROLE` are separate, so the account that pauses does not have to be the account that resumes. --- ## Integrating Compliance Checks -The Policy Registry is a singleton precompile that stores reusable allowlists, blocklists, and composite policies. The token does not keep membership lists. It stores which policy applies to an operation, then asks the registry whether the relevant account is allowed. - -```mermaid -flowchart LR - E[Policy engine] - A[Token admin] - R[Policy Registry] - T[B20 token] - E -->|update membership| R - A -->|updatePolicy| T - T -->|isAuthorized| R -``` +The Policy Registry is a global singleton precompile. It stores reusable allowlists, blocklists, and composite policies (union or intersect). Tokens do not keep membership lists. -A compliance system connects as a **policy engine** by administering membership on the registry. The token admin binds that policy to a scope with `updatePolicy`. One policy can be attached to many tokens. The token never calls the engine; the engine writes to the registry, and the node reads `isAuthorized` when it executes the call. +Each movement path consults its own policy scopes. `transfer` checks `TRANSFER_SENDER_POLICY` (`from`) and `TRANSFER_RECEIVER_POLICY` (`to`). `transferFrom` also checks `TRANSFER_EXECUTOR_POLICY` (`msg.sender`). `mint` checks `MINT_RECEIVER_POLICY` (`to`). -### Transfer control path +You add a policy on the registry, then attach its ID to a scope with `updatePolicy`. One policy can be attached to many tokens or many scopes. A compliance system administers membership on the registry. The token never calls that system; the node reads `isAuthorized` when it executes the call. -You submit a transfer the same way you submit any other onchain call: as a transaction to a Base node, targeting the asset address. +The full scope list is in [Policies](./concepts/policies.md). Attach-then-gate looks like this: ```mermaid -flowchart TD - U[User / Application] - N[Base node] - A[B20 Asset] - P[Pause check] - R[Policy Registry] - S[State + events] - U -->|transfer| N - N --> A - A --> P - P -->|sender and receiver| R - R --> S +sequenceDiagram + participant Admin + participant Registry as Policy Registry + participant Token as B20 token + participant Alice + + Admin->>Registry: createPolicy(ALLOWLIST) + Admin->>Token: updatePolicy(TRANSFER_RECEIVER_POLICY, id) + Alice->>Token: transfer(Bob) + Token->>Registry: isAuthorized(id, Bob) + Registry-->>Token: false + Token-->>Alice: revert PolicyForbids + + Admin->>Registry: updateAllowlist(Bob) + Alice->>Token: transfer(Bob) + Token->>Registry: isAuthorized(id, Bob) + Registry-->>Token: true + Token-->>Alice: allowed ``` -1. A wallet or application submits a transaction that calls `transfer` on the asset. -2. The node executes the call. If `TRANSFER` is paused, the transaction reverts. -3. The asset reads the policy IDs on `TRANSFER_SENDER_POLICY` (`from`) and `TRANSFER_RECEIVER_POLICY` (`to`), then asks the Policy Registry whether each account is authorized. `transferFrom` also checks `TRANSFER_EXECUTOR_POLICY` against `msg.sender`. -4. If those checks pass, the node updates balances and emits `Transfer`. If a check fails, the transaction reverts and state does not change. - -Slots default to always-allow until the admin attaches a policy. `approve` is not policy-gated. See [Policies](./concepts/policies.md). +1. Create an allowlist or blocklist on the registry. +2. The token admin binds that policy ID to a scope. +3. On the next matching call, the token asks the registry whether the relevant account is authorized. +4. Authorized: the call continues. Denied: the call reverts with `PolicyForbids`. +5. Unset scopes default to always-allow. `approve` is not policy-gated. You submit these calls the same way you submit any other transaction to a Base node. --- @@ -205,4 +206,4 @@ If you are implementing B20: For exact interfaces and protocol definitions: → [Reference](./reference/) -→ [Specifications](./specs/) \ No newline at end of file +→ [Specifications](./specs/) From f38a0dc82974b453a64caffb2992a9e367e8752c Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 15:37:17 -0400 Subject: [PATCH 08/13] docs(overview): add pause vectors and rewrite compliance Give roles, pause, and policy a why-then-how flow. Drop the stack section that duplicated What is B20. Co-authored-by: Cursor --- docs/overview.md | 82 ++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/docs/overview.md b/docs/overview.md index 7f478ea4..96fb7017 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -7,7 +7,6 @@ Why does it exist? How does the Factory create an asset? How do roles and pause work? How do compliance checks integrate? -Where does B20 sit in the Base stack? Where should I go next? Someone should be able to read just this and explain B20 at a high level. @@ -93,11 +92,11 @@ The Activation Registry is a Base-operated safety switch that turns Factory and ## Configuring Roles -B20 uses [OpenZeppelin AccessControl](https://docs.openzeppelin.com/contracts/5.x/access-control) on the token to gate privileged features: mint, burn, seize, metadata, and the pause vectors (`TRANSFER`, `MINT`, `BURN`, `SEIZE`). Roles are not a separate registry. +Roles let an issuer assign each privileged operation to a specific account. An admin can grant minting to a minter, seizing to a compliance operator, and pausing of a single feature (`TRANSFER`, `MINT`, `BURN`, or `SEIZE`) without pausing the rest of the token. -One `DEFAULT_ADMIN_ROLE` holder grants and revokes those roles. A privileged call checks the role first, then the matching pause vector. Holder `transfer` skips the role check; it still hits the `TRANSFER` pause vector and policy. +B20 implements this with [OpenZeppelin AccessControl](https://docs.openzeppelin.com/contracts/5.x/access-control) on the token. Roles are not a separate registry. One `DEFAULT_ADMIN_ROLE` holder grants and revokes the operating roles. A privileged call checks the role first, then the matching pause vector. Holder `transfer` skips the role check; it still hits the `TRANSFER` pause vector and policy. -The full role list and what each role gates is in [Roles](./concepts/roles.md). A grant-then-call looks like this: +The full role list and what each role gates is in [Roles](./concepts/roles.md). A role-gated call looks like this: ```mermaid sequenceDiagram @@ -116,20 +115,54 @@ sequenceDiagram 1. At creation, `initialAdmin` holds `DEFAULT_ADMIN_ROLE`. 2. That admin grants operating roles such as `MINT_ROLE` and `PAUSE_ROLE`. 3. A caller without the required role is rejected with `AccessControlUnauthorizedAccount`. -4. A caller who holds the role still reverts with `ContractPaused` if that feature's pause vector is on. -5. `PAUSE_ROLE` and `UNPAUSE_ROLE` are separate, so the account that pauses does not have to be the account that resumes. + +--- + +## Pause Vectors + +Pause vectors stop a class of operations on a token without pausing the rest of the asset. An issuer uses them when an off-chain workflow needs a feature frozen (for example a settlement window), or when a vulnerability is found and that path must stop immediately. + +Pause is per feature, not global. The four vectors are `TRANSFER`, `MINT`, `BURN`, and `SEIZE`. Pausing `MINT` halts new issuance while transfers continue. `approve` is not pause-gated. + +`pause` requires `PAUSE_ROLE`. `unpause` requires `UNPAUSE_ROLE`. Those roles are separate, so the account that pauses does not have to be the account that resumes. + +A paused call looks like this: + +```mermaid +sequenceDiagram + participant Pauser + participant Token as B20 token + participant Caller + participant Unpauser + + Caller->>Token: mint(to, amount) + Token-->>Caller: allowed + + Pauser->>Token: pause([MINT]) + Caller->>Token: mint(to, amount) + Token-->>Caller: revert ContractPaused(MINT) + + Unpauser->>Token: unpause([MINT]) + Caller->>Token: mint(to, amount) + Token-->>Caller: allowed +``` + +1. A caller who holds `MINT_ROLE` can mint while `MINT` is unpaused. +2. An account with `PAUSE_ROLE` pauses `MINT`. Other features stay live. +3. The next `mint` reverts with `ContractPaused(MINT)`, even if the caller still holds `MINT_ROLE`. +4. An account with `UNPAUSE_ROLE` unpauses `MINT`. Minting works again. --- ## Integrating Compliance Checks -The Policy Registry is a global singleton precompile. It stores reusable allowlists, blocklists, and composite policies (union or intersect). Tokens do not keep membership lists. +Most compliance checks reduce to a set of addresses and an allow-or-deny decision on a specific function. B20 uses that model instead of per-token hooks: you maintain an allowlist or blocklist, bind it to a function on the token, and the call proceeds or reverts. -Each movement path consults its own policy scopes. `transfer` checks `TRANSFER_SENDER_POLICY` (`from`) and `TRANSFER_RECEIVER_POLICY` (`to`). `transferFrom` also checks `TRANSFER_EXECUTOR_POLICY` (`msg.sender`). `mint` checks `MINT_RECEIVER_POLICY` (`to`). +Those lists live in the Policy Registry, a global singleton precompile, not on the token. Allowlists, blocklists, and composite policies (union or intersect) are stored there and referenced by policy ID. Because the registry is shared, one list can back many tokens: you maintain membership once, and every attached token sees the same result. -You add a policy on the registry, then attach its ID to a scope with `updatePolicy`. One policy can be attached to many tokens or many scopes. A compliance system administers membership on the registry. The token never calls that system; the node reads `isAuthorized` when it executes the call. +A token admin binds a policy ID to a policy scope with `updatePolicy`. A scope sits in a similar place to a hook: it runs on a specific function. When that function runs, the token asks the registry `isAuthorized(policyId, account)` and reverts with `PolicyForbids` if the check fails. Which scope runs on which function is in [Policies](./concepts/policies.md). -The full scope list is in [Policies](./concepts/policies.md). Attach-then-gate looks like this: +A policy-gated transfer looks like this: ```mermaid sequenceDiagram @@ -154,34 +187,9 @@ sequenceDiagram 1. Create an allowlist or blocklist on the registry. 2. The token admin binds that policy ID to a scope. -3. On the next matching call, the token asks the registry whether the relevant account is authorized. +3. On `transfer`, the token asks the registry whether the receiver is authorized. 4. Authorized: the call continues. Denied: the call reverts with `PolicyForbids`. -5. Unset scopes default to always-allow. `approve` is not policy-gated. You submit these calls the same way you submit any other transaction to a Base node. - ---- - -## B20 in the Stack - -A B20 call uses the same submission path as any other contract call. The node runs shared precompile logic instead of per-token bytecode. - -```mermaid -flowchart TD - A[Application / Wallet / Backend] - I[B20 interface] - P[B20 precompiles] - X[Base execution] - S[Canonical state] - A -->|transaction| I - I --> P - P --> X - X --> S -``` - -- **Application-facing interface.** Wallets and apps call ERC-20-style functions at the asset address. -- **B20 precompiles.** The Factory, each token, and the Policy Registry are node-native. Every asset shares the same logic. -- **Base execution.** The node applies authorization, policy, and pause checks, then commits canonical state and events. - -[How B20 Works](./architecture.md) walks this path through the dispatcher, version resolution, and storage. +5. Unset scopes default to always-allow. `approve` is not policy-gated. --- From feecd66f1f93b38e784d008a0e90ebcb1a91261a Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 16:43:01 -0400 Subject: [PATCH 09/13] docs: fill out constants/errors/events reference tables and rewrite architecture Fills in the constants, errors, and events reference tables with the actual role/policy hashes, error selectors, and event catalogue, and rewrites architecture.md around the precompile execution model and protocol-evolution guarantees. Co-Authored-By: Claude --- docs/architecture.md | 90 +++++++++++++++++++----------------- docs/concepts/policies.md | 14 ++++++ docs/reference/constants.md | 59 +++++++++++++++++++++++- docs/reference/errors.md | 91 ++++++++++++++++++++++++++++++++++++- docs/reference/events.md | 64 +++++++++++++++++++++++++- 5 files changed, 271 insertions(+), 47 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 8f66224c..305bc9b4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,57 +1,65 @@ -# How B20 Works +# B20 Execution Architecture -*The canonical explanation of the B20 system. Every serious B20 consumer — integrator, indexer, or implementer — should read this document. Audience guides link back here instead of re-explaining these mechanics.* +*How B20 actually executes: how its precompiles differ from ordinary contracts, how calls get routed to them, and how the protocol evolves without breaking history. For what each primitive means and how to use it (assets, roles, policies), see [Concepts](concepts/). For the "B20 in 10 minutes" tour, see [Overview](overview.md).* -## Execution model +## 1. How B20 Uses Precompiles -```text -Application - ↓ -B20 ABI - ↓ -Precompile dispatcher - ↓ -Version resolution - ↓ -Asset logic - ↓ -Policies / authorization - ↓ -State mutation - ↓ -Events -``` +### 1.1 Normal Contracts vs Precompiles +- On every `CALL`/`STATICCALL` (etc.), the EVM checks a precompile registry *before* it ever loads bytecode at the target address. +- If the address matches an entry in that registry, native code runs directly — bytecode is never loaded or interpreted. +- Only a registry miss falls through to normal contract execution (load bytecode → interpret). An address with no bytecode and no registry entry behaves like an empty account: the call stops immediately with no output — this is what a precompile address looks like *before* the hardfork that introduces it. +- Classic Ethereum precompiles (`ecrecover`, `sha256`, `ripemd160`, `modexp`, `ecadd`/`ecmul`/`ecpairing`, `blake2f`, etc.) are looked up through this exact same registry. B20 doesn't bypass or extend the EVM's dispatch path — it plugs into it. -_TODO — narrative walkthrough of the pipeline above._ +### 1.2 B20's Native Contract Model +- B20 tokens, the Factory, the Policy Registry, and the Activation Registry are all precompiles: native logic hosted by the execution client at a fixed or derived address, not deployed EVM bytecode. +- Unlike classic precompiles — pure, stateless functions — B20's precompiles are stateful: they hold persistent storage (balances, roles, policy IDs, pause state, ...) and emit real events. They behave as system contracts, not one-shot pure functions. +- Because they're stateful, they aren't cacheable the way a pure function's result would be — every call re-reads live storage. +- From the outside, calling a B20 precompile looks identical to calling a normal ERC-20 contract: same ABI encoding, same `CALL` semantics. The precompile nature is invisible above the EVM. -## Lifecycle of an asset +### 1.3 B20 Address Space +- Fixed-address precompiles (Factory, Policy Registry, Activation Registry) sit at known, hardcoded addresses (`StdPrecompiles`). +- B20 token addresses are different — derived, not fixed, and self-describing: `0xB2` prefix + variant byte + `keccak256(sender, salt)` suffix, computed at creation time. +- The address alone answers "is this a B20 token" and "which variant" (`isB20`, `getB20Address`) — no external registry lookup needed to recognize one. -_TODO — creation via the factory through to end-of-life states._ +### 1.4 How Calls Are Routed +- Fixed-address precompiles are matched directly in a static registry table. +- B20 token addresses can't be pre-registered individually — they're created at runtime, so there's no fixed list to check against. They're resolved through a dynamic fallback lookup that decodes the variant straight out of the address itself and builds the right dispatcher (Asset vs Stablecoin) on the fly. +- This is exactly why the address encoding in §1.3 exists — it's what makes dynamic routing possible without a token registry. -## State model +### 1.5 Where State Lives +- State backing a B20 precompile lives in the execution client's own state — the same storage substrate as contract storage — not inside "fake bytecode." Reads and writes are metered with the same gas costs as native `SLOAD`/`SSTORE`. +- Each precompile (Factory, Policy Registry, each token) owns its own storage. There's no shared global state between tokens beyond what's explicitly referenced — e.g. a policy ID pointing at a shared entry in the Policy Registry. -_TODO_ +### 1.6 How a B20 Call Executes +- Once a call is routed to a precompile: reject an unexpected value transfer (nonpayable) → charge calldata gas → resolve the active logic version for the current hardfork → decode the call against that version's frozen ABI → run the operation's checks (role, pause, policy) → mutate state → emit events. +- Gas isn't a single flat fee: a calldata cost plus metered storage/log costs, charged on the same schedule as native opcodes (warm/cold access, refunds included). A precompile that reports using more gas than the call's limit halts the call — the same outcome as running out of gas mid-execution. +- Activation gating happens inside this step, not by hiding the address: once a hardfork introduces a precompile, the address always exists from then on. An inactive feature makes specific write operations revert rather than removing the address from the registry — and deactivating a feature blocks *new* creation, it doesn't retroactively disable assets that already exist. -## Policy model +## 2. How B20 Evolves -_TODO — see [Policies](concepts/policies.md) for the full model._ +### 2.1 Protocol Upgrades +- B20 changes ship as part of hardforks (e.g. Beryl → Cobalt) — the same mechanism that gates any other protocol-level change. +- A hardfork can introduce an entirely new precompile (the Activation Registry itself only exists from Beryl onward) or a new logic version for an existing one. -## Permissions / roles +### 2.2 Logic Versions +- Each precompile's logic is versioned. Once a version ships, it's frozen forever — self-contained, with no shared mutable state or traits across versions. +- Why: editing logic in place at a fixed address would change execution for historical blocks too, breaking replay from genesis. Freezing is what preserves consensus. -_TODO — see [Roles](concepts/roles.md) for the full model._ +### 2.3 Fork / Version Resolution +- A hardfork resolves to a specific logic version (fork → version enum → frozen implementation) — resolved once per call, never "whatever is current." +- A call reverts if no version is resolved for the active fork (calling logic that doesn't exist yet), rather than silently falling back to a default. -## Events +### 2.4 Adding New Functions +- New functionality ships as a new frozen version alongside the old ones — never by editing an existing version in place. +- Additive-only guarantee: a hardfork can add selectors, events, and errors; it never removes or changes ones that already shipped. -_TODO — see [Events reference](reference/events.md) for the exhaustive list._ +### 2.5 ABI Evolution +- The logic interface itself is append-only: new versions may add methods, never remove or change existing signatures. +- Deprecated symbols are kept, not deleted (e.g. `burnBlocked`, the instant `updateMultiplier`) — old callers keep working unchanged. -## Upgrades / versioning +### 2.6 Historical Execution +- Old transactions replay deterministically: the dispatcher resolves the version that was active *at that block's fork*, not "current" logic — so replaying history always re-executes the version that was live at the time. -_TODO — see [Versioning](concepts/versioning.md) for the full model._ - -## Invariants - -_TODO_ - -## Full transaction walkthrough - -_TODO — trace one transaction end-to-end through every layer above._ +### 2.7 Backwards Compatibility +- Nothing that already shipped changes meaning — existing selectors, events, and errors keep their exact semantics across every later fork. +- Consumers integrated against an old version keep working after a new version ships alongside it. They simply don't get new capabilities until they adopt the new ABI surface. diff --git a/docs/concepts/policies.md b/docs/concepts/policies.md index 3dfc27e5..2d8a3756 100644 --- a/docs/concepts/policies.md +++ b/docs/concepts/policies.md @@ -28,3 +28,17 @@ _TODO — every scope defaults to `ALWAYS_ALLOW` at token creation unless overri ## Configuring policies _TODO_ + + + +# Policies + +- What is a policy? +- What problem does it solve? +- Policy IDs +- Policy scopes +- Allowlist vs blocklist +- Composite policies +- How policies are shared +- How policy administration works +- Examples \ No newline at end of file diff --git a/docs/reference/constants.md b/docs/reference/constants.md index 7e03b7d7..23f40a8d 100644 --- a/docs/reference/constants.md +++ b/docs/reference/constants.md @@ -1,7 +1,62 @@ # Constants -*Role identifiers, policy-type identifiers, and other fixed constants. See [`B20Constants`](../../src/lib/B20Constants.sol).* +*Role identifiers, policy-type identifiers, precompile addresses, and other fixed constants. See [`B20Constants`](../../src/lib/B20Constants.sol) and [`StdPrecompiles`](../../src/StdPrecompiles.sol).* + +## Precompile addresses + +*Fixed addresses of Base's singleton precompiles. See [`StdPrecompiles`](../../src/StdPrecompiles.sol).* + +| Name | Value | Purpose | +|---|---|---| +| `B20_FACTORY_ADDRESS` | `0xB20f000000000000000000000000000000000000` | Deploys and looks up B-20 tokens; every asset and stablecoin instance is created through the [`IB20Factory`](../../src/interfaces/IB20Factory.sol) at this address. | +| `POLICY_REGISTRY_ADDRESS` | `0x8453000000000000000000000000000000000002` | Stores allowlist/blocklist/composite policies and answers `isAuthorized` checks consulted by every policy scope (see [Policies](../concepts/policies.md)). | +| `ACTIVATION_REGISTRY_ADDRESS` | `0x8453000000000000000000000000000000000001` | Gates whether a B-20 variant or feature is live on a given chain; checked by the factory before it will create that variant. | + +## Roles + +*Role identifiers checked via `hasRole`. See [`B20Constants`](../../src/lib/B20Constants.sol) and [`IB20`](../../src/interfaces/IB20.sol). Hex values are `keccak256` of the role name, verified with `cast keccak ""` and cross-checked in `chisel`.* + +| Name | Value | Purpose | +|---|---|---| +| `DEFAULT_ADMIN_ROLE` | `bytes32(0)` | Required to call `grantRole`, `revokeRole`, `setRoleAdmin`, `updatePolicy`, and `updateSupplyCap`. | +| `MINT_ROLE` | `keccak256("MINT_ROLE")`
`0x154c00819833dac601ee5ddded6fda79d9d8b506b911b3dbd54cdb95fe6c3686` | Required to call `mint` and `mintWithMemo`. | +| `BURN_ROLE` | `keccak256("BURN_ROLE")`
`0xe97b137254058bd94f28d2f3eb79e2d34074ffb488d042e3bc958e0a57d2fa22` | Required to call `burn` and `burnWithMemo`. | +| `BURN_BLOCKED_ROLE` | `keccak256("BURN_BLOCKED_ROLE")`
`0x7408fdc0d31c7bcb349eab611f5d1168acd4303574993f8cdc98b1cd18c41cae` | Required to call the deprecated `burnBlocked`. | +| `SEIZE_ROLE` | `keccak256("SEIZE_ROLE")`
`0x3469b8b0d89e9604f8510ed143f74a8336d22955d4f83e23bf53d9414e27f432` | Required to call `seizeWithMemo`. | +| `PAUSE_ROLE` | `keccak256("PAUSE_ROLE")`
`0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d` | Required to call `pause`. | +| `UNPAUSE_ROLE` | `keccak256("UNPAUSE_ROLE")`
`0x265b220c5a8891efdd9e1b1b7fa72f257bd5169f8d87e319cf3dad6ff52b94ae` | Required to call `unpause`. | +| `METADATA_ROLE` | `keccak256("METADATA_ROLE")`
`0x6bd6b5318a46e5fff572d5e4258a20774aab40cc35ac7680654b9081fcc82f80` | Required to call `updateName`, `updateSymbol`, `updateContractURI`, and `updateExtraMetadata`. | +| `OPERATOR_ROLE` | `keccak256("OPERATOR_ROLE")`
`0x97667070c54ef182b0f5858b034beac1b6f3089aa2d3188bb1e8929f4fa9b929` | B20Asset-only. Required to call `announce`, `updateUIMultiplier`, `cancelUIMultiplierUpdate`, and the deprecated `updateMultiplier`. | + +## Policy types + +*Policy scopes consulted by the PolicyRegistry. See [`B20Constants`](../../src/lib/B20Constants.sol) and [Policies](../concepts/policies.md). Hex values are `keccak256` of the policy name, verified with `cast keccak ""` and cross-checked in `chisel`.* + +| Name | Value | Purpose | +|---|---|---| +| `TRANSFER_SENDER_POLICY` | `keccak256("TRANSFER_SENDER_POLICY")`
`0xb81736c875ab819dd97f59f2a6542cfb731ad52b4ae15a6f24df2fb02b0327f5` | Consulted for `from` on `transfer` and `transferFrom`. | +| `TRANSFER_RECEIVER_POLICY` | `keccak256("TRANSFER_RECEIVER_POLICY")`
`0x8a4b3fa2d8b921852bc0089c6ef0958aa6961897be36fd731330fe2cd23f8363` | Consulted for `to` on `transfer` and `transferFrom`. | +| `TRANSFER_EXECUTOR_POLICY` | `keccak256("TRANSFER_EXECUTOR_POLICY")`
`0x10be5173aff2a44e748bd9acd8b19fe34689581398a9db7ba2fb671e786ff7d8` | Consulted for `msg.sender` on `transferFrom` only. | +| `MINT_RECEIVER_POLICY` | `keccak256("MINT_RECEIVER_POLICY")`
`0xa0d5ae037e66a09119acf080a1d807abb9b6d03b6b9130eb19f7c1e6bdb8ffc8` | Consulted for `to` on `mint`. | +| `SEIZE_HOLDER_POLICY` | `keccak256("SEIZE_HOLDER_POLICY")`
`0x1497ab2b67ebb0a75dd9cdd6aec9f0e64620e6b87e911af7a088ac12e58d9ef2` | Consulted for `from` on `seizeWithMemo`; `from` is seizable when unauthorized under this policy. | +| `SEIZE_RECEIVER_POLICY` | `keccak256("SEIZE_RECEIVER_POLICY")`
`0xbf15b19caf5c77422c038bc25f26b8b815c3a14f6d04c6616076b81bcfe07b3d` | Consulted for `to` on `seizeWithMemo`. | + +## Feature and validation bounds + +*Bitmasks and inclusive bounds used for pause features and B20Asset creation validation. See [`B20Constants`](../../src/lib/B20Constants.sol).* + +| Name | Value | Purpose | +|---|---|---| +| `ALL_FEATURES_PAUSED` | `15` (`0b1111`) | Bitmask with all `PausableFeature` bits set (`TRANSFER \| MINT \| BURN \| SEIZE`). | +| `MIN_ASSET_DECIMALS` | `6` | Inclusive lower bound for `B20AssetCreateParams.decimals`; the floor most stablecoin-grade integrations expect. | +| `MAX_ASSET_DECIMALS` | `18` | Inclusive upper bound for `B20AssetCreateParams.decimals`; the ERC-20 community ceiling every common wallet/indexer renders correctly. | +| `MAX_SUPPLY_CAP` | `type(uint128).max` | Inclusive upper bound for the supply cap (and therefore `totalSupply`); doubles as the unbounded ("no cap") sentinel. | + +## Asset-variant precision constants + +*Fixed-point constants used by the multiplier/rebasing surface. See [`IB20Asset`](../../src/interfaces/IB20Asset.sol).* | Name | Value | Purpose | |---|---|---| -| _TODO_ | | | +| `WAD_PRECISION` | `1e18` | Fixed-point precision used to scale `multiplier`; `multiplier`, `toUIAmount`, and `fromUIAmount` all divide/multiply by this. | +| `MAX_UI_MULTIPLIER` | `type(uint128).max` | Maximum multiplier the setters accept — the overflow guard enforced by `updateMultiplier` and `updateUIMultiplier`. Exposed so callers can read the bound without triggering `InvalidMultiplier`. | diff --git a/docs/reference/errors.md b/docs/reference/errors.md index 0a42d780..08a2c793 100644 --- a/docs/reference/errors.md +++ b/docs/reference/errors.md @@ -1,7 +1,94 @@ # Errors -*Exhaustive list of custom errors, selectors, and the conditions that trigger them.* +*Exhaustive list of custom errors, selectors, and the conditions that trigger them. Selectors are the 4-byte `keccak256` hash of the error signature — computed with `cast sig "ErrorName(types...)"`. Enum parameters encode as their underlying `uint8`.* + +*Note: several error names are reused across files with different parameters (or none), which changes the selector. `PolicyNotFound()` ([`IPolicyRegistry`](../../src/interfaces/IPolicyRegistry.sol)) and `PolicyNotFound(uint64)` ([`IB20`](../../src/interfaces/IB20.sol)) are unrelated errors with different selectors, as are `Unauthorized()` (`IB20` / `IPolicyRegistry`) and `Unauthorized(address)` ([`IActivationRegistry`](../../src/interfaces/IActivationRegistry.sol)). Conversely, `LengthMismatch(uint256,uint256)` shares one selector across [`IB20Asset`](../../src/interfaces/IB20Asset.sol) and [`B20FactoryLib`](../../src/lib/B20FactoryLib.sol) — they're independently declared but identical in signature.* + +## [`IB20`](../../src/interfaces/IB20.sol) + +| Error | Selector | Thrown when | +|---|---|---| +| `NonPayable()` | `0x6fb1b0e9` | ETH was attached to a call targeting a nonpayable token selector. | +| `AccessControlUnauthorizedAccount(address account, bytes32 neededRole)` | `0xe2517d3f` | `account` does not hold `neededRole`. | +| `Unauthorized()` | `0x82b42900` | Caller failed a positional authorization check that isn't expressible as "missing role X". | +| `ContractPaused(uint8 feature)` | `0xfd8c4245` | The `PausableFeature` covering the operation is currently paused. | +| `InsufficientAllowance(address spender, uint256 allowance, uint256 needed)` | `0x192b9e4e` | `spender`'s allowance is less than `needed` for the requested `transferFrom`. | +| `InsufficientBalance(address sender, uint256 balance, uint256 needed)` | `0xdb42144d` | `sender`'s balance is less than `needed` for the requested transfer or burn. | +| `InvalidSender(address sender)` | `0x4c14f64c` | The transfer's source address is invalid (typically `address(0)`). | +| `InvalidReceiver(address receiver)` | `0x9cfea583` | The transfer's destination address is invalid (typically `address(0)`). | +| `InvalidApprover(address approver)` | `0x8bc146c4` | The approval's `owner` address is invalid (typically `address(0)`). | +| `InvalidSpender(address spender)` | `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(uint256 currentSupply, uint256 proposedCap)` | `0x0a3780ce` | The proposed supply cap is below the current `totalSupply`, or above `type(uint128).max`. | +| `SupplyCapExceeded(uint256 cap, uint256 attempted)` | `0x4b344b11` | The mint would push `totalSupply` past the configured cap. | +| `PolicyForbids(bytes32 policyScope, uint64 policyId)` | `0xa43fec12` | A policy slot denied the operation. | +| `PolicyNotFound(uint64 policyId)` | `0xcccad523` | The provided policy ID does not exist in the policy registry. | +| `UnsupportedPolicyType(bytes32 policyScope)` | `0xcdd98a4a` | `policyScope` is not a slot this token (or its variant) supports. | +| `AccountNotSeizable(address account)` | `0x91dbbc8d` | `seizeWithMemo` was called against a `from` that is not seizable under `SEIZE_HOLDER_POLICY`. | +| `AccountNotBlocked(address account)` | `0x64a5cb46` | The deprecated `burnBlocked` was called against a `from` that is currently authorized under `TRANSFER_SENDER_POLICY` (i.e. not blocked). | +| `ExpiredSignature(uint256 deadline)` | `0xbd2a913c` | An EIP-2612 `permit` was submitted with a `deadline` strictly less than `block.timestamp`. | +| `InvalidSigner(address signer, address owner)` | `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`. | + +## [`IB20Asset`](../../src/interfaces/IB20Asset.sol) + +| Error | Selector | Thrown when | +|---|---|---| +| `AnnouncementIdAlreadyUsed(string id)` | `0xd10b3c9e` | `announce` was called with an `id` that has already been consumed. | +| `InvalidMetadataKey()` | `0x86ea3abb` | `updateExtraMetadata` was called with an empty `key`. | +| `InvalidMultiplier()` | `0x6f12f3dc` | A multiplier setter (`updateUIMultiplier` or the deprecated `updateMultiplier`) was called with a multiplier of zero or above the `type(uint128).max` overflow guard. | +| `EffectiveAtInPast(uint256 effectiveAt)` | `0x14119cf6` | `updateUIMultiplier` was called with an `effectiveAt` that is not in the future. | +| `EffectiveAtTooFar(uint256 effectiveAt)` | `0x1ce214fa` | `updateUIMultiplier` was called with an `effectiveAt` above `type(uint64).max`. | +| `UIMultiplierUpdateExists(uint256 effectiveAt)` | `0x4481a68e` | `updateUIMultiplier` was called while a live pending update already exists. | +| `UIMultiplierUpdateDoesNotExist()` | `0xa7d6a5ca` | `cancelUIMultiplierUpdate` was called when there is no live pending update. | +| `LengthMismatch(uint256 leftLen, uint256 rightLen)` | `0xab8b67c6` | A batched function was called with parallel arrays of differing lengths. | +| `EmptyBatch()` | `0xc2e5347d` | A batched function was called with empty arrays. | +| `AnnouncementInProgress()` | `0x5c5f0829` | An inner call dispatched by `announce` tried to re-invoke `announce`. | +| `InternalCallMalformed(bytes call)` | `0x4e2f143e` | An inner call dispatched by `announce` was shorter than four bytes. | +| `InternalCallFailed(bytes call)` | `0xb288a127` | An inner call dispatched by `announce` reverted with an ordinary revert (reason not bubbled). | + +## [`IB20Factory`](../../src/interfaces/IB20Factory.sol) + +| Error | Selector | Thrown when | +|---|---|---| +| `NonPayable()` | `0x6fb1b0e9` | ETH was attached to a call targeting a nonpayable factory selector. | +| `TokenAlreadyExists(address token)` | `0x15ef3a57` | A token already exists at the deterministic address derived from `(variant, msg.sender, salt)`. | +| `InvalidVariant()` | `0xf10e8e43` | `variant` is not a recognized `B20Variant`. | +| `UnsupportedVersion(uint8 version, uint8 variant)` | `0xc0d8b4e0` | The leading `version` byte in `params` does not match any known encoding for the requested variant. | +| `MissingRequiredField(string field)` | `0x4a43ae87` | A required string argument was the empty string. | +| `InvalidCurrency(string code)` | `0x997c1de8` | The stablecoin `currency` was non-empty but contained a non-`A`-`Z` byte. | +| `InvalidDecimals(uint8 decimals)` | `0xca950391` | The asset `decimals` was outside `[B20Constants.MIN_ASSET_DECIMALS, B20Constants.MAX_ASSET_DECIMALS]`. | +| `InitCallFailed(uint256 index)` | `0x4eae0860` | One of the `initCalls` reverted with no bubbled reason. | + +## [`IPolicyRegistry`](../../src/interfaces/IPolicyRegistry.sol) + +| Error | Selector | Thrown when | +|---|---|---| +| `NonPayable()` | `0x6fb1b0e9` | ETH was attached to a call targeting a nonpayable policy registry selector. | +| `Unauthorized()` | `0x82b42900` | Caller is not the admin required by the attempted operation. | +| `PolicyNotFound()` | `0x720caa4f` | The referenced policy ID does not exist. | +| `IncompatiblePolicyType()` | `0xf1011ef5` | The operation is incompatible with the policy's type. | +| `ZeroAddress()` | `0xd92e233d` | A required address argument was the zero address. | +| `BatchSizeTooLarge(uint256 maxBatchSize)` | `0x083e2f67` | A membership batch exceeded the registry limit. | +| `NoPendingAdmin()` | `0xb4539afa` | `finalizeUpdateAdmin` was called with no pending admin staged. | +| `ChildPoliciesOutsideOfRange()` | `0x697ec868` | A composite policy was created or updated with a child-policy count outside `[MIN_COMPOSITE_CHILD_POLICIES, MAX_COMPOSITE_CHILD_POLICIES]`. | +| `InvalidChildPolicy(uint64 childPolicyId)` | `0x46508ef6` | A child policy is not an existing simple (ALLOWLIST/BLOCKLIST) policy. | + +## [`IActivationRegistry`](../../src/interfaces/IActivationRegistry.sol) + +| Error | Selector | Thrown when | +|---|---|---| +| `Unauthorized(address caller)` | `0x8e4a23d6` | Caller is not the activation admin. | +| `AlreadyActivated(bytes32 feature)` | `0x866b0041` | `activate` was called on a feature that is already activated. | +| `FeatureNotActivated(bytes32 feature)` | `0xb9b2a425` | `checkActivated` was called on an inactive feature, or `deactivate` was called on a feature that is already inactive. | +| `DelegateCallNotAllowed()` | `0x0d89438e` | The precompile was invoked via `DELEGATECALL` or `CALLCODE`. | +| `StaticCallNotAllowed()` | `0xbeaba5b7` | A state-mutating entry point was invoked from a `STATICCALL` frame. | + +## [`B20FactoryLib`](../../src/lib/B20FactoryLib.sol) | Error | Selector | Thrown when | |---|---|---| -| _TODO_ | | | +| `LengthMismatch(uint256 leftLen, uint256 rightLen)` | `0xab8b67c6` | Two parallel arrays passed to a `build*` helper had different lengths. | diff --git a/docs/reference/events.md b/docs/reference/events.md index c93b1228..e7c0487e 100644 --- a/docs/reference/events.md +++ b/docs/reference/events.md @@ -1,7 +1,67 @@ # Events -*Exhaustive list of events emitted by the B20 system.* +*Exhaustive list of events emitted by the B20 system, grouped by declaring file.* + +## [`IB20`](../../src/interfaces/IB20.sol) + +| Event | Emitted by | When | +|---|---|---| +| `Transfer(address indexed from, address indexed to, uint256 amount)` | `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `mint`, `mintWithMemo`, `burn`, `burnWithMemo`, `burnBlocked`, `seizeWithMemo` | Every successful transfer, mint (`from = address(0)`), or burn (`to = address(0)`), including memo'd, blocked-burn, and seize variants. | +| `Approval(address indexed owner, address indexed spender, uint256 amount)` | `approve`, `permit` | An allowance is set. | +| `Memo(address indexed caller, bytes32 indexed memo)` | `transferWithMemo`, `transferFromWithMemo`, `mintWithMemo`, `burnWithMemo` | Immediately after the underlying `Transfer` event. `caller` is the `msg.sender` of the memo'd call. | +| `BurnedBlocked(address indexed caller, address indexed from, uint256 amount)` | `burnBlocked` (deprecated) | In addition to `Transfer(from, address(0), amount)`. | +| `Seized(address indexed caller, address indexed from, address indexed to, uint256 amount)` | `seizeWithMemo` | In addition to `Transfer(from, to, amount)` and `Memo(caller, memo)`. Records a transfer-based seizure. | +| `RoleGranted(bytes32 indexed role, address indexed account, address indexed sender)` | `grantRole` | `account` is granted `role`. `sender` is the originating caller. | +| `RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender)` | `revokeRole`, `renounceRole`, `renounceLastAdmin` | `role` is revoked from `account`. `sender` is the admin bearer (`revokeRole`) or `account` itself (`renounceRole`/`renounceLastAdmin`). | +| `RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole)` | `setRoleAdmin` | The admin role for `role` changes. | +| `LastAdminRenounced(address indexed previousAdmin)` | `renounceLastAdmin` | In addition to the standard `RoleRevoked(DEFAULT_ADMIN_ROLE, previousAdmin, previousAdmin)` event. | +| `Paused(address indexed updater, PausableFeature[] features)` | `pause` | `features` is the call argument (not the resulting paused state). | +| `Unpaused(address indexed updater, PausableFeature[] features)` | `unpause` | `features` is the call argument (not the resulting paused state). | +| `PolicyUpdated(bytes32 indexed policyScope, uint64 oldPolicyId, uint64 newPolicyId)` | `updatePolicy`; also token creation | A token's policy slot changes. Initial slot assignment at creation also emits this with `oldPolicyId == 0`. | +| `SupplyCapUpdated(address indexed updater, uint256 oldSupplyCap, uint256 newSupplyCap)` | `updateSupplyCap` | The supply cap changes. | +| `ContractURIUpdated()` | `updateContractURI` | Parameterless per ERC-7572; integrators re-fetch `contractURI()`. | +| `NameUpdated(address indexed updater, string newName)` | `updateName` | The token name changes. Carries the new name string. | +| `SymbolUpdated(address indexed updater, string newSymbol)` | `updateSymbol` | The token symbol changes. Carries the new symbol string. | +| `EIP712DomainChanged()` | `updateName` | ERC-5267 domain-change signal, emitted exactly once per successful call, immediately after `NameUpdated`. `updateSymbol` does NOT emit this. | + +## [`IB20Asset`](../../src/interfaces/IB20Asset.sol) + +| Event | Emitted by | When | +|---|---|---| +| `MultiplierUpdated(uint256 multiplier)` | `updateMultiplier` (deprecated instant setter) | Deprecated legacy-topic mirror, emitted alongside `UIMultiplierUpdated` so indexers on the old topic keep working.[^1] | +| `UIMultiplierUpdateCancelled(uint256 cancelledMultiplier, uint256 cancelledEffectiveAt)` | `cancelUIMultiplierUpdate`; `updateUIMultiplier` | A scheduled multiplier update is cancelled — explicitly, or implicitly when `updateUIMultiplier` clears a live pending update. | +| `ExtraMetadataUpdated(string key, string value)` | `updateExtraMetadata` | An extra-metadata entry is set, updated, or removed (empty `value` indicates removal). | +| `Announcement(address indexed caller, string id, string description, string uri)` | `announce` | Opens an announcement bracket. | +| `EndAnnouncement(string id)` | `announce` | Closes the bracket opened by the paired `Announcement` with the same `id`. | + +[^1]: The function-level docs show only `updateMultiplier` emitting `MultiplierUpdated`; the scheduled `updateUIMultiplier` emits `UIMultiplierUpdated` only. The event's own doc-comment in source additionally names `updateUIMultiplier` as an emitter of `MultiplierUpdated`, which conflicts with `updateUIMultiplier`'s own `@notice` — flagging here rather than silently picking one. + +## [`IB20Factory`](../../src/interfaces/IB20Factory.sol) + +| Event | Emitted by | When | +|---|---|---| +| `B20Created(address indexed token, B20Variant indexed variant, string name, string symbol, uint8 decimals, bytes variantEventParams)` | `createB20` | Once per invocation, after the token's identity is sealed and before any `initCalls` are dispatched. `variantEventParams` carries variant-specific identity data (empty for ASSET; ABI-encoded `B20StablecoinEventParams` for STABLECOIN). | + +## [`IPolicyRegistry`](../../src/interfaces/IPolicyRegistry.sol) + +| Event | Emitted by | When | +|---|---|---| +| `PolicyCreated(uint64 indexed policyId, address indexed creator, PolicyType policyType)` | `createPolicy`, `createPolicyWithAccounts`, `createCompositePolicy` | A new policy is created. | +| `PolicyAdminStaged(uint64 indexed policyId, address indexed currentAdmin, address indexed pendingAdmin)` | `stageUpdateAdmin` | A new admin is staged. `pendingAdmin == address(0)` clears a prior nomination. | +| `PolicyAdminUpdated(uint64 indexed policyId, address indexed previousAdmin, address indexed newAdmin)` | `finalizeUpdateAdmin`, `renounceAdmin`; also policy creation | The active admin changes. `newAdmin == address(0)` indicates renunciation; `previousAdmin == address(0)` indicates initial assignment at creation. | +| `AllowlistUpdated(uint64 indexed policyId, address indexed updater, bool allowed, address[] accounts)` | `updateAllowlist` | One or more accounts have their ALLOWLIST membership set to `allowed` in a single batch. | +| `BlocklistUpdated(uint64 indexed policyId, address indexed updater, bool blocked, address[] accounts)` | `updateBlocklist` | One or more accounts have their BLOCKLIST membership set to `blocked` in a single batch. | +| `CompositePolicyUpdated(uint64 indexed policyId, address indexed updater, uint64[] childPolicyIds)` | `createCompositePolicy`, `updateComposite` | A composite policy's child set is set or replaced in full. Emitted on creation and on every subsequent update; carries the complete post-update set. | + +## [`IActivationRegistry`](../../src/interfaces/IActivationRegistry.sol) + +| Event | Emitted by | When | +|---|---|---| +| `FeatureActivated(bytes32 indexed feature, address indexed caller)` | `activate` | `feature` is activated. | +| `FeatureDeactivated(bytes32 indexed feature, address indexed caller)` | `deactivate` | `feature` is deactivated. | + +## [`IERC8056`](../../src/interfaces/IERC8056.sol) (`IScaledUIAmount`) | Event | Emitted by | When | |---|---|---| -| _TODO_ | | | +| `UIMultiplierUpdated(uint256 oldMultiplier, uint256 newMultiplier, uint256 effectiveAtTimestamp)` | `updateUIMultiplier` (scheduled); `updateMultiplier` (deprecated instant setter) | The UI multiplier is updated — scheduled setters emit this alone; the deprecated instant setter emits this alongside `MultiplierUpdated`. | From d8991fb0111287f6416428b0dcb6d862904ad053 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 16:47:49 -0400 Subject: [PATCH 10/13] docs: remove versions.md in favor of changelog hardfork ordinals The changelog/README.md ordinal table is already the source of truth for hardfork<->version mapping, so a separate reference/versions.md duplicated it. Repoint the three referring TODOs at the changelog index instead. Co-Authored-By: Claude --- docs/README.md | 2 +- docs/concepts/versioning.md | 2 +- docs/guides/indexers.md | 2 +- docs/reference/versions.md | 7 ------- 4 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 docs/reference/versions.md diff --git a/docs/README.md b/docs/README.md index 9e4618d6..1a36d3c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,4 +14,4 @@ Building something? Looking for exact technical details? - [Concepts](concepts/) — the mental model: assets, policies, roles, execution, versioning -- [Reference](reference/) — interfaces, events, errors, constants, versions +- [Reference](reference/) — interfaces, events, errors, constants diff --git a/docs/concepts/versioning.md b/docs/concepts/versioning.md index 7aa7705f..711a3144 100644 --- a/docs/concepts/versioning.md +++ b/docs/concepts/versioning.md @@ -16,4 +16,4 @@ _TODO — ABI/selector stability guarantees for existing integrations._ ## Where to check the current version -_TODO — see [Versions reference](../reference/versions.md) and [`CHANGELOG.md`](../../CHANGELOG.md)._ +_TODO — see [`CHANGELOG.md`](../../CHANGELOG.md) and the [changelog index's hardfork ordinals](../../changelog/README.md#hardfork-ordinals)._ diff --git a/docs/guides/indexers.md b/docs/guides/indexers.md index 1eb7cb95..c28ecd77 100644 --- a/docs/guides/indexers.md +++ b/docs/guides/indexers.md @@ -24,4 +24,4 @@ _TODO — see [Versioning](../concepts/versioning.md) for the underlying model._ ## Schema / version compatibility -_TODO — see [Versions reference](../reference/versions.md)._ +_TODO — see the [changelog index's hardfork ordinals](../../changelog/README.md#hardfork-ordinals)._ diff --git a/docs/reference/versions.md b/docs/reference/versions.md deleted file mode 100644 index 83190624..00000000 --- a/docs/reference/versions.md +++ /dev/null @@ -1,7 +0,0 @@ -# Versions - -*B20 versions by hardfork. See [`CHANGELOG.md`](../../CHANGELOG.md) for behavioral changes and [`changelog/`](../../changelog/README.md) for selector-level detail.* - -| Version | Hardfork | Changes | -|---|---|---| -| _TODO_ | | | From b0e15575fb91fc76bb70a56ae212e159e6539092 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 17:05:24 -0400 Subject: [PATCH 11/13] docs(architecture): explain precompiles as native client dispatch Rewrite the contracts-vs-precompiles section into a continuous narrative so readers can follow native execution, self-managed state and gas, registry routing, and the shared EVM flow. Co-authored-by: Cursor --- docs/architecture.md | 46 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 305bc9b4..d7183ea5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -5,10 +5,48 @@ ## 1. How B20 Uses Precompiles ### 1.1 Normal Contracts vs Precompiles -- On every `CALL`/`STATICCALL` (etc.), the EVM checks a precompile registry *before* it ever loads bytecode at the target address. -- If the address matches an entry in that registry, native code runs directly — bytecode is never loaded or interpreted. -- Only a registry miss falls through to normal contract execution (load bytecode → interpret). An address with no bytecode and no registry entry behaves like an empty account: the call stops immediately with no output — this is what a precompile address looks like *before* the hardfork that introduces it. -- Classic Ethereum precompiles (`ecrecover`, `sha256`, `ripemd160`, `modexp`, `ecadd`/`ecmul`/`ecpairing`, `blake2f`, etc.) are looked up through this exact same registry. B20 doesn't bypass or extend the EVM's dispatch path — it plugs into it. + +Precompiles are code compiled into the node client. Unlike regular smart contracts, they are not deployed as EVM bytecode and the EVM interpreter does not execute them. They run as native code, so they bypass the opcode-by-opcode interpreter loop: decode, execute, update stack and memory, then repeat. That native path is why they are faster. Ethereum introduced them because some operations, such as hashing and cryptographic primitives, were too expensive to run efficiently in the EVM. Callers still see a contract-like interface. + +Because the interpreter is not in the path, a precompile implements its own state access and gas accounting. State is still stored through the EVM state model, the same way regular contracts store state. Gas metering is defined by the precompile itself rather than by per-opcode interpreter costs. + +The node decides which path to take. On every `CALL`, `STATICCALL`, and related opcode, the EVM checks a precompile registry before it loads bytecode at the target address. If the address is registered, native code runs and bytecode is never loaded or interpreted. If it is not registered, the node runs regular EVM code. The client identifies a precompile by a reserved address mapped in that registry. + +```mermaid +flowchart TD + A[Call arrives at node] --> B{Target address in precompile registry?} + B -->|yes| C[Run native precompile] + B -->|no| D[Run regular EVM code] +``` + +Classic Ethereum precompiles (`ecrecover`, `sha256`, `ripemd160`, `modexp`, `ecadd`/`ecmul`/`ecpairing`, `blake2f`, and others) are looked up through this same registry. B20 does not bypass or extend the EVM dispatch path. It registers into that path. An address with no bytecode and no registry entry behaves like an empty account: the call returns immediately with no output. That is how a precompile address looks before the hardfork that introduces it. + +From the outside, the two paths look the same until the EVM reaches the target. The actor submits a transaction, the node validates and gossips it, the block builder executes it, and the EVM calls the contract address. A regular contract then runs bytecode. A precompile runs native client code. Both paths read and write EVM state. + +```mermaid +flowchart TB + classDef highlight fill:#fff3b0,stroke:#d4a017,color:#000 + + subgraph regular [Regular] + direction LR + RA[Actor] -->|submits tx| RN[Node] + RN -->|validate and gossip| RB[Block builder] + RB -->|executes tx| RE[EVM] + RE -->|call contract address| RC[Bytecode] + RC -->|read/write| RS[EVM state] + end + + subgraph precompile [Precompile] + direction LR + PA[Actor] -->|submits tx| PN[Node] + PN -->|validate and gossip| PB[Block builder] + PB -->|executes tx| PE[EVM] + PE -->|call contract address| PC[Native client code] + PC -->|read/write| PS[EVM state] + end + + class RC,PC highlight +``` ### 1.2 B20's Native Contract Model - B20 tokens, the Factory, the Policy Registry, and the Activation Registry are all precompiles: native logic hosted by the execution client at a fixed or derived address, not deployed EVM bytecode. From f2458325457effcf6d6b6ec6e05d2aad81782729 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 17:56:06 -0400 Subject: [PATCH 12/13] docs(architecture): describe precompile gas, dispatch, and EVM state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spell out that recognized precompiles run registered native code, meter gas and EVM-like errors themselves, and write straight into account storage — the same slots a contract would use. Co-authored-by: Cursor --- docs/architecture.md | 97 +++++++++++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index d7183ea5..18376ae7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # B20 Execution Architecture -*How B20 actually executes: how its precompiles differ from ordinary contracts, how calls get routed to them, and how the protocol evolves without breaking history. For what each primitive means and how to use it (assets, roles, policies), see [Concepts](concepts/). For the "B20 in 10 minutes" tour, see [Overview](overview.md).* +*How B20 actually executes: how its precompiles differ from ordinary contracts, how a token gets created and recognized as one, and how the protocol evolves without breaking history. For what each primitive means and how to use it (assets, roles, policies), see [Concepts](concepts/). For the "B20 in 10 minutes" tour, see [Overview](overview.md).* ## 1. How B20 Uses Precompiles @@ -48,56 +48,95 @@ flowchart TB class RC,PC highlight ``` -### 1.2 B20's Native Contract Model -- B20 tokens, the Factory, the Policy Registry, and the Activation Registry are all precompiles: native logic hosted by the execution client at a fixed or derived address, not deployed EVM bytecode. -- Unlike classic precompiles — pure, stateless functions — B20's precompiles are stateful: they hold persistent storage (balances, roles, policy IDs, pause state, ...) and emit real events. They behave as system contracts, not one-shot pure functions. -- Because they're stateful, they aren't cacheable the way a pure function's result would be — every call re-reads live storage. -- From the outside, calling a B20 precompile looks identical to calling a normal ERC-20 contract: same ABI encoding, same `CALL` semantics. The precompile nature is invisible above the EVM. +### 1.2 B20's Precompiles -### 1.3 B20 Address Space -- Fixed-address precompiles (Factory, Policy Registry, Activation Registry) sit at known, hardcoded addresses (`StdPrecompiles`). -- B20 token addresses are different — derived, not fixed, and self-describing: `0xB2` prefix + variant byte + `keccak256(sender, salt)` suffix, computed at creation time. -- The address alone answers "is this a B20 token" and "which variant" (`isB20`, `getB20Address`) — no external registry lookup needed to recognize one. +The Factory, the Policy Registry, the Activation Registry, and every B20 token are precompiles: native, stateful logic at a reserved address, not deployed bytecode. -### 1.4 How Calls Are Routed -- Fixed-address precompiles are matched directly in a static registry table. -- B20 token addresses can't be pre-registered individually — they're created at runtime, so there's no fixed list to check against. They're resolved through a dynamic fallback lookup that decodes the variant straight out of the address itself and builds the right dispatcher (Asset vs Stablecoin) on the fly. -- This is exactly why the address encoding in §1.3 exists — it's what makes dynamic routing possible without a token registry. +- **Factory** — creates B20 tokens through a single `createB20` entrypoint. +- **Policy Registry** — holds shared allowlists, blocklists, and composite policies that tokens query for authorization. +- **Activation Registry** — a Base-operated switch that turns Factory and token features on or off. +- **B20 token** — the asset itself: balances and transfers, plus roles, pause, mint, burn, seize, and policy checks. -### 1.5 Where State Lives -- State backing a B20 precompile lives in the execution client's own state — the same storage substrate as contract storage — not inside "fake bytecode." Reads and writes are metered with the same gas costs as native `SLOAD`/`SSTORE`. -- Each precompile (Factory, Policy Registry, each token) owns its own storage. There's no shared global state between tokens beyond what's explicitly referenced — e.g. a policy ID pointing at a shared entry in the Policy Registry. +B20 is the first stateful precompile on Base. Classic Ethereum precompiles are pure, stateless functions. B20's precompiles hold persistent storage and emit real events. They behave as system contracts, not one-shot pure functions. The storage they read and write is the same EVM state that regular contracts use. -### 1.6 How a B20 Call Executes -- Once a call is routed to a precompile: reject an unexpected value transfer (nonpayable) → charge calldata gas → resolve the active logic version for the current hardfork → decode the call against that version's frozen ABI → run the operation's checks (role, pause, policy) → mutate state → emit events. -- Gas isn't a single flat fee: a calldata cost plus metered storage/log costs, charged on the same schedule as native opcodes (warm/cold access, refunds included). A precompile that reports using more gas than the call's limit halts the call — the same outcome as running out of gas mid-execution. -- Activation gating happens inside this step, not by hiding the address: once a hardfork introduces a precompile, the address always exists from then on. An inactive feature makes specific write operations revert rather than removing the address from the registry — and deactivating a feature blocks *new* creation, it doesn't retroactively disable assets that already exist. +There are two kinds of B20 precompile: singletons and many-to-many. -## 2. How B20 Evolves +Singletons have one instance at a fixed address. The Factory, Policy Registry, and Activation Registry are registered in the node's static precompile table and matched there on every call. -### 2.1 Protocol Upgrades +Many-to-many precompiles share one native implementation across many addresses. Token addresses are created at runtime, so they cannot be entries in that fixed table. The node recognizes them dynamically by decoding the address itself. How that routing works is covered in [§2](#2-how-a-token-is-created). + +### 1.3 State and Execution + +Once the node recognizes the target as a precompile, it routes the call to the native code registered for that address. Bytecode is never loaded. + +That registered code is responsible for gas and for errors. It charges gas for calldata, `SLOAD`, `SSTORE`, and logs on the same schedule those opcodes would have paid. It also raises the same class of failures a contract would: out of gas, revert, and custom errors. Out of gas is out of gas. A revert restores EVM state the same way a normal contract revert does. + +The precompile charges gas, then decodes the calldata and runs the function that selector maps to. A `transfer` call runs transfer. A `createB20` call runs createB20. + +Those functions have state to update. Precompiles share state with the EVM: they write straight into the account storage at their own address, the same slots a contract would use. There is no side database. A `transfer` updates that token's balances. A later `balanceOf` or `eth_getStorageAt` is an `SLOAD` of what that write committed. + +```mermaid +flowchart TD + A["Call: transfer(Bob, 100)"] --> B + + subgraph rust [Rust precompile] + B[Charge gas] + B --> C[Decode calldata] + C --> D[Run transfer] + end + + subgraph evm [EVM state] + E[Alice balance] + F[Bob balance] + end + + D -->|"write −100"| E + D -->|"write +100"| F + E --> G["Views and nodes read the same slots"] + F --> G +``` + +Layout is [ERC-7201](https://eips.ethereum.org/EIPS/eip-7201) at the precompile's own address: a namespace root at `keccak256(namespace) - 1`, masked to a slot boundary, fields at fixed offsets from that root, and keyed data — balances, allowances — at `keccak256(key, slot)`, the same mapping formula Solidity uses. Each precompile writes only its own account. Tokens never share a storage account. Shared lists live on the Policy Registry; the token stores only a policy ID. + +Checks include activation, role, pause, and policy. Activation does not hide the address: once a hardfork introduces a precompile, the address stays in the routing table. Inactive writes revert with `FeatureNotActivated`. Reads stay available. Deactivating a variant blocks new Factory creation. Existing tokens keep running. + +## 2. How a Token Is Created + +### 2.1 Creating a Token +- Every token comes from one entrypoint on the Factory: `createB20(variant, salt, params, initCalls)`. +- The Factory computes the token's address deterministically, seals its identity (name, symbol, decimals, etc.), emits `B20Created`, grants the initial admin role (or skips it, to create an adminless token), then runs `initCalls` against the new token before returning its address. +- Once `createB20` returns, the Factory has no further access to the token — creation is a one-shot, one-transaction event. + +### 2.2 Recognizing a B20 Token +- The address itself encodes the answer: `0xB2` prefix + variant byte + `keccak256(sender, salt)` suffix. `isB20(address)` reads that encoding directly — no registry lookup needed. +- This is also how routing works after creation: since token addresses can't be pre-registered, the node resolves them through a dynamic lookup that decodes the variant straight from the address and dispatches to the right logic (Asset vs Stablecoin) on the fly. +- Before a token is created, its address behaves like any other unregistered address — calling it is a no-op, the same empty-account behavior described in §1.1. + +## 3. How B20 Evolves + +### 3.1 Protocol Upgrades - B20 changes ship as part of hardforks (e.g. Beryl → Cobalt) — the same mechanism that gates any other protocol-level change. - A hardfork can introduce an entirely new precompile (the Activation Registry itself only exists from Beryl onward) or a new logic version for an existing one. -### 2.2 Logic Versions +### 3.2 Logic Versions - Each precompile's logic is versioned. Once a version ships, it's frozen forever — self-contained, with no shared mutable state or traits across versions. - Why: editing logic in place at a fixed address would change execution for historical blocks too, breaking replay from genesis. Freezing is what preserves consensus. -### 2.3 Fork / Version Resolution +### 3.3 Fork / Version Resolution - A hardfork resolves to a specific logic version (fork → version enum → frozen implementation) — resolved once per call, never "whatever is current." - A call reverts if no version is resolved for the active fork (calling logic that doesn't exist yet), rather than silently falling back to a default. -### 2.4 Adding New Functions +### 3.4 Adding New Functions - New functionality ships as a new frozen version alongside the old ones — never by editing an existing version in place. - Additive-only guarantee: a hardfork can add selectors, events, and errors; it never removes or changes ones that already shipped. -### 2.5 ABI Evolution +### 3.5 ABI Evolution - The logic interface itself is append-only: new versions may add methods, never remove or change existing signatures. - Deprecated symbols are kept, not deleted (e.g. `burnBlocked`, the instant `updateMultiplier`) — old callers keep working unchanged. -### 2.6 Historical Execution +### 3.6 Historical Execution - Old transactions replay deterministically: the dispatcher resolves the version that was active *at that block's fork*, not "current" logic — so replaying history always re-executes the version that was live at the time. -### 2.7 Backwards Compatibility +### 3.7 Backwards Compatibility - Nothing that already shipped changes meaning — existing selectors, events, and errors keep their exact semantics across every later fork. - Consumers integrated against an old version keep working after a new version ships alongside it. They simply don't get new capabilities until they adopt the new ABI surface. From 25a22278346408d1957d4ed38b0fa02d79339e98 Mon Sep 17 00:00:00 2001 From: Rayyan Alam Date: Fri, 28 Aug 2026 18:46:47 -0400 Subject: [PATCH 13/13] docs: drop the implementer stub and add a guide template Remove the unfinished implementer page and its nav links, and add a compliance-guide template for the remaining how-to pages. Co-authored-by: Cursor --- README.md | 2 +- docs/README.md | 1 - docs/guides/implementers.md | 27 ------------------------- docs/guides/template.md | 40 +++++++++++++++++++++++++++++++++++++ docs/overview.md | 18 ----------------- 5 files changed, 41 insertions(+), 47 deletions(-) delete mode 100644 docs/guides/implementers.md create mode 100644 docs/guides/template.md diff --git a/README.md b/README.md index e5379b7e..6e50c616 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ A collection of Solidity interfaces, libraries, and mock implementations for Bas ## Documentation -See [`docs/`](docs/README.md) for the full documentation map: overview, architecture, audience guides (integrator/indexer/implementer), concepts, and reference. +See [`docs/`](docs/README.md) for the full documentation map: overview, architecture, audience guides (integrator/indexer), concepts, and reference. ## Changelog diff --git a/docs/README.md b/docs/README.md index 1a36d3c0..f513bd7c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,6 @@ Building something? - [Integrator Guide](guides/integrators.md) - [Indexer Guide](guides/indexers.md) -- [Implementer Guide](guides/implementers.md) Looking for exact technical details? diff --git a/docs/guides/implementers.md b/docs/guides/implementers.md deleted file mode 100644 index c40438ce..00000000 --- a/docs/guides/implementers.md +++ /dev/null @@ -1,27 +0,0 @@ -# Implementer Guide - -*Now that you understand B20 (see [Architecture](../architecture.md)), here's what you specifically need to care about as someone implementing or modifying B20 itself.* - -## Implementation boundaries - -_TODO_ - -## Version resolution - -_TODO — see [Versioning](../concepts/versioning.md) for the underlying model._ - -## Historical execution - -_TODO — how older hardfork versions must remain callable._ - -## Invariants - -_TODO — see [Architecture: Invariants](../architecture.md#invariants)._ - -## Testing - -_TODO — see the root [README: Test Integration](../../README.md#test-integration) and [Live precompile testing](../../README.md#live-precompile-testing)._ - -## Consensus considerations - -_TODO_ diff --git a/docs/guides/template.md b/docs/guides/template.md new file mode 100644 index 00000000..fe40d9fb --- /dev/null +++ b/docs/guides/template.md @@ -0,0 +1,40 @@ +# Configure Compliance for a B20 Asset + +## Goal + +Restrict transfers so only eligible holders can receive the asset. + +## Before You Start + +- You have a B20 asset +- You control the appropriate admin role + +## Steps + +1. Create an allowlist policy +2. Add eligible addresses +3. Attach the policy to the receiver scope +4. Test an allowed transfer +5. Test a denied transfer + +## Example + +... + +## Verify + +... + +## Common Errors + +... + +## Related Concepts + +- Policies +- Policy Registry + +## Reference + +- updatePolicy(...) +- createPolicy(...) \ No newline at end of file diff --git a/docs/overview.md b/docs/overview.md index 96fb7017..5641bde1 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,17 +1,3 @@ -“B20 in 10 Minutes.” Very readable, probably 5–10 minutes. - -It answers: - -What is B20? -Why does it exist? -How does the Factory create an asset? -How do roles and pause work? -How do compliance checks integrate? -Where should I go next? - -Someone should be able to read just this and explain B20 at a high level. - - # B20 Overview B20 is Base's native token standard for issuing and managing programmable assets onchain. @@ -207,10 +193,6 @@ If you are indexing B20: → [Indexer Guide](./guides/indexers.md) -If you are implementing B20: - -→ [Implementer Guide](./guides/implementers.md) - For exact interfaces and protocol definitions: → [Reference](./reference/)