From 8781d34777f3e9e67ed6c980692fa4b817fccbf6 Mon Sep 17 00:00:00 2001 From: Conner Swenberg Date: Tue, 2 Jun 2026 09:49:23 -0400 Subject: [PATCH] docs: align B20 docs with interface specs and soften securities framing - ActivationRegistry: drop the B20_FACTORY row (it is a precompile-address handle in StdPrecompiles.sol, not a feature constant in ActivationRegistryFeatureList.sol); show the caller param in the mermaid FeatureActivated/FeatureDeactivated event labels - B20/Asset.md: describe the multiplier as a uniform rebase (drop split / reverse-split framing); recast the announcement example as a community-ratified rebase; note OPERATOR_ROLE gates metadata-like operations elevated to their own role due to higher compromise severity - B20/Factory.md, B20/README.md, B20/Stablecoin.md: terminology and wording consistency (policy scopes, self-declared currency code) Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/ActivationRegistry/README.md | 5 ++--- docs/B20/Asset.md | 21 ++++++--------------- docs/B20/Factory.md | 2 +- docs/B20/README.md | 6 +++--- docs/B20/Stablecoin.md | 8 ++++---- 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/docs/ActivationRegistry/README.md b/docs/ActivationRegistry/README.md index d8ba905..3d630a7 100644 --- a/docs/ActivationRegistry/README.md +++ b/docs/ActivationRegistry/README.md @@ -10,7 +10,6 @@ The canonical IDs in use today, defined in [`ActivationRegistryFeatureList`](../ | Constant | Preimage | Value | |---|---|---| -| `B20_FACTORY` | `"base.b20_factory"` | `0x78751e29c8bcc0d609ab18e9fbc4158e73f7db25ae2ee095dad42e2578b1e800` | | `B20_ASSET` | `"base.b20_asset"` | `0xcdcc772fe4cbdb1029f822861176d09e646db96723d4c1e82ddfdeb8163ef54c` | | `B20_STABLECOIN` | `"base.b20_stablecoin"` | `0xecfa0def2c10020caaf65e6155aa69c84b24892aaef76eeac52e0e2b3a0b8601` | | `POLICY_REGISTRY` | `"base.policy_registry"` | `0xb582ebae03f16fee49a6763f78df482fb11ae73f103ed0d330bbe556aa90a43f` | @@ -28,7 +27,7 @@ sequenceDiagram Admin->>ActivationRegistry: activate(featureId) Note over ActivationRegistry: features[featureId] = true - ActivationRegistry-->>Admin: emit FeatureActivated(featureId) + ActivationRegistry-->>Admin: emit FeatureActivated(feature, caller) ``` Reverts: `Unauthorized` (non-admin caller), `AlreadyActivated`, `DelegateCallNotAllowed` / `StaticCallNotAllowed`. @@ -44,7 +43,7 @@ sequenceDiagram Admin->>ActivationRegistry: deactivate(featureId) Note over ActivationRegistry: features[featureId] = false - ActivationRegistry-->>Admin: emit FeatureDeactivated(featureId) + ActivationRegistry-->>Admin: emit FeatureDeactivated(feature, caller) ``` Reverts: `Unauthorized`, `AlreadyDeactivated`, `DelegateCallNotAllowed` / `StaticCallNotAllowed`. diff --git a/docs/B20/Asset.md b/docs/B20/Asset.md index c819999..b979224 100644 --- a/docs/B20/Asset.md +++ b/docs/B20/Asset.md @@ -4,7 +4,7 @@ The Asset variant of B20 — designed for assets of all kinds. Everything in [B2 ## 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 rescale every balance — for splits, reverse-splits, or rebases — 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. +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. Read the current multiplier with `multiplier()`; the value is in WAD precision (`1e18`, exposed as `WAD_PRECISION()`). `toScaledBalance(rawBalance)` converts a raw amount to its scaled view, `toRawBalance(scaledBalance)` 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. @@ -25,25 +25,18 @@ Indexers should treat every `Announcement` log as the start of exactly one brack 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 execute a 2-for-1 forward split atomically. +// Disclose and apply a community-ratified rebase atomically. bytes[] memory internalCalls = new bytes[](1); internalCalls[0] = abi.encodeCall(IB20Asset.updateMultiplier, (newMultiplier)); IB20Asset(token).announce({ internalCalls: internalCalls, - id: "2026-Q3-split", - description: "2-for-1 forward split", + id: "2026-Q3-rebase", + description: "Community governance proposal #42: ratified rebase", uri: "https://disclosures.example.com/..." }); ``` -The two supply-action setters should be wrapped in `announce()`: - -- `updateMultiplier(...)` -- `batchMint(...)` - -Direct invocation by a role holder is permitted as an **emergency override** — it succeeds but produces no bracket events. Suitable only for break-glass scenarios where the inability to emit an announcement is itself part of the response. - ## 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). @@ -58,10 +51,8 @@ Each Asset token can carry an arbitrary set of named metadata entries — a gene ### `OPERATOR_ROLE` -Gates the two supply-action setters (`updateMultiplier`, `batchMint`) and the `announce` wrapper itself. Held separately from `DEFAULT_ADMIN_ROLE` so supply-action operators don't need full admin authority. Operationally paired with `METADATA_ROLE` — when granting one, you typically grant the other to the same address. +Gates the `updateMultiplier` and the `announce`. 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 common stablecoin uses and the floor most integrations expect; `18` is the ERC-20 community ceiling that every wallet and indexer renders correctly. - -The stablecoin variant is unchanged — it hardcodes `decimals()` to `6`. +`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 index b24a7c0..8c25b60 100644 --- a/docs/B20/Factory.md +++ b/docs/B20/Factory.md @@ -16,7 +16,7 @@ Variant-specific creation arguments, ABI-encoded as a versioned struct (one stru ### `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 slot wiring, 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. +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. Build the array with [`B20FactoryLib`](../../src/lib/B20FactoryLib.sol) helpers (or encode manually): diff --git a/docs/B20/README.md b/docs/B20/README.md index a6072d2..2171d65 100644 --- a/docs/B20/README.md +++ b/docs/B20/README.md @@ -24,7 +24,7 @@ Standard role taxonomy: | `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` | Sanctions-burns against policy-blocked accounts (`burnBlocked`) | +| `BURN_BLOCKED_ROLE` | Burns against policy-blocked accounts (`burnBlocked`) | | `PAUSE_ROLE` | `pause` | | `UNPAUSE_ROLE` | `unpause` | | `METADATA_ROLE` | `updateName`, `updateSymbol`, `updateContractURI` | @@ -54,7 +54,7 @@ Because scopes are per-actor, send-side and receive-side rules can be configured > ⚠️ **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 — typo'd scopes hard-revert rather than silently no-op'ing. +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). @@ -114,4 +114,4 @@ ERC-1271 contract signatures are deliberately NOT accepted — permit recovers v | Variant | Decimals | What it adds | |---|---|---| | [Asset](Asset.md) | 6-18 (configurable per token) | multiplier, announcements, extra metadata, batched issuance | -| [Stablecoin](Stablecoin.md) | 6 (fixed) | currency ISO code | +| [Stablecoin](Stablecoin.md) | 6 (fixed) | self-declared currency code | diff --git a/docs/B20/Stablecoin.md b/docs/B20/Stablecoin.md index fa74575..62e5fb5 100644 --- a/docs/B20/Stablecoin.md +++ b/docs/B20/Stablecoin.md @@ -2,12 +2,12 @@ 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. -## Fixed Decimals (6) - -`decimals()` is hard-wired to `6`. The choice matches existing popular stablecoins. - ## 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.