Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions docs/ActivationRegistry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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`.
Expand All @@ -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`.
21 changes: 6 additions & 15 deletions docs/B20/Asset.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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).
Expand All @@ -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.
2 changes: 1 addition & 1 deletion docs/B20/Factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
6 changes: 3 additions & 3 deletions docs/B20/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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).

Expand Down Expand Up @@ -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 |
8 changes: 4 additions & 4 deletions docs/B20/Stablecoin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading